📜 ⬆️ ⬇️

How to use UrlManager to set up routing and create “friendly” URLs


Hello dear readers! I continue the series of articles on how we developed an atypical, large-scale project using the Yii2 framework and AngularJS.

In the previous article, I described the benefits of the technology stack we chose, and proposed the modular architecture of our application .

This article focuses on setting up routing and creating a URL using urlManager for each module separately. I will also sort out the process of creating your own rules for specific URLs by writing a class that extends UrlRuleInterface. In conclusion, I will describe how we implemented the generation and output of meta tags for public pages of the site.
')
The most interesting under the cut.

URL Rules


I assume that you most likely have already used UrlManager earlier, at least to turn on the CNC and hide index.php from the URL.

//... 'urlManager' => [ 'class' => 'yii\web\UrlManager', 'enablePrettyUrl' => true, 'showScriptName' => false, ], /.. 

But UrlManager can do much more than that. Beautiful URLs have a great influence on the site’s search engine results. You also need to consider that you can hide the structure of your application by defining your own rules.

'enableStrictParsing' => true is a very useful property that restricts access only to rules that are already configured. In the configuration example, the route www.our-site.com will point to site / default / index, but www.our-site.com/site/default/index will show page 404.

 //... 'urlManager' => [ 'class' => 'yii\web\UrlManager', 'enablePrettyUrl' => true, 'showScriptName' => false, 'enableStrictParsing' => true, 'rules' => [ '/' => 'site/default/index', ], ], /.. 

Since we have divided the application into modules and want these modules to be as independent as possible from each other, URL rules can be added dynamically to the URL manager. This will make it possible to distribute and reuse modules without having to tweak UrlManager, because modules will manage their own URLs with rules.

In order for dynamically added rules to take effect during the routing process, you must add them during the self-tuning stage. For modules, this means that they must implement yii \ base \ BootstrapInterface and add rules in the bootstrap () bootstrap method, thus:

 <?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>' ] ); } } 

The file Bootstrap.php with this code is added to the module folder / modules / site /. And we will have such a file in every module that will add its own Url rules.

Note that you must also list these modules in yii \ web \ Application :: bootstrap () so that they can participate in the self-tuning process. To do this, in the /frontend/config/main.php file, list the modules in the bootstrap array:

 //... 'params' => require(__DIR__ . '/params.php'), 'bootstrap' => [ 'modules\site\Bootstrap', 'modules\users\Bootstrap', 'modules\cars\Bootstrap' 'modules\lease\Bootstrap' 'modules\seo\Bootstrap' ], ]; 

Please note that since writing the first article I have added a few more modules:


Custom URL Rules


Although the standard class yii \ web \ UrlRule is quite flexible for most projects, there are situations where you have to create your own rule classes.

For example, on a car dealer website, you can support a URL format like / new-lease / state / Make-Model-Location / Year, where state, Make, Model, Year and Location must match some data stored in the base table data. The default class will not work here, since it relies on statically declared patterns.

A little distracted from the code, and I will describe to you the essence of the task that confronted us.

According to the specification, we needed to make the following types of pages with the corresponding rules for creating Url and meta tags:

Search results pages


They, in turn, are divided into three types:

Dealer Ads
url: / new-lease / (state) / (Make) - (Model) - (Location)
url: / new-lease / (state) / (Make) - (Model) - (Location) / (Year)

Custom ads:
url: / lease-transfer / (state) / (Make) - (Model) - (Location)
url: / lease-transfer / (state) / (Make) - (Model) - (Location) / (Year)

For example: / new-lease / NY / volkswagen-GTI-New-York-City / 2015

Search results when the location is not specified in the filter:
/ (new-lease | lease-transfer) / (Make) - (Model) / (year)

Title: (Make) (Model) (Year) for Lease in (Location). (New Leases | Lease Transfers)
For example: 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).

Ad Review Page


They, in turn, are divided into two types:

