Yii2 Routing – UrlManager

Chances are you’ve probably used UrlManager before, at least to enable “Pretty URLs” and hide index.php from the URL.

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

It can do much more than that. Learn how to get the most of it in this tutorial.

URL Rules

User friendly URLs are great, search engine friendly URLs are even better. If that’s not enough consider the fact that you can hide the structure of your app by defining your own rules. ‘enableStrictParsing’ => true, is a very useful addition – it limits the access only to the rules that are configured. In the example configuration the www.example.org route will point to site/index, but the www.example.org/site/index will show a 404 page.

'urlManager' => [
    'class' => 'yii\web\UrlManager',
    'enablePrettyUrl' => true,
    'showScriptName' => false,
    'enableStrictParsing' => true,
    'rules' => [
        '/' => 'site/index',
        'login' => 'site/login',
        'logout' => 'site/logout',
        'signup' => 'site/signup',
        'request-password-reset' => 'site/request-password-reset',
        'reset-password' => 'site/reset-password',
    ],
],

I always set ‘enableStrictParsing’ => true, , for security reasons, better discipline and a for a convenient way to see what is exposed to the public.

Rules with parameters

URLs that pass parameters like /view?id=1 can be converted to /view/1 with a simple rule such as this.

'view/<id:\d+>' => 'post/view',

You can then generate the customized URLs using yii\helpers\Url.

echo Url::to('post/view', ['id' => 1]);

Or generate links with help from yii\helpers\Html.

<?= Html::a('Click Me', ['post/view', 'id' => 1]) ?>

This is what these helpers were made for in the first place. Now if you change the rule (e.g. change /view/1 to /show/1), all your generated URLs will update automatically.

Pagination

Pagination is a perfect use case. Note that you also need an additional rule if you want your route to be accessible without any parameters.

'new/<page:\d+>' => 'site/new',
'new' => 'site/new',

It is possible to make it work in just one rule, by defining defaults.

[
    'pattern' => '/new/<page:\d+>',
    'route' => 'site/new',
    'defaults' => [
        'page' => 1
    ],
],

But then we will encounter a problem. Our helpers and widgets will stop generating correct URLs for routes without the page parameter because they won’t be able to find an appropriate rule from our rule list. So we’re back to the two rule method.

'new/<page:\d+>' => 'site/new',
'new' => 'site/new',

Not bad, considering we have to type less than for the method with the defaults.

But what if you need complex routes like this one:

/jobs/civil_engineers,bridge_engineers/florida,orlando/2

URL Rule Hell

I had to configure UrlManager for such a structure and it took some time. Every combination of parameters must be described for every route to be accessible and generateble. Not only that, but they must be defined in that order or they simply won’t work.

'jobs/<category:\w+>,<subcategory:\w+>/<state:\w+>,<city:\w+>/<page:\d>' => 'site/jobs',
'jobs/<category:\w+>,<subcategory:\w+>/<state:\w+>/<page:\d>' => 'site/jobs',
'jobs/<category:\w+>,<subcategory:\w+>/<page:\d>' => 'site/jobs',
'jobs/<category:\w+>/<state:\w+>,<city:\w+>/<page:\d>' => 'site/jobs',
'jobs/<category:\w+>/<state:\w+>/<page:\d>' => 'site/jobs',
'jobs/<category:\w+>/<page:\d>' => 'site/jobs',

'jobs/<category:\w+>,<subcategory:\w+>/<state:\w+>,<city:\w+>' => 'site/jobs',
'jobs/<category:\w+>,<subcategory:\w+>/<state:\w+>' => 'site/jobs',
'jobs/<category:\w+>,<subcategory:\w+>' => 'site/jobs',
'jobs/<category:\w+>/<state:\w+>,<city:\w+>' => 'site/jobs',
'jobs/<category:\w+>/<state:\w+>' => 'site/jobs',
'jobs/<category:\w+>' => 'site/jobs',

'jobs' => 'site/jobs',

If you define them in a different order you will start getting classic URLs or a mix of both like /jobs/civil_engineers?state=florida.

This happens because UrlManager goes through the list and if it finds a rule that it deems appropriate – it uses that rule to generate the URL. All of these routes are handled by the site/jobs action which is part of the reason for the confusion.

Notice how I’ve put the more complex routes first. This is because UrlManager ignores the rules that require parameters that are not provided to the UrlManager. If we will ask it to generate a URL for a route without a page parameter it will ignore the first 6 rules. If the rules were in a different order UrlManager might have stopped on a different rule an the resulting URL would end in ?page=2 instead of /2.

That is why the order for rules is important.

There is another way to achieve the same of even better results.

Custom URL Rules

Yii2 allows us to define custom URL 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 from yii\web\UrlRule, extend from it or in some cases just implement yii\web\UrlRuleInterface. Below is the code that I wrote for the URL structure discussed previously. I haven’t done a benchmark yet, but I’m pretty sure one rule will work much faster than 13 rules. You might want to click the “Open code in new window” button. It’s easier to read that way.

<?php
namespace frontend\components;

use Yii;
use yii\web\UrlRuleInterface;

