Как использовать UrlManager для настройки роутинга и создания «дружелюбных» URL
Здравствуйте, дорогие читатели!
Я продолжаю цикл статей о том, как мы разрабатывали нетипичный, крупный проект с использованием Yii2 framework и AngularJS.
В предыдущей статье я описал преимущества, выбранного нами стека технологий, и предложил модульную архитектуру нашего приложения.
В этом материале речь пойдет о настройке роутинга и создании URL при помощи urlManager для каждого модуля по отдельности. Также разложу по полочкам процесс создания собственных правил для специфических URL, с помощью написания класса, который расширяет UrlRuleInterface. В завершении опишу, как мы реализовали генерацию и вывод мета тегов для публичных страниц сайта.
Самое интересное под катом.
URL RulesЯ предполагаю, что вы, скорее всего, уже использовали UrlManager ранее, по крайней мере, для того, чтобы включить ЧПУ и скрыть index.php из URL.
//...
'urlManager' => [
'class' => 'yii\web\UrlManager',
'enablePrettyUrl' => true,
'showScriptName' => false,
],
/..
Но UrlManager может гораздо больше этого. Красивые URL-адреса оказывают большое влияние на выдачу сайта в поисковых системах. Также необходимо учитывать, что вы можете скрыть структуру вашего приложения, определив свои собственные правила.
«enableStrictParsing» => true — очень полезное свойство, которое ограничивает доступ только к правилам, которые уже настроены. В примере конфигурации маршрут www.our-site.com будет указывать на site/default/index, но www.our-site.com/site/default/index покажет страницу 404.
//...
'urlManager' => [
'class' => 'yii\web\UrlManager',
'enablePrettyUrl' => true,
'showScriptName' => false,
'enableStrictParsing' => true,
'rules' => [
'/' => 'site/default/index',
],
],
/..
Так как мы разделили приложение на модули и хотим, чтобы эти модули были максимально независимы друг от друга, URL правила могут быть добавлены динамически в URL менеджер. Это даст возможность распространять и переиспользовать модули, без необходимости донастраивать UrlManager, потому что модули будут управлять своими собственными URL правилами.
Для того чтобы динамически добавленные правила вступили в силу во время процесса маршрутизации, вы должны добавить их на стадии самонастройки. Для модулей это означает, что они должны имплементировать yii\base\BootstrapInterface и добавить правила в методе начальной загрузки bootstrap (), таким образом:
getUrlManager()->addRules(
[
// объявление правил здесь
'' => 'site/default/index',
'<_a:(about|contacts)>' => 'site/default/<_a>'
]
);
}
}
Файл Bootstrap.php с этим кодом мы добавляем в папку модуля /modules/site/
И такой файл у нас будет в каждом модуле, который будет добавлять свои Url правила.
Обратите внимание, что вы должны также перечислить эти модули в yii\web\Application: bootstrap (), чтобы они могли участвовать в процессе самонастройки. Для этого в файл /frontend/config/main.php перечислить модули в массиве bootstrap:
//...
'params' => require(__DIR__ . '/params.php'),
'bootstrap' => [
'modules\site\Bootstrap',
'modules\users\Bootstrap',
'modules\cars\Bootstrap'
'modules\lease\Bootstrap'
'modules\seo\Bootstrap'
],
];
Обратите внимание, что с момента написания первой статьи я добавил еще несколько модулей:
- modules/users — Модуль, в котором будут обрабатываться операции с пользователем и все страницы пользователя (регистрация, логин, личный кабинет)
- modules/cars — Модуль, в котором будут работать с базой данных брендов, марок, модификаций автомобилей.
- modules/lease — Модуль, в котором будут обрабатываться объявления, добавленные пользователями.
- modules/seo — Модуль для SEO. Тут будут храниться все компоненты и хелперы, которые нам будут помогать соответствовать SEO требованиям. О них я буду писать далее.
Несмотря на то, что стандартный класс yii\web\UrlRule является достаточно гибким для большинства проектов, есть ситуации, когда вы должны создать собственные классы правил.
Например, на веб-сайте автомобильного дилера, вы можете поддерживать формат URL, как /new-lease/state/Make-Model-Location/Year, где state, Make, Model, Year и Location должны соответствовать некоторым данным, хранящимся в таблице базы данных. Дефолтный класс здесь работать не будет, так как он опирается на статически объявленные паттерны.
Немного отвлечемся от кода, и я опишу вам суть задачи, которая стояла перед нами.
Согласно спецификации, нам необходимо было сделать следующие типы страниц с соответствующими им правилами формирования Url и meta тегов:
Страницы результатов поискаОни же в свою очередь делятся на три типа:
объявления от дилеров
url: /new-lease/(state)/(Make)-(Model)-(Location)
url: /new-lease/(state)/(Make)-(Model)-(Location)/(Year)
пользовательские объявления:
url: /lease-transfer/(state)/(Make)-(Model)-(Location)
url: /lease-transfer/(state)/(Make)-(Model)-(Location)/(Year)
Например: /new-lease/NY/volkswagen-GTI-New-York-City/2015
Результаты поиска, когда местоположение не указано в фильтре:
/(new-lease|lease-transfer)/(Make)-(Model)/(year)
Title: (Make) (Model) (Year) for Lease in (Location). (New Leases|Lease Transfers)
Например: Volkswagen GTI 2015 for Lease in New York City. Dealer Leases.
Keywords: (Make), (Model), (Year), for, Lease, in, (Location), (New, Leases|Lease, Transfers)
Description: List of (Make) (Model) (Year) in (Location) available for lease. (Dealer Leases|Lease Transfers).
Они же в свою очередь делятся на два типа:
объявление от дилера:
url: /new-lease/(state)/(make) — (model) — (year) — (color) — (fuel type) — (location) — (id)
пользовательские объявление:
url: /lease-transfer/(state)/(make) — (model) — (year) — (color) — (fuel type) — (location) — (id)
Title: (make) — (model) — (year) — (color) — (fuel type) for lease in (location)
Keywords: (year), (make), (model), (color), (fuel type), (location), for, lease
Description: (year) (make) (model) (color) (fuel type) for lease (location)
url: /i/(make) — (model) — (year)
Title: (make) — (model) — (year)
Keywords: (year), (make), (model)
Description: (year), (make), (model)
Это только часть тех страниц, которые нам необходимо было реализовать. Отрывок из технического задания приведен здесь для того, что вы понимали, чего нам нужно добиться от Url менеджера и насколько это нетривиальные правила.
Yii2 позволяет определить пользовательские URL через синтаксический анализ и логику генерации URL, сделав пользовательский класс UrlRule. Если вы хотите сделать свой собственный UrlRule вы можете либо скопировать код из yii\web\UrlRule и расширить его или, в некоторых случаях, просто имплементировать yii\web\UrlRuleInterface.
Ниже приведен код, который написан нашей командой для структуры URL, которые мы обсудили. Для него мы создали файл /modules/seo/components/UrlRule.php. Не считаю этот код эталоном, но уверен, что он однозначно выполняет поставленную задачу.
url = str_replace(' ', '_', substr($params['url'],1) );
$route->route = 'lease/search/index';
$route->params = json_encode(['make'=>$params['make'], 'model'=>$params['model'], 'location'=>$params['location'] ]);
$route->save();
return '/'.$params['url'];
}
}
if (isset($params['type']) && in_array($params['type'], ['user','dealer'])) {
$type = ($params['type'] == 'dealer')? 'new-lease' : 'lease-transfer';
} else {
return false;
}
if ((isset($params['zip']) && !empty($params['zip'])) || (isset($params['location']) && isset($params['state']))) {
// make model price zip type
if (isset($params['zip']) && !empty($params['zip'])) {
$zipdata = Zip::findOneByZip($params['zip']);
} else {
$zipdata = Zip::findOneByLocation($params['location'], $params['state']);
}
// city state_code
if (!empty($zipdata)) {
$url = $type . '/' . $zipdata['state_code'] . '/' . $params['make'] . '-' . $params['model'] . '-' . $zipdata['city'];
if (!empty($params['year'])) {
$url.='/'.$params['year'];
}
$url = str_replace(' ', '_', $url);
if($search_url = Route::findRouteByUrl($url)) {
return '/'.$url;
} else {
$route = new Route();
$route->url = str_replace(' ','_',$url);
$route->route = 'lease/search/index';
$pars = ['make'=>$params['make'], 'model'=>$params['model'], 'location'=>$zipdata['city'], 'state'=>$zipdata['state_code'] ]; //, 'zip'=>$params['zip'] ];
if (!empty($params['year'])) {
$pars['year']=$params['year'];
}
$route->params = json_encode($pars);
$route->save();
return $route->url;
}
}
}
if (isset($params['make'], $params['model'] )) {
$url = $type . '/' . $params['make'] . '-' . $params['model'] ;
if (!empty($params['year'])) {
$url.='/'.$params['year'];
}
$url = str_replace(' ', '_', $url);
if($search_url = Route::findRouteByUrl($url)) {
return '/'.$url;
} else {
$route = new Route();
$route->url = str_replace(' ','_',$url);
$route->route = 'lease/search/index';
$pars = ['make'=>$params['make'], 'model'=>$params['model'] ];
if (!empty($params['year'])) {
$pars['year']=$params['year'];
}
$route->params = json_encode($pars);
$route->save();
return $route->url;
}
}
}
return false;
}
/**
* Parse request
* @param \yii\web\Request|UrlManager $manager
* @param \yii\web\Request $request
* @return array|boolean
*/
public function parseRequest($manager, $request)
{
$pathInfo = $request->getPathInfo();
/**
* Parse request for search URLs with location and year
*/
if (preg_match('%^(?Please-transfer|new-lease)\/(?P[A-Za-z]{2})\/(?P[._\sA-Za-z-0-9-]+)\/(?P\d{4})?%', $pathInfo, $matches)) {
$route = Route::findRouteByUrl($pathInfo);
if (!$route) {
return false;
}
$params = [
'node' => $matches['url'] . '/' . $matches['year'],
'role' => $matches['role'],
'state' => $matches['state'],
'year' => $matches['year']
];
if (!empty($route['params'])) {
$params = array_merge($params, json_decode($route['params'], true));
}
return [$route['route'], $params];
}
/**
* Parse request for search URLs with location and with year
*/
if (preg_match('%^(?Please-transfer|new-lease)\/(?P[._\sA-Za-z-0-9-]+)\/(?P\d{4})%', $pathInfo, $matches)) {
$route = Route::findRouteByUrl($pathInfo);
if (!$route) {
return false;
}
$params = [
'node' => $matches['url'] . '/' . $matches['year'],
'role' => $matches['role'],
'year' => $matches['year']
];
if (!empty($route['params'])) {
$params = array_merge($params, json_decode($route['params'], true));
}
return [$route['route'], $params];
}
/**
* Parse request for leases URLs and search URLs with location
*/
if (preg_match('%^(?Please-transfer|new-lease)\/(?P[A-Za-z]{2})\/(?P[_A-Za-z-0-9-]+)?%', $pathInfo, $matches)) {
$route = Route::findRouteByUrl([$matches['url'], $pathInfo]);
if (!$route) {
return false;
}
$params = [
'role' => $matches['role'],
'node' => $matches['url'],
'state' => $matches['state']
];
if (!empty($route['params'])) {
$params = array_merge($params, json_decode($route['params'], true));
}
return [$route['route'], $params];
}
/**
* Parse request for search URLs without location and year
*/
if (preg_match('%^(?Please-transfer|new-lease)\/(?P[._\sA-Za-z-0-9-]+)?%', $pathInfo, $matches)) {
$route = Route::findRouteByUrl($pathInfo);
if (!$route) {
return false;
}
$params = [
'node' => $matches['url'],
'role' => $matches['role'],
];
if (!empty($route['params'])) {
$params = array_merge($params, json_decode($route['params'], true));
}
return [$route['route'], $params];
}
/**
* Parse request for Information pages URLs
*/
if (preg_match('%^i\/(?P[_A-Za-z-0-9-]+)?%', $pathInfo, $matches)) {
$route = Route::findRouteByUrl($matches['url']);
if (!$route) {
return false;
}
$params = Json::decode($route['params']);
$params['node'] = $route['url'];
return [$route['route'], $params];
}
return false;
}
}
Для того, чтобы его использовать, нужно только добавить этот класс в массив правил yii\web\UrlManager::$rules.
Для этого создадим файл Bootstrap.php в модуле /modules/seo (по аналогии с файлом Bootstrap.php в модуле /modules/site) и объявим в нем такое правило:
//...
public function bootstrap($app)
{
$app->getUrlManager()->addRules(
[
[
'class' => 'modules\seo\components\UrlRule,
],
]
);
}
/..
Это специальное правило предназначено для очень конкретного случая использования. Мы не планируем повторно использовать это правило в других проектах, поэтому оно не имеет настроек.
Так как правило не настраивается, нет необходимости расширять от yii\web\UrlRule, yii\base\Object, или от чего-нибудь еще. Просто достаточно имплементировать интерфейс yii\web\UrlRuleInterface. Потому как мы не планируем повторно использовать это правило в наших переиспользуемых модулях, мы его определили в SEO модуле.
parseRequest () просматривает маршрут, и если он совпадает с регулярным выражением в условии, он разбирает дальше, чтобы извлечь параметры.
В этом методе мы используем вспомогательную модель Route, в которой в поле url хранятся сформированные ссылки. По ним и происходит поиск на соответствие методом findRouteByUrl. Этот метод возвращает нам одну запись с таблицы (в случае, если такая имеется) с полями:
- url — часть поискового запроса, по которому нашлась запись,
- route — маршрут, которому надо передать управление,
- params — дополнительные, параметры в формате JSON строки, которые необходимо передать в действие для дальнейшей работы.
parseRequest () возвращает массив с действием и параметрами:
[
‘lease/search/view’,
[
'node' => new-lease/NY/ volkswagen-GTI-New-York-City/2016,
'role' => ‘new-lease’,
'state' => ‘NY’,
'year' => ‘2016’
]
]
Иначе возвращает false, чтобы указать UrlManager, что он не может разобрать запрос.
createUrl () строит URL из предоставленных параметров, но только если URL был предложен для действий lease/lease/view, cars/info/view или lease/search/view.
Из соображений производительностиПри разработке сложных веб-приложений важно оптимизировать правила URL так, чтобы синтаксический анализ запросов и создания URL занимал меньше времени.
При анализе или создании URL-адреса, URL-менеджер анализирует правила URL в том порядке, в котором они были объявлены. Таким образом, вы можете рассмотреть возможность корректировки порядка правил URL так, чтобы более определенные и/или часто используемые правила размещались перед менее использованными правилами.
Часто встречается, что ваше приложение состоит из модулей, каждый из которых имеет свой собственный набор правил URL с module ID, как и их общий префикс.
Генерация и вывод мета теговДля того чтобы генерировать и выводить meta теги в определенном формате для указанных типов страниц, был написан специальный хелпер, который разместился в файле modules/seo/helpers/Meta.php. В нем содержится следующий код:
view->registerMetaTag(['name' => 'keywords','content' => 'lease, car, transfer']);
Yii::$app->view->registerMetaTag(['name' => 'description','content' => 'Carvoy - Change the way you lease! Lease your next new car online and we\'ll deliver it to your doorstep.']);
break;
case 'lease':
$title = $model->make . ' - ' . $model->model . ' - ' . $model->year . ' - ' . $model->exterior_color . ' - ' . $model->engineFuelType . ' for lease in ' . $model->location;
Yii::$app->view->registerMetaTag(['name' => 'keywords','content' => Html::encode($model->year . ', ' . $model->make . ', ' . $model->model . ', ' . $model->exterior_color . ', ' . $model->engineFuelType . ', ' . $model->location . ', for, lease')]);
Yii::$app->view->registerMetaTag(['name' => 'description','content' => Html::encode($model->year . ' ' . $model->make . ' ' . $model->model . ' ' . $model->exterior_color . ' ' . $model->engineFuelType . ' for lease in ' . $model->location)]);
break;
case 'info_page':
$title = $model->make . ' - ' . $model->model . ' - ' . $model->year;
Yii::$app->view->registerMetaTag(['name' => 'keywords','content' => Html::encode($model->year . ', ' . $model->make . ', ' . $model->model)]);
Yii::$app->view->registerMetaTag(['name' => 'description','content' => Html::encode($model->year . ' ' . $model->make . ' ' . $model->model)]);
break;
case 'search':
if ($model['role'] == 'd') $role = 'Dealer Lease';
elseif ($model['role'] == 'u') $role = 'Lease Transfers';
else $role = 'All Leases';
if (isset($model['make']) && isset($model['model'])) {
$_make = (is_array($model['make']))? (( isset($model['make']) && ( count($model['make']) == 1) )? $model['make'][0] : false ) : $model['make'];
$_model = (is_array($model['model']))? (( isset($model['model']) && ( count($model['model']) == 1) )? $model['model'][0] : false ) : $model['model'];
$_year = false;
$_location = false;
if (isset($model['year'])) {
$_year = (is_array($model['year']))? (( isset($model['year']) && ( count($model['year']) == 1) )? $model['year'][0] : false ) : $model['year'];
}
if (isset($model['location'])) {
$_location = (is_array($model['location']))? (( isset($model['location']) && ( count($model['location']) == 1) )? $model['location'][0] : false ) : $model['location'];
}
if ( ($_make || $_model) && !(isset($model['make']) && ( count($model['make']) > 1)) ) {
$title = $_make . (($_model)? ' ' . $_model : '') . (($_year)? ' ' . $_year : '') . ' for Lease' . (($_location)? ' in ' . $_location . '. ' : '. ') . $role . '.';
} else {
$title = 'Vehicle for Lease' . (($_location)? ' in ' . $_location . '. ' : '. ') . $role . '.';
}
Yii::$app->view->registerMetaTag(['name' => 'keywords','content' => Html::encode( ltrim($_make . (($_model)? ', ' . $_model : '') . (($_year)? ', ' . $_year : '') . ', for, Lease' . (($_location)? ', in, ' . $_location : '') . ', ' . implode(', ', (explode(' ', $role))), ', ') ) ]);
Yii::$app->view->registerMetaTag(['name' => 'description','content' => Html::encode( 'List of '. ((!$_model && !$_make)? 'Vehicles' : '') . $_make . (($_model)? ' ' . $_model : '') . (($_year)? ' ' . $_year : '') . (($_location)? ' in ' . $_location : '') . ' available for lease. ' . $role . '.' )]);
} else {
$title = 'Search results';
}
break;
}
return $title;
}
}
Используем этот хелпер в view страницы, для которой необходимо установить meta теги. Например, для страницы просмотра объявления, добавляем следующую строку в файл /modules/lease/views/frontend/lease/view.php
//...
$this->title = \modules\seo\helpers\Meta::all('lease', $model);
/..
Первым параметром в метод мы передаем тип страницы, для которой генерируются meta теги. Вторым параметром передается модель текущего объявления.
Внутри метода происходит генерация мета тегов в зависимости от типа страницы и добавление их в head с помощью метода registerMetaTag класса yii\web\View
Метод возвращает нам сгенерированную строку для тега title. Таким образом, через свойство $title класса yii\web\View, мы задаем заголовок страницы.
Спасибо за внимание!
Материал подготовлен: greebn9k (Сергей Грибняк), pavel-berezhnoy (Павел Бережной), silmarilion (Андрей Хахарев)
Комментарии (3)
1 сентября 2016 в 12:13
–1↑
↓
Существующие классы yii2 UrlManager «из коробки» дают возможность быстро «на коленке» набросать необходимые правила и подходят исключительно только для случаев dev-окружения. Либо для случаев когда технические возможности платформы позволяют не обращать внимания на нагрузку (RAM, CPU) которую они создают.В случае если разработчик ставит перед собой задачу оптимизации издержек своего yii2 приложения, он так или иначе будет вынужден писать свой собственный класс для URL Manager.
1 сентября 2016 в 13:40
+1↑
↓
можно написать один UrlRule, покрывающий все кейсы.1 сентября 2016 в 13:56
0↑
↓
Странно, в моей практике URL manager ещё не становился ни разу узким местом приложения.