Dealer ad:
url: / new-lease / (state) / (make) - (model) - (year) - (color) - (fuel type) - (location) - (id)

Custom ad:
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)

Information pages about the car


url: / i / (make) - (model) - (year)
Title: (make) - (model) - (year)
Keywords: (year), (make), (model)
Description: (year), (make), (model)

This is only part of the pages that we needed to implement. An excerpt from the technical assignment is given here so that you understand what we need to achieve from the Url manager and how far these are non-trivial rules.

Yii2 allows you to define custom URLs through parsing and URL generation logic by making a custom UrlRule class. If you want to make your own UrlRule, you can either copy the code from yii \ web \ UrlRule and expand it or, in some cases, simply implement yii \ web \ UrlRuleInterface.

Below is the code that was written by our team for the URL structure we discussed. For it, we created the file / modules/seo/components/UrlRule.php. I do not consider this code a standard, but I am sure that it definitely performs the task.

 <?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 Pages create 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 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('%^(?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; } } 

In order to use it, you only need to add this class to the array of rules yii \ web \ UrlManager :: $ rules.

To do this, create the Bootstrap.php file in the / modules / seo module (similar to the Bootstrap.php file in the / modules / site module) and declare the following rule in it:
 //... public function bootstrap($app) { $app->getUrlManager()->addRules( [ [ 'class' => 'modules\seo\components\UrlRule, ], ] ); } /.. 

This special rule is intended for a very specific use case. We do not plan to reuse this rule in other projects, so it has no settings.

Since the rule is not configurable, there is no need to extend from yii \ web \ UrlRule, yii \ base \ Object, or from anything else. Just implement the interface yii \ web \ UrlRuleInterface. Because we do not plan to reuse this rule in our reusable modules, we defined it in the SEO module.

parseRequest () scans the route, and if it matches the regular expression in the condition, it parses further to extract the parameters.

In this method, we use the auxiliary Route model, in which the generated links are stored in the url field. According to them, the search for compliance with the findRouteByUrl method takes place. This method returns us one record from the table (in case there is one) with fields:


parseRequest () returns an array with an action and parameters:

 [ 'lease/search/view', [ 'node' => new-lease/NY/ volkswagen-GTI-New-York-City/2016, 'role' => 'new-lease', 'state' => 'NY', 'year' => '2016' ] ] 

Otherwise, it returns false to indicate to the UrlManager that it cannot parse the request.

createUrl () builds a URL from the provided parameters, but only if the URL has been proposed for action lease / lease / view, cars / info / view or lease / search / view.

For performance reasons


When developing complex web applications, it is important to optimize URL rules so that it takes less time to parse requests and create URLs.

When analyzing or creating a URL, the URL manager analyzes the URL rules in the order in which they were declared. Thus, you can consider adjusting the order of URL rules so that more specific and / or frequently used rules are placed before less used rules.

It is often found that your application consists of modules, each of which has its own set of URL rules with a module ID, as well as their common prefix.

Generate and output meta tags


In order to generate and display meta tags in a specific format for the specified page types, a special helper was written, which is located in the modules / seo / helpers / Meta.php file. It contains the following code:

 <?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 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; } } 

We use this helper in the view page for which you want to set meta tags. For example, for the ad viewing page, add the following line to the file / modules/lease/views/frontend/lease/view.php

 //... $this->title = \modules\seo\helpers\Meta::all('lease', $model); /.. 

The first parameter to the method is the type of page for which meta tags are generated. The second parameter is the current ad model.

Meta tags are generated inside the method depending on the page type and added to the head using the registerMetaTag method of the yii \ web \ View class. The method returns us the generated string for the title tag. Thus, through the $ title property of the yii \ web \ View class, we set the page title.

Thanks for attention!

Material prepared: greebn9k (Sergey Gribnyak), pavel-berezhnoy (Pavel Berezhnoy), silmarilion (Andrey Khakharev)

Source: https://habr.com/ru/post/308948/


All Articles