Як використовувати 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(), таким чином:

<?php
namespace modules\site;
use yii\base\BootstrapInterface;

class Bootstrap implements BootstrapInterface
{
/**
* @inheritdoc
*/
public function bootstrap($app)
{
$app->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 вимогам. Про них я буду писати далі.
Користувальницькі URL Rules
Незважаючи на те, що стандартний клас 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. Не вважаю цей код еталоном, але впевнений, що він однозначно виконує поставлене завдання.

<?php
namespace modules\seo\components;
use modules\seo\models\Route;
use modules\zipdata\models\Zip;
use yii\helpers\Json;
use Yii;
use yii\web\UrlRuleInterface;

class UrlRule implements UrlRuleInterface
{
public function createUrl($manager, $route, $params)
{
/**
* Lease module create urls
*/
if ($route === 'lease/lease/view'){
if (isset($params['state'], $params['node'], $params['role'])) {
$role = ($params['role'] == 'dealer') ? 'new-lease' : 'lease-transfer';
return $role . '/' . $params['state'] . '/' . $params['node'];
}
}
if ($route === 'lease/lease/update') {
if (isset($params['state'], $params['node'], $params['role'])) {
$role = ($params['role'] == 'dealer') ? 'new-lease' : 'lease-transfer';
return $role . '/' . $params['state'] . '/' . $params['node'] . '/edit/update';
}
}

/**
* Information Pages create urls
*/
if ($route === 'cars/info/view') {
if (isset($params['node'])) {
return 'i/' . $params['node'];
}
}

/**
* Search create Pages urls
*/
if ($route === 'lease/search/view') {
if (!empty($params['url'])) {
$params['url'] = str_replace(' ', '_', $params['url']);
if($search_url = Route::findRouteByUrl($params['url'])) {
return '/'.$params['url'];
} else {
$route = new Route();
$route->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 price model 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('%^(?P<role>lease-transfer|new-lease)\/(?P<state>[A-Za-z]{2})\/(?P<url>[._\sA-Za-z-0-9-]+)\/(?P<year>\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('%^(?P<role>lease-transfer|new-lease)\/(?P<url>[._\sA-Za-z-0-9-]+)\/(?P<year>\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('%^(?P<role>lease-transfer|new-lease)\/(?P<state>[A-Za-z]{2})\/(?P<url>[_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('%^(?P<role>lease-transfer|new-lease)\/(?P<url>[._\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<url>[_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. У ньому міститься наступний код:

<?php
namespace modules\seo\helpers;
use Yii;
use yii\helpers\Html;

/**
* @package modules\seo\helpers
*/
class Meta
{
/**
* Генерує meta теги title, keywords, description і повертає рядок Заголовка сторінки.
*
* @param string $type Тип сторінки, для якої генеруються meta теги
* @param object $model
* @return string $title Заголовок сторінки
*/
public static function all($type, $model = null)
{
$title = 'Carvoy | A new generation of leasing a car!'; // Заголовок сторінки за замовчуванням.

switch ($type) {
case 'home':
$title = 'Carvoy | A new generation of leasing a car!';
Yii::$app->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 поріг.']);
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 (Андрій Хахарев)
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.