Система алиасов

14 min read

В этой статье рассмотрим метод, который позволяет создавать красивые адреса для страниц сайта. Посмотрите на адрес этой статьи в браузере. Как видите, адрес не содержит указатель на раздел статей и идентификатор конкретной статьи, а представляет собой лишь краткое и приглядное слово. Хотите такие же маршруты для своих сайтов? Тогда читайте дальше.

Этот метод называется системой алиасов. Она создается для того, чтобы в адресной строке заменять адрес вида «example.com/profile.php?id=2365» или «example.com/article/1234» более привлекательными словами.

Например, подобная система работает на сайте американского каталога стартапов AngelList:

Адрес может быть статьей, профилем пользователя, категорией, и вообще всем, чем угодно.

Что в итоге мы получим?

Адреса вида

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. Подписывайтесь на группу нашего клуба — мы продолжим рассказывать о наших экспериментах и разработках.

Благодарю за внимание!