Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

How to Start Development of a Large Project With Yii2 (Part 2)

DZone's Guide to

How to Start Development of a Large Project With Yii2 (Part 2)

Continuing with my series of articles on how I developed a complicated, large-scale project using the Yii2 framework and AngularJS, this time around, we’ll talk about routing set-up and URL generation for each individual module using UrlManager.

· Web Dev Zone ·
Free Resource

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Hey, folks!

Today, I’m going to continue my series of articles on how I (and my team) developed a complicated, large-scale project using the Yii2 framework and AngularJS. In my previous article, I described the benefits of the technology stack we opted for and the modular structure of our application.

In this article, we’ll talk about routing set-up and URL generation for each individual module using UrlManager. Also, I’ll break down the details on how to create custom rules for specific URLs by utilizing classes to enhance UrlRuleInterface capabilities. To finish the article, I’ll describe how we implemented meta tags generation and meta tags output features to the website’s public pages.

URL Rules

I’m quite sure that you’ve used UrlManager before. After all, you should have activated user-friendly URLs and hidden index.php from URLs at least once in your life, right? So, you won’t find anything strange about the following code:

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

However, you can do lots of stuff with UrlManager. And the code above is just a mere example of what it’s capable of.

Neat URLs are SEO-friendly. You can easily hide your app’s structure by setting up your own rules by ‘enableStrictParsing’ => true. Use it when you need to restrict access to your set-up clusters of rules.

In the configuration’s example is the route for www.our-site.com that aims at site/default/index, but when you try www.our-site.com/site/default/index, a 404 Error page will pop up. Let’s have a look at the code below.

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

As we’ve divided our application in multiple modules and want these modules to be as independent from each other as possible, we can add URL rules to UrlManager dynamically. It’ll allow us to distribute and repurpose the modules, not having to set UrlManager once again.

To activate dynamically added rules during routing, you need to add them on the bootstrapping stage. It means that the modules need to implement yii/base/BootstrapInterface and add the rules at the bootstrapping stage bootstrap() the following way:

namespace modules\site;
use yii\base\BootstrapInterface;
class Bootstrap implements BootstrapInterface
{
   /**
    * @inheritdoc
    */
   public function bootstrap($app)
   {
       $app->getUrlManager()->addRules(
           [
               // declare the rules here
               '' => 'site/default/index',
               '<_a:(about|contacts)>' => 'site/default/<_a>'
           ]
       );
   }
}

After that, we add Bootstrap.php with the code above to the module’s folder /modules/site/. We need to create similar files for every module to add/store their own URL rules.

To note, you should also specify these files in yii/web/Application::bootstrap(). This way you include them into the bootstrapping process. Specify all the necessary modules in Bootstrap’s /frontend/config/main.php:

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

I’d like to point out that I’ve added several additional modules since I published the previous article. Those modules are:

modules/users – A module which is responsible for user-related operations and all user-related pages such as registration page, login, personal account, etc.

modules/cars – A module created specifically for cars’ database. It’s responsible for makes, models, trims, etc.

modules/lease – A module for processing user-added offers and requests (leases).

modules/seo – An SEO module. It stores all components and helpers that enable search engine optimization features in the app. I’ll talk about them more later.

User-Related URL Rules

Though a standard class yii/web/UrlRule is rather flexible when used with the overwhelming majority of projects, sometimes you need to create your own rule classes. For instance, you can support the following format on a car-leasing website: /new-lease/state/Make-Model-Location/Year—where state, Make, Model, Year, and Location should correspond to data which is stored in the database table. The default class won’t be valid here as it depends on statically defined patterns.

Here’s the list of pages:

  • Search results. There are three types of them:

    • Car dealer offers (car dealer leases)
      url: /new-lease/(state)/(Make)-(Model)-(Location)
      url: /new-lease/(state)/(Make)-(Model)-(Location)/(Year)

    • User offers (user leases)
      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/(new-lease|lease-transfer)/(Make)-(Model)/(year)

    • (Search results when location isn’t specified in the filter)
      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).

  • Offer pages (Lease pages). There are three types of them as well:

    • Dealer’s offer view (dealer’s lease view)
      url: /new-lease/(state)/(make) - (model) - (year) - (color) - (fuel type) - (location) - (id)

    • User’s offer view (user’s lease view)
      url: /lease-transfer/(state)/(make) - (model) - (year) - (color) - (fuel type) - (location) - (id)

  • Car-focused info pages

    • url: /i/(make) - (model) - (year)

    • Title: (make) - (model) - (year)

    • Keywords: (year), (make), (model)

    • Description: (year), (make), (model)

