Статьи

Создание собственного URL Shortener

Большинство из нас знакомы с такими URL-адресами, как bit.ly или t.co, в наших каналах Twitter или Facebook. Это примеры сокращенных URL-адресов, которые представляют собой короткий псевдоним или указатель на более длинную ссылку на страницу. Например, я могу отправить вам сокращенный URL-адрес http://bit.ly/SaaYw5 , который перенаправит вас на очень длинный URL-адрес Google с результатами поиска о том, как гладить рубашку. Было бы намного проще отправить 20-символьный URL-адрес bit.ly вашему сыну, который учится в колледже и готовится к своему первому собеседованию.

В этой статье вы узнаете, как создать полнофункциональный сокращатель URL для вашего сайта, который будет работать независимо от того, используете ли вы фронт-контроллер / фреймворк или нет. Если вы используете фронт-контроллер, я расскажу вам, как легко интегрировать этот укороченный URL-адрес без необходимости углубляться в программирование контроллера.

Отвечая на некоторые распространенные вопросы

Так что с bit.ly и многими другими сокращителями URL, такими как он есть и свободно доступными, зачем нам создавать свои собственные? Большинство этих сокращающих сервисов даже имеют простой в использовании API, так что мы можем программно генерировать сокращенный URL и использовать его в наших PHP-скриптах.

Наилучшие причины — удобство, эстетика и узнаваемость бренда. Если, например, на вашем веб-сайте есть приложение, которое создает большое количество отчетов, очень активный блог или большой фотоальбом, будет много ссылок. Укороченный URL-адрес позволит вам программно создать простую и понятную ссылку, которую можно отправить по электронной почте вашим читателям или опубликовать на вашем веб-сайте. Очевидным преимуществом наличия собственного является то, что ваши читатели мгновенно узнают ваш бренд на вашем сайте.

Вы можете удивиться, почему вы всегда видите буквы, смешанные с цифрами, в сокращенных URL. Имея более десяти опций (0-9) на цифру, мы можем иметь значительно больше комбинаций, сохраняя при этом максимально короткий код.

Символы, которые мы будем использовать, это цифры 1-9 вместе с различными заглавными и строчными буквами. Я удалил все гласные, чтобы предотвратить создание ссылок, которые являются непреднамеренными плохими словами, и я удалил все символы, которые могут быть перепутаны друг с другом. Это дает нам список из примерно 50 символов, доступных для каждой цифры, что означает, что с двумя символами у нас есть 2500 возможных комбинаций, 125 000 возможностей с тремя символами и колоссальные 6,5 миллиона комбинаций всего с четырьмя символами!

Планирование базы данных

Давайте short_urls таблицу short_urls . Это простая таблица, и оператор создания находится ниже:

 CREATE TABLE IF NOT EXISTS short_urls ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, long_url VARCHAR(255) NOT NULL, short_code VARBINARY(6) NOT NULL, date_created INTEGER UNSIGNED NOT NULL, counter INTEGER UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY (id), KEY short_code (short_code) ) ENGINE=InnoDB; 

У нас есть стандартный автоинкрементный первичный ключ и поля для полного URL-адреса, сокращенный код для URL-адреса (индексированный для более быстрого поиска), временная метка, когда была создана строка, и количество обращений к короткому URL-адресу.

Обратите внимание, что поле long_url имеет максимальную длину 255 символов, что должно быть достаточно для большинства приложений. Если вам нужно хранить более длинные URL-адреса, вам нужно изменить его определение на TEXT .

Теперь перейдем к PHP!

Создание краткого кода URL

