В этой статье рассмотрим метод, который позволяет создавать красивые адреса для страниц сайта. Посмотрите на адрес этой статьи в браузере. Как видите, адрес не содержит указатель на раздел статей и идентификатор конкретной статьи, а представляет собой лишь краткое и приглядное слово. Хотите такие же маршруты для своих сайтов? Тогда читайте дальше.
Этот метод называется системой алиасов. Она создается для того, чтобы в адресной строке заменять адрес вида «example.com/profile.php?id=2365» или «example.com/article/1234» более привлекательными словами.
Например, подобная система работает на сайте американского каталога стартапов AngelList:
- https://angel.co/meerkat — страница компании
- https://angel.co/itai-danino — профиль пользователя
- https://angel.co/php — раздел с вакансиями
- https://angel.co/russia— рубрика по стране
- https://angel.co/moscow — рубрика города
Адрес может быть статьей, профилем пользователя, категорией, и вообще всем, чем угодно.
Что в итоге мы получим?
Адреса вида
http://example.com/title-of-substancе
Вместо
http://example.com/substance/id
title-of-substance и есть наш алиас, и он будет указывать на конкретную сущность.
Например, адрес страницы которую вы сейчас читаете, указывает на эту статью:
https://ifmo.su/alias-system
Как работает обычная система роутинга
Для начала рассмотрим обычную систему роутинга — задания структурированных адресов страниц, понятных для людей (ЧПУ). ЧПУ (от жаргонного «человеко-понятный урл» ) — это веб-адрес, содержащий читаемые слова вместо параметров запроса метода GET в адресной строке браузера. Этот подход широко распространен в интернете, он позволяет получить адреса вида «example.com/article/23» вместо «examle.com/article.php?id=23»
Идея в том, что все запросы перенаправляются веб-сервером в один файл, точку входа. В apache2 это можно сделать с помощью файла .htaccess и mod_rewrite, а в nginx с помощью задания location в конфигурационном файле. Далее, в этой точке входа адрес сравнивается с шаблоном из списка роутов и вызывается соответствующий скрипт для дальнейшей обработки запроса.
Пример списка роутов (файл Routes.php)
array(
'article/<id>' => array(
'controller' => 'Articles.php',
'action' => 'showArticle',
),
'user/<id>' => array(
'controller' => 'Profiles.php',
'action' => 'showProfile',
),
);
Как работает система алиасов
Решение
На схеме видно, что мы создаем централизованный роут, который будет определять с какой сущностью мы будем работать. Для этого создадим специальную таблицу с названием «Aliases». В ней нам нужно хранить алиас, тип ресурса и уникальный идентификатор ресурса. Это позволит нам понять, какой контроллер нужно вызвать и с каким ресурсом работать.
Структура таблицы
uri type id
где uri — строка из адреса type — тип сущности, например: «Статья» , «Профиль пользователя» и так далее. Хранится в виде цифровых констант id — указатель на сущность, то есть его идентификатор в базе данных
Могут возникнуть проблемы, когда одному названию соответствует несколько сущностей. Например, «example.com/victory» может быть и пользователем, и статьей и любой другой сущностью. Решается это добавлением индексов к алиасу - «example.com/victory-1»
Ограничения
Нужно заранее составить список зарезервированных адресов, которые могут быть использованы системой для обработки внутренних запросов, таких как: admin, auth, login, signup и так далее. Такие адреса будем считать системными.
Особенности реализации
Таблица алиасов будет довольно быстро разрастаться, следовательно, искать сохраненный алиас в таблице по строковому полю типа может быть не эффективно. Намного эффективнее искать по бинарному хэшу. Для осуществления этого добавим в таблицу Aliases еще одно поле — «hash» с типом данных.
Реализация
Самое время рассмотреть примеры реализации основных функций. Выбор фреймворка или языка не принципиален, но в данной статье мы будем работать с «Kohana Framework».
Создадим новый роут в файле «bootstrap.php». Он должен находиться в начале списка. Делается это для того, чтобы все запросы в первую очередь проходили через него. Добавим к этому роуту функцию-фильтр, которая будет проверять, является ли адрес системным. Если такой адрес найден в табличке «ForbiddenAliases» (или в другом месте, где вы храните список системных роутов), то данный адрес не будет обрабатываться системой алиасов. В этом случае за него будут отвечать обычные обработчики маршрутов.
Назовем роут «URI» с шаблоном . В данном случае, — это все, что находится в адресе сайта после первого слэша. Например, в адресе «ifmo.su/telegram-bot» в параметр попадет строка «telegram-bot».
Route::set('URI', '<route>')
->filter(function (Route $route, $params, Request $request) {
$alias = $params['route'];
$model_uri = Model_Uri::Instance();
if ( $model_uri->isForbidden($alias) ) {
return false;
}
})->defaults(array(
'controller' => 'Uri',
'action' => 'get',
));
С помощью функции-фильтра проверяем, не является ли роут системным. Для этого ищем его в списке запрещенных адресов. Если данный адрес в нем не найден, то полученные из запроса параметры передаем контроллеру «Uri» в его метод «get».
class Controller_Uri extends Controller {
public function action_get()
{
$route = $this->request->param('route');
$sub_action = $this->request->param('subaction');
$model_alias = new Model_Alias();
/**
* Getting Controller, action and ID of substance
*/
$realRequest = $model_alias->getRealRequestParams( $route, $sub_action );
Метод «getRealRequestParams» возвращает массив состоящий из элементов - «controller», «action» и «id», которые соответствуют сущности. Например, «example.com/aliases» является статьей, а не профилем пользователя. Следовательно, в массиве будут храниться контроллер и экшен, который работают со статьями.
public function getRealRequestParams($route, $sub_action = null)
{
$model_uri = Model_Uri::Instance();
$alias = $this->getAlias( $route );
if ( empty($alias) )
throw new HTTP_Exception_404();
return array(
'controller' => 'Controller_' . $model_uri->controllersMap[$alias['type']],
'action' => 'action_show',
'id' => $alias['id'],
);
}
Мы решили, что тип сущности в базе данных будем хранить в виде числовых констант. Теперь, нам нужно создать файл, где мы будем хранить соответствия этих констант названиям контроллеров, отвечающих за данный раздел.
/**
* Controllers
*/
const ARTICLE = 1;
const USER = 2;
/**
* Specifies controller to each URI type
*/
public $controllersMap = array(
self::ARTICLE => 'Articles',
self::USER => 'Users',
Например, мы нашли алиас в таблице и получили массив с такими элементами:
array(
'uri' => 'aliases',
'type' => 1,
'id' => 1234,
);
В карте контроллеров, в переменной $controllersMap, единица соответствует статьям, следовательно мы должны передать идентификатор ресурса скрипту, который обрабатывает статьи.
Результатом выполнения функции «getRealRequestParams» при запросе «https://ifmo.su/alias-system» будет:
array (
'controller' => 'Controller_Articles',
'action' => 'show',
'id' => 74,
);
Таким способом можно создавать разные карты. Например, карту методов «ActionsMap» для реализации роутов вида «/alias/edit» и других. Для этого нужна переменная «subaction».
Route('URI', '<route>(/<subaction>)');
Но в этой статье мы рассматриваем только один метод - «show».
После того, как мы выяснили с каким ресурсом мы будем работать, в контроллере «Uri» создадим экземпляр контроллера, название которого получили с помощью «getRealRequestParams». Далее передаем конструктору этого класса параметры запроса.
$realRequest = $model_alias->getRealRequestParams( $route, $sub_action );
$controller_name = $realRequest['controller'];
$action_name = $realRequest['action'];
$Controller = new $controller_name( $this->request, $this->response );
Устанавливаем идентификатор страницы.
/**
* Set ID as query param
* In actions use $this->request->query('id') instead of $this->request->param('id')
*/
$this->request->query('id', $realRequest['id']);
Вызываем нужный нам метод. Функции before() и after() вызываются до и после основного экшена в контроллере, для того, чтобы наш вызываемый контроллер прошел стандартный цикл запроса before() → controller → after(). Например, выполнил проверку авторизации пользователей и другие общие методы, выполняемые до и после обработки контроллера.
/**
* Now just execute real action in initial Request instance
*/
$Controller->before();
$Controller->$action_name();
$Controller->after();
Готово! Теперь вы можете обращаться к ресурсам вашего сайта через алиасы.
Реализуем доступ к ресурсу по нескольким адресам
Если вы хотите чтобы ваши ресурсы также были доступны по старым адресам вида «example.com/article/1234», то вам достаточно добавить слово «article» в список запрещенных роутов.
Также, к одному конкретному ресурсу можно обращаться через разные алиасы. Например, адреса «ifmo.su/aliases» и «ifmo.su/alias-system» ссылаются на эту статью. Это полезно при обновлении статей и других ресурсов, чтобы обеспечить к ним доступ по старым ссылкам, которые могли быть опубликованы в интернете.
В таблицу «Aliases» добавим еще одно поле — «deprecated» с типом . Каждая запись будет иметь по умолчанию «deprecated» = 0. Это означает, что алиас является основным для конкретного ресурса, и занять этот адрес нельзя ( см. Коллизии ), а «deprecated» = 1 значит, что данный ресурс имеет более актуальный адрес. В таком случае, алиас могут занять другие сущности.
Рассмотрим функцию, которая генерирует уникальный адрес для определенной страницы. Этот метод проверяет, существует ли в базе данных такой алиас. Если алиас найден и помечен как неактуальный с помощью поля «deprecated», то мы можем его занять и удалить старую запись. Если алиас найден и актуален, то дописываем к нему индекс или “указатель”, который сделает его уникальным.
public static function generateAlias($route)
{
$alias = self::getAlias( $route );
$hashedRoute = md5( $route, true );
/**
* Setting $newAlias [String] = $route as default until we looking for new unengaged alias.
*/
$newAlias = $route;
if ( isset( $alias ) && Arr::get($alias, 'deprecated') ) {
self::deleteAlias($hashedRoute);
return $newAlias;
} elseif ( !empty($alias) && !Arr::get($alias, 'deprecated') ) {
for($index = 1; ; $index++) {
$newAlias = $newAlias.'-'.$index;
$aliasExist = self::getAlias($newAlias);
if ( empty($aliasExist) ) {
return $newAlias;
break;
} else {
$newAlias = $route;
}
}
} elseif ( !empty( $alias ) ) {
$newAlias = $route;
}
return $newAlias;
}
Рассмотрим метод, который обновляет алиас. Мы договорились, что в базе будем хранить хэш от алиаса. Поле «hash» в таблице «Aliases» имеет тип , следовательно, нам нужно записать хэш в бинарном виде. Переменной «hashedRoute» присвоим бинарный хэш.
public static function updateAlias($oldAlias = null, $alias, $type, $id)
{
$hashedRoute = md5($alias, true);
$hashedOldRoute = md5($oldAlias);
$update = DB::update()
->set('deprecated', 1)
->where('hash', '=', $hashedOldRoute)
->execute();
return self::addAlias($alias, $type, $id);
}
Когда мы редактируем ресурс, нам нужно обновить в таблице поле «deprecated». После того, как мы изменили это поле, создадим новый алиас с типом сущности этого ресурса и с его идентификатором. Теперь ресурс доступен по 2 адресам. Таким образом реализуется поддержка нескольких алиасов одним ресурсом, но при этом есть риск, что старый адрес могут занять, потому что он считается свободным, а текущий (обновленный) — занятым.
Результат
В итоге мы получили модуль системы алиасов. Осталось прописать их при добавлении новых ресурсов. Нам было достаточно добавить строки:
$alias = Model_Alias::generateUri( $uri );
$article->uri = Model_Alias::addAlias($alias, Model_Uri::ARTICLE, $article_id);
В функцию «generateUri» передаем ключевое слово. В нашем случае это поле «uri» формы добавления статей. На выходе мы получим свободный алиас, и запишем его в табличку. Теперь наша статья доступна по сгенерированному алиасу.
А вот пример обновления статьи:
$alias = Model_Alias::generateUri( $uri );
$article->uri = Model_Alias::updateAlias($article->uri, $alias, Model_Uri::ARTICLE, $article_id);
Систему алиасов можно внедрить в любую архитектуру. Если вы используете какой-нибудь другой фреймворк, то вам достаточно знать, как работает роутинг в вашей системе. Посмотреть пример реализации такой системы можно по адресу: https://github.com/codex-team/kohana-aliases. Подписывайтесь на группу нашего клуба — мы продолжим рассказывать о наших экспериментах и разработках.
Благодарю за внимание!