Below you’ll find the code for URL structure created by our developers. To store the code, we created /modules/seo/components/UrlRule.php file. I’ll be frank here: I don’t think that our code is perfect; however, it does its job well.

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

To use it, just add this Class to the rule set yii/web/UrlManager::$rules. Just create a simple Bootstrap.php file in /modules/seo module (similar to the one we created for /modules/site) and declare the following rule:

//...
    public function bootstrap($app)
    {
       $app->getUrlManager()->addRules(
           [
               [
                 'class' => 'modules\seo\components\UrlRule,
               ],
           ]
       );
    }
//..

This rule is very specific, so use it with caution. We aren’t going to use it in other projects. It explains why it isn’t set up properly. Therefore, you don’t need to expand yii/web/UrlRule, yii/base/Object, etc. You can just implement yii/web/UrlRuleInterface. To put it simply, we won’t re-use that rule in our reusable modules – so we just defined it in our SEO module.

parseRequest () checks the route, and if it properly aligns with regular expression of the condition, it’ll parse the URL string further to get an array of matches.

In this model, we use a Route model. It includes the URL that stores well-formed links used to check if they correspond to our ruleset by findRouteByUrl method. The method allows us to retrieve a single row with corresponding fields from the database table (if any) with this fieds below:

  1. url – a vital part of our search term (used to find the row)

  2. route – a route, which is necessary to transfer control

  3. params – additional parameters (JSON string), which are used to transfer the operation for further processing.

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

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

The other way around, it returns false to notify UrlManager that this class can’t process the search term.

createUrl () creates an URL from rendered parameters, but only if the URL was used for lease/lease/view, cars/info/view/ or lease/search/view routes.

What to Do With the Productivity Part?

When you develop a complex Web application, it’s crucial to optimize all URL rules to simplify the syntactic analysis of search terms and accelerate the URL generation process.

In case the analysis or URL generation are underway, UrlManager will analyze URL rules in order of their definition. That is, you can easily correct the order in which URL rules are defined to put frequently used rules in the first place.

It’s a common thing when an app consists of multiple modules with different sets of URL rules with module ID, their common prefix.

Generation and Output of Meta Tags

To generate and put out meta tags in a given format for designated types of pages, we’ve developed a special helper. We placed it into modules/seo/helpers/Meta.php file. It includes the following code:

namespace modules\seo\helpers;
use Yii;
use yii\helpers\Html;
/**
* @package modules\seo\helpers
*/
class Meta
{
   /**
    * It generates meta tags title, keywords, description and returns string with Title of the page.
    *
    * @param string $type Page type, for which meta tags are generated
    * @param object $model
    * @return string $title Title of the page
    */
   public static function all($type, $model = null)
   {
       $title = 'Carvoy | A new generation of leasing a car!'; // Title of the page by default.
       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;
   }
}

Use this helper in the page’s view where you need to install meta tags. For example, if you need to implement meta tags to the offer’s view (lease view), just add the following code to /modules/lease/views/frontend/lease/view.php file:

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

As a first parameter, you’ll transfer the page’s type, for which meta tags are generated. As a second parameter – the model of a given offer (lease).

Meta tag generation will take place within the method based on the page’s type and their addition to head with registerMetaTag method of yii/web/View class. The method will return a generated string for title meta tag. In other words, we’ll declare page’s title using the $title of yii/web/View class.

This is it. I hope, this article will help you generate URLs using UrlManager.

And thanks for your time! If you have any questions, ask them in the comments section or through contacts on the site clever-solution.com. Tune in for more!

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Topics:
php ,url ,angular js ,yii framework ,yii developers ,yii-high power web applications

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}