class JobsUrlRule implements UrlRuleInterface
{
    /**
     * Parses the given request and returns the corresponding route and parameters.
     * @param \yii\web\UrlManager $manager the URL manager
     * @param \yii\web\Request $request the request component
     * @return array|boolean the parsing result. The route and the parameters are returned as an array.
     * If false, it means this rule cannot be used to parse this path info.
     */
    public function parseRequest($manager, $request)
    {
        $pathInfo = $request->getPathInfo();
        //This rule only applies to paths that start with 'jobs'
        if (strpos($pathInfo, 'jobs') !== 0) {
            return false;
        }
        //controller/action that will handle the request
        $route = 'site/jobs';
        //parameters in the URL (category, subcategory, state, city, page)
        $params = [];

        $parameters = explode('/', $pathInfo);

        if (count($parameters) > 1) {
            $categoryParameters = explode(',', $parameters[1]);
            $params['category'] = $categoryParameters[0];
            $params['subcategory'] = count($categoryParameters) > 1 ? $categoryParameters[1] : '';
        }

        if (count($parameters) > 2 ) {
            //The page number can come after the category, subcategory information
            if (is_numeric($parameters[2])) {
                $params['page'] = (int) $parameters[2];
            } else {
                $locationParameters= explode(',', $parameters[2]);
                $params['state'] = $locationParameters[0];
                $params['city'] = count($locationParameters) > 1 ? $locationParameters[1] : '';
            }

        }
        //Or the page number can be last
        if (count($parameters) > 3 ) {
            $params['page'] = (int) $parameters[3];
        }

        if (count($parameters) > 4 ) {
            return false;
        }

        Yii::trace("Request parsed with URL rule: site/jobs", __METHOD__);

        return [$route, $params];
    }

    /**
     * Creates a URL according to the given route and parameters.
     * @param \yii\web\UrlManager $manager the URL manager
     * @param string $route the route. It should not have slashes at the beginning or the end.
     * @param array $params the parameters
     * @return string|boolean the created URL, or false if this rule cannot be used for creating this URL.
     */
    public function createUrl($manager, $route, $params)
    {
        if ($route !== 'site/jobs') {
            return false;
        }
        //If a parameter is defined and not empty - add it to the URL
        $url = 'jobs/';
        if (array_key_exists('category', $params) && !empty($params['category'])) {
            $url .= $params['category'];
        }
        if (array_key_exists('subcategory', $params) && !empty($params['subcategory'])) {
            $url .= ',' . $params['subcategory'];
        }
        if (array_key_exists('state', $params) && !empty($params['state'])) {
            $url .= '/' . $params['state'];
        }
        if (array_key_exists('city', $params) && !empty($params['city'])) {
            $url .= ',' . $params['city'];
        }
        if (array_key_exists('page', $params) && !empty($params['page'])) {
            $url .= '/' . $params['page'];
        }

        return $url;
    }
}

To use it I only had to add this line to the rule array.

['class' => 'frontend\components\CategoryUrlRule'],

This custom rule is designed for a very specific use case. I don’t plan on reusing this rule which is why it’s not configurable. Since it’s not configurable – there is no need to extend from yii\web\UrlRule, yii\base\Object, or anything else. Just implementing the interface is enough. parseRequest() scans the route and if it starts with jobs it gets broken down further to extract the parameters. Returns false to tell UrlManager that it can’t parse the request. Otherwise returns an array with an action and parameters.

array (size=2)
  0 => string 'site/jobs' (length=9)
  1 => 
    array (size=5)
      'category' => string 'civil_engineers' (length=15)
      'subcategory' => string 'bridge_engineers' (length=16)
      'state' => string 'florida' (length=7)
      'city' => string 'orlando' (length=7)
      'page' => int 2

createUrl() builds a URL from provided parameters, but only if the URL was requested for the site/jobs action. Don’t be afraid of writing your own URL rules for complex routing schemes. As long as you understand how UrlManager works and test your code well it should work even better than with the standard UrlRule.

Published by

Alexander

Your friendly, neighborhood, full stack, pseudorandom text generator

11 thoughts on “Yii2 Routing – UrlManager”

  1. Hello , I Think you lost name of method
    in this section
    class JobsUrlRule implements UrlRuleInterface
    {
    $pathInfo = $request->getPathInfo();

    should be like this
    class JobsUrlRule implements UrlRuleInterface
    {
    public function parseRequest($request)
    {
    $pathInfo = $request->getPathInfo();

  2. Hi! Thanks for tutorial
    For some reasons I do have this error

    Unable to find ‘frontend\rules\CategoryUrlRule’ in file: /Applications/MAMP/htdocs/yii2/advanced/frontend/rules/CategoryUrlRule.php. Namespace missing?

    Any ideas why?

    1. Hi! It should work if you put namespace frontend\rules; in the very begging of CategoryUrlRule.php

      My CategoryUrlRule.php had the following path frontend\componentsCategoryUrlRule.php hence namespace frontend\components;

    1. This kind of structure is very much possible. You can always make a Custom URL Rule that queries the database to see if a product or post with such a slug exists and returns the correct controller/action.

    2. hi

      you cant fix it:

      in config main.php

      ‘rules’ => [
      ‘article//page-‘ => ‘article/cate’,

      in view

      <a href=" $val[‘slug’],’page’=>1]); ?>”> test

Leave a Reply