Код для создания и декодирования коротких кодов URL будет в классе с именем ShortUrl. Сначала давайте посмотрим на код, отвечающий за создание коротких кодов:

 <?php class ShortUrl { protected static $chars = "123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ"; protected static $table = "short_urls"; protected static $checkUrlExists = true; protected $pdo; protected $timestamp; public function __construct(PDO $pdo) { $this->pdo = $pdo; $this->timestamp = $_SERVER["REQUEST_TIME"]; } public function urlToShortCode($url) { if (empty($url)) { throw new Exception("No URL was supplied."); } if ($this->validateUrlFormat($url) == false) { throw new Exception( "URL does not have a valid format."); } if (self::$checkUrlExists) { if (!$this->verifyUrlExists($url)) { throw new Exception( "URL does not appear to exist."); } } $shortCode = $this->urlExistsInDb($url); if ($shortCode == false) { $shortCode = $this->createShortCode($url); } return $shortCode; } protected function validateUrlFormat($url) { return filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED); } protected function verifyUrlExists($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_NOBODY, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_exec($ch); $response = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return (!empty($response) && $response != 404); } protected function urlExistsInDb($url) { $query = "SELECT short_code FROM " . self::$table . " WHERE long_url = :long_url LIMIT 1"; $stmt = $this->pdo->prepare($query); $params = array( "long_url" => $url ); $stmt->execute($params); $result = $stmt->fetch(); return (empty($result)) ? false : $result["short_code"]; } protected function createShortCode($url) { $id = $this->insertUrlInDb($url); $shortCode = $this->convertIntToShortCode($id); $this->insertShortCodeInDb($id, $shortCode); return $shortCode; } protected function insertUrlInDb($url) { $query = "INSERT INTO " . self::$table . " (long_url, date_created) " . " VALUES (:long_url, :timestamp)"; $stmnt = $this->pdo->prepare($query); $params = array( "long_url" => $url, "timestamp" => $this->timestamp ); $stmnt->execute($params); return $this->pdo->lastInsertId(); } protected function convertIntToShortCode($id) { $id = intval($id); if ($id < 1) { throw new Exception( "The ID is not a valid integer"); } $length = strlen(self::$chars); // make sure length of available characters is at // least a reasonable minimum - there should be at // least 10 characters if ($length < 10) { throw new Exception("Length of chars is too small"); } $code = ""; while ($id > $length - 1) { // determine the value of the next higher character // in the short code should be and prepend $code = self::$chars[fmod($id, $length)] . $code; // reset $id to remaining value to be converted $id = floor($id / $length); } // remaining value of $id is less than the length of // self::$chars $code = self::$chars[$id] . $code; return $code; } protected function insertShortCodeInDb($id, $code) { if ($id == null || $code == null) { throw new Exception("Input parameter(s) invalid."); } $query = "UPDATE " . self::$table . " SET short_code = :short_code WHERE id = :id"; $stmnt = $this->pdo->prepare($query); $params = array( "short_code" => $code, "id" => $id ); $stmnt->execute($params); if ($stmnt->rowCount() < 1) { throw new Exception( "Row was not updated with short code."); } return true; } ... 

Когда мы создаем экземпляр нашего класса ShortUrl , мы передаем ему наш экземпляр объекта PDO. Конструктор сохраняет эту ссылку и устанавливает член $timestamp .

Мы вызываем метод urlToShortCode() передавая ему длинный URL, который мы хотим сократить. Метод оборачивает все необходимое для создания короткого URL-кода, который мы добавим к нашему доменному имени.

urlToShortCode() вызывает validateUrlFormat() который просто использует PHP-фильтр, чтобы убедиться, что URL правильно отформатирован. Затем, если статическая переменная $checkUrlExists имеет значение true, будет verifyUrlExists() , которая использует cURL для связи с URL-адресом и убедитесь, что она не возвращает ошибку 404 (не найдено). В качестве альтернативы вы можете проверить состояние 200 (ОК), но это может вызвать проблемы, если страница неожиданно вернет код ответа 301 (перемещено) или 401 (неавторизовано).

Не имеет смысла иметь дублирующиеся записи, поэтому код проверяет это с помощью функции urlExistsInDb() которая запрашивает у базы данных длинный URL-адрес. Если он находит URL, он возвращает соответствующий короткий код, в противном случае он возвращает false, поэтому мы знаем, что нам нужно его создать. Обратите внимание, что http://www.example.com и http://example.com — это разные URL-адреса, поэтому, если вы хотите предотвратить такое дублирование, вам придется добавить некоторые регулярные выражения.

createShortCode() делегирует следующие задачи определенным методам:

  1. insertUrlInDb() для вставки длинного URL в базу данных и возврата идентификатора новой строки.
  2. convertIntToShortCode() для преобразования идентификатора новой строки в нашу схему чисел base-50.
  3. insertShortCodeInDb() для обновления строки вновь созданным коротким кодом.

Когда мы хотим создать короткий URL, все, что нам нужно сделать, — это создать экземпляр класса, передать экземпляр PDO в конструктор, вызвать метод urlToShortCode() с длинным URL, который мы хотим сократить, и добавить возвращенный короткий код к домен и передать его обратно в контроллер, который запросил его.

 <?php include "../include/config.php"; include "../include/ShortUrl.php"; try { $pdo = new PDO(DB_PDODRIVER . ":host=" . DB_HOST . ";dbname=" . DB_DATABASE, DB_USERNAME, DB_PASSWORD); } catch (PDOException $e) { trigger_error("Error: Failed to establish connection to database."); exit; } $shortUrl = new ShortUrl($pdo); try { $code = $shortUrl->urlToShortCode($_POST["url"]); printf('<p><strong>Short URL:</strong> <a href="%s">%1$s</a></p>', SHORTURL_PREFIX . $code); exit; } catch (Exception $e) { // log exception and then redirect to error page. header("Location: /error"); exit; } 

Расшифровка короткого кода

Код для декодирования короткого кода и получения длинного URL-адреса также является частью класса ShortUrl . Мы вызываем метод shortCodeToUrl() и передаем ему короткий код, извлеченный из URI. shortCodeToUrl() также принимает необязательный параметр $increment , который по умолчанию имеет значение true. Затем он делегирует следующее:

  1. validateShortCodeFormat() гарантирует, что предоставленный короткий код содержит только буквы и цифры.
  2. getUrlFromDb() запрашивает базу данных о предоставленном кратком коде и возвращает поля id записи, long_url и counter.
  3. Если параметр $increment имеет значение true, вызывается incrementCounter() для увеличения значения поля счетчика строки.

Вот остальная часть класса:

 ... public function shortCodeToUrl($code, $increment = true) { if (empty($code)) { throw new Exception("No short code was supplied."); } if ($this->validateShortCode($code) == false) { throw new Exception( "Short code does not have a valid format."); } $urlRow = $this->getUrlFromDb($code); if (empty($urlRow)) { throw new Exception( "Short code does not appear to exist."); } if ($increment == true) { $this->incrementCounter($urlRow["id"]); } return $urlRow["long_url"]; } protected function validateShortCode($code) { return preg_match("|[" . self::$chars . "]+|", $code); } protected function getUrlFromDb($code) { $query = "SELECT id, long_url FROM " . self::$table . " WHERE short_code = :short_code LIMIT 1"; $stmt = $this->pdo->prepare($query); $params=array( "short_code" => $code ); $stmt->execute($params); $result = $stmt->fetch(); return (empty($result)) ? false : $result; } protected function incrementCounter($id) { $query = "UPDATE " . self::$table . " SET counter = counter + 1 WHERE id = :id"; $stmt = $this->pdo->prepare($query); $params = array( "id" => $id ); $stmt->execute($params); } } 

Объединяя все вместе

Сборка / изменение фронт-контроллера или адаптация этого пакета к существующей платформе выходят за рамки этой статьи, и поэтому я решил r.php нашу логику декодирования в файл с именем r.php (r r.php перенаправление). Мы можем написать наши сокращенные URL-адреса как http://example.com/r/X4c, где r.php (или r/index.php зависимости от вашего дизайна) будет контроллером. Этот формат будет легко интегрировать практически в любой каркас, не затрагивая существующий фронт-контроллер.

В связи с этим, если вы хотите узнать, как создавать свои собственные передние контроллеры, посмотрите отличную серию «Введение в шаблон передних контроллеров» .

Одним из преимуществ такой конструкции является то, что при желании вы можете иметь отдельный контроллер для разных частей вашего сайта, используя разные таблицы, чтобы сохранить короткие коды, организованные и максимально короткие. http://example.com/b/ может быть для сообщений в блоге, а http://example.com/i/ может быть для изображений.

«Но что, если я не использую фронт-контроллер или фреймворк?», Спросите вы «Я просто прочитал всю эту статью даром?». Хотя это не так красиво, вы можете использовать формат http://example.com/ r? c = X4c, где r/index.php содержит скрипт декодирования.

Вот как выглядит r.php :

 <?php include "../include/config.php"; include "../include/ShortUrl.php"; // How are you getting your short code? // from framework or front controller using a URL format like // http://.example.com/r/X4c // $code = $uri_data[1]; // from the query string using a URL format like // http://example.com/r?c=X4c where this file is index.php in the // directory http_root/r/index.php $code = $_GET["c"]; try { $pdo = new PDO(DB_PDODRIVER . ":host=" . DB_HOST . ";dbname=" . DB_DATABASE, DB_USERNAME, DB_PASSWORD); } catch (PDOException $e) { trigger_error("Error: Failed to establish connection to database."); exit; } $shortUrl = new ShortUrl($pdo); try { $url = $shortUrl->shortCodeToUrl($code); header("Location: " . $url); exit; } catch (Exception $e) { // log exception and then redirect to error page. header("Location: /error"); exit; } 

В зависимости от того, как вы получаете короткий код, переменная $code устанавливается вместе с другими вашими настройками конфигурации. Мы устанавливаем наше соединение PDO, ShortUrl экземпляр экземпляра ShortUrl и вызываем shortCodeToUrl() передавая ему короткий код и оставляя счетчик, устанавливающий значение по умолчанию. Если короткий код действителен, у вас будет длинный URL-адрес, на который вы сможете перенаправить пользователя.

В заключение

Итак, у вас есть собственный сокращатель URL, который невероятно легко добавить на ваш существующий сайт. Конечно, существует множество способов улучшить этот пакет, таких как:

  • Абстрактное взаимодействие с вашей базой данных для удаления избыточного кода.
  • Добавить способ кеширования сокращенных URL-запросов.
  • Добавьте некоторую аналитику в запрошенные короткие URL-адреса за пределами поля counter .
  • Добавить способ отфильтровывать вредоносные страницы.

Я хотел бы воспользоваться этой возможностью, чтобы поблагодарить Тимоти Борончика за его терпеливый совет на протяжении всего моего процесса написания. Для меня было честью написать эту статью для SitePoint и поработать с ним.

Не стесняйтесь размещать пример кода этой статьи на GitHub и делиться своим вкладом и улучшениями.

Спасибо за чтение и счастливого PHPing!

Изображение через Fotolia