Documentation

Understanding the Router — Zend Framework 2 2.4.9 documentation

In-depth tutorial for beginners

Understanding the Router

Right now we have a pretty solid set up for our module. However, we’re not really doing all too much yet, to be precise, all we do is display all Blog entries on one page. In this chapter you will learn everything you need to know about the Router to create other routes to be able to display only a single blog, to add new blogs to your application and to edit and delete existing blogs.

Different route types

Before we go into details on our application, let’s take a look at the most important route types that Zend Framework offers.

Zend\Mvc\Router\Http\Literal

The first common route type is the Literal-Route. As mentioned in a previous chapter a literal route is one that matches a specific string. Examples for URLs that are usually literal routes are:

Configuration for a literal route requires you to set up the route that should be matched and needs you to define some defaults to be used, for example which controller and which action to call. A simple configuration for a literal route looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 'router' => array(
     'routes' => array(
         'about' => array(
             'type' => 'literal',
             'options' => array(
                 'route'    => '/about-me',
                 'defaults' => array(
                     'controller' => 'AboutMeController',
                     'action'     => 'aboutme',
                 ),
             ),
         )
     )
 )

Zend\Mvc\Router\Http\Segment

The second most commonly used route type is the Segment-Route. A segmented route is used for whenever your url is supposed to contain variable parameters. Pretty often those parameters are used to identify certain objects within your application. Some examples for URLs that contain parameters and are usually segment routes are:

Configuring a Segment-Route takes a little more effort but isn’t difficult to understand. The tasks you have to do are similar at first, you have to define the route-type, just be sure to make it Segment. Then you have to define the route and add parameters to it. Then as usual you define the defaults to be used, the only thing that differs in this part is that you can assign defaults for your parameters, too. The new part that is used on routes of the Segment type is to define so called constraints. They are used to tell the Router what “rules” are given for parameters. For example, an id-parameter is only allowed to be of type integer, the year-parameter is only allowed to be of type integer and may only contain exactly four digits. A sample configuration can look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 'router' => array(
     'routes' => array(
         'archives' => array(
             'type' => 'segment',
             'options' => array(
                 'route'    => '/news/archive/:year',
                 'defaults' => array(
                     'controller' => 'ArchiveController',
                     'action'     => 'byYear',
                 ),
                 'constraints' => array(
                     'year' => '\d{4}'
                 )
             ),
         )
     )
 )

This configuration defines a route for a URL like domain.com/news/archive/2014. As you can see, our route now contains the part :year. This is called a route-parameter. Route parameters for Segment-Routes are defined by a full-colon (“:”) in front of a string; the string is the parameter name.

Under constraints you see that we have another array. This array contains regular expression rules for each parameter of your route. In our example case the regex uses two parts, the first one being \d which means “a digit”, so any number from 0-9. The second part is {4} which means that the part before this has to match exactly four times. So in easy words we say “four digits”.

If now you call the URL domain.com/news/archive/123, the router will not match the URL because we only support years with four digits.

You may notice that we did not define any defaults for the parameter year. This is because the parameter is currently set up as a required parameter. If a parameter is supposed to be optional we need to define this inside the route definition. This is done by adding square brackets around the parameter. Let’s modify the above example route to have the year parameter optional and use the current year as default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 'router' => array(
     'routes' => array(
         'archives' => array(
             'type' => 'segment',
             'options' => array(
                 'route'    => '/news/archive[/:year]',
                 'defaults' => array(
                     'controller' => 'ArchiveController',
                     'action'     => 'byYear',
                     'year'       => date('Y')
                 ),
                 'constraints' => array(
                     'year' => '\d{4}'
                 )
             ),
         )
     )
 )

Notice that now we have a part in our route that is optional. Not only the parameter year is optional. The slash that is separating the year parameter from the URL string archive is optional, too, and may only be there whenever the year parameter is present.

Different routing concepts

When thinking about the whole application it becomes clear that there are a lot of routes to be matched. When writing these routes you have two options. One option is to spend less time writing routes that in turn are a little slow in matching. Another option is to write very explicit routes that match a little faster but require more work to define. Let’s take a look at both of them.

Generic routes

A generic route is one that matches many URLs. You may remember this concept from Zend Framework 1 where basically you didn’t even bother about routes because we had one “god route” that was used for everything. You define the controller, the action, and all parameters within just one single route.

The big advantage of this approach is the immense time you save when developing your application. The downside, however, is that matching such a route can take a little bit longer due to the fact that so many variables need to be checked. However, as long as you don’t overdo it, this is a viable concept. For this reason the ZendSkeletonApplication uses a very generic route, too. Let’s take a look at a generic route:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 'router' => array(
     'routes' => array(
         'default' => array(
             'type' => 'segment',
             'options' => array(
                 'route'    => '/[:controller[/:action]]',
                 'defaults' => array(
                     '__NAMESPACE__' => 'Application\Controller',
                     'controller'    => 'Index',
                     'action'        => 'index',
                 ),
                 'constraints' => [
                     'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
                     'action'     => '[a-zA-Z][a-zA-Z0-9_-]*',
                 ]
             ),
         )
     )
 )

Let’s take a closer look as to what has been defined in this configuration. The route part now contains two optional parameters, controller and action. The action parameter is optional only when the controller parameter is present.

Within the defaults-section it looks a little bit different, too. The __NAMESPACE__ will be used to concatenate with the controller parameter at all times. So for example when the controller parameter is “news” then the controller to be called from the Router will be Application\Controller\news, if the parameter is “archive” the Router will call the controller Application\Controller\archive.

The defaults-section then is pretty straight forward again. Both parameters, controller and action, only have to follow the conventions given by PHP-Standards. They have to start with a letter from a-z, upper- or lowercase and after that first letter there can be an (almost) infinite amount of letters, digits, underscores or dashes.

The big downside to this approach not only is that matching this route is a little slower, it is that there is no error-checking going on. For example, when you were to call a URL like domain.com/weird/doesntExist then the controller would be “Application\Controller\weird” and the action would be “doesntExistAction”. As you can guess by the names let’s assume neither controller nor action does exist. The route will still match but an Exception will be thrown because the Router will be unable to find the requested resources and we’ll receive a 404-Response.

Explicit routes using child_routes

Explicit routing is done by defining all possible routes yourself. For this method you actually have two options available, too.

Without config structure

The probably most easy to understand way to write explicit routes would be to write many top level routes like in the following configuration:

As you can see with this little example, all routes have an explicit name and there’s lots of repetition going on. We have to redefine the default controller to be used every single time and we don’t really have any structure within the configuration. Let’s take a look at how we could bring more structure into a configuration like this.

Using child_routes for more structure

Another option to define explicit routes is to be using child_routes. Child routes inherit all options from their respective parents. Meaning: when the controller doesn’t change, you do not need to redefine it. Let’s take a look at a child routes configuration using the same example as above:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 'router' => array(
     'routes' => array(
         'news' => array(
             'type' => 'literal',
             'options' => array(
                 'route'    => '/news',
                 'defaults' => array(
                     'controller' => 'NewsController',
                     'action'     => 'showAll',
                 ),
             ),
             // Defines that "/news" can be matched on its own without a child route being matched
             'may_terminate' => true,
             'child_routes' => array(
                 'archive' => array(
                     'type' => 'segment',
                     'options' => array(
                         'route'    => '/archive[/:year]',
                         'defaults' => array(
                             'action'     => 'archive',
                         ),
                         'constraints' => array(
                             'year' => '\d{4}'
                         )
                     ),
                 ),
                 'single' => array(
                     'type' => 'segment',
                     'options' => array(
                         'route'    => '/:id',
                         'defaults' => array(
                             'action'     => 'detail',
                         ),
                         'constraints' => array(
                             'id' => '\d+'
                         )
                     ),
                 ),
             )
         ),
     )
 )

This routing configuration requires a little more explanation. First of all we have a new configuration entry which is called may_terminate. This property defines that the parent route can be matched alone, without child routes needing to be matched, too. In other words all of the following routes are valid:

  • /news
  • /news/archive
  • /news/archive/2014
  • /news/42

If, however, you were to set may_terminate => false, then the parent route would only be used for global defaults that all child_routes were to inherit. In other words: only child_routes can be matched, so the only valid routes would be:

  • /news/archive
  • /news/archive/2014
  • /news/42

The parent route would not be able to be matched on its own.

Next to that we have a new entry called child_routes. In here we define new routes that will be appended to the parent route. There’s no real difference in configuration from routes you define as a child route to routes that are on the top level of the configuration. The only thing that may fall away is the re-definition of shared default values.

The big advantage you have with this kind of configuration is the fact that you explicitly define the routes and therefore you will never run into problems of non-existing controllers like you would with generic routes like described above. The second advantage would be that this kind of routing is a little bit faster than generic routes and the last advantage would be that you can easily see all possible URLs that start with /news.

While ultimately this falls into the category of personal preference bare in mind that debugging of explicit routes is significantly easier than debugging generic routes.

A practical example for our Blog Module

Now that we know how to configure new routes, let’s first create a route to display only a single Blog from our Database. We want to be able to identify blog posts by their internal ID. Given that ID is a variable parameter we need a route of type Segment. Furthermore we want to put this route as a child route to the route of name blog.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 <?php
 // FileName: /module/Blog/config/module.config.php
 return array(
     'db'              => array( /** DB Config */ ),
     'service_manager' => array( /* ServiceManager Config */ ),
     'view_manager'    => array( /* ViewManager Config */ ),
     'controllers'     => array( /* ControllerManager Config */ ),
     'router' => array(
         'routes' => array(
             'blog' => array(
                 'type' => 'literal',
                 'options' => array(
                     'route'    => '/blog',
                     'defaults' => array(
                         'controller' => 'Blog\Controller\List',
                         'action'     => 'index',
                     ),
                 ),
                 'may_terminate' => true,
                 'child_routes'  => array(
                     'detail' => array(
                         'type' => 'segment',
                         'options' => array(
                             'route'    => '/:id',
                             'defaults' => array(
                                 'action' => 'detail'
                             ),
                             'constraints' => array(
                                 'id' => '[1-9]\d*'
                             )
                         )
                     )
                 )
             )
         )
     )
 );

With this we have set up a new route that we use to display a single blog entry. We have assigned a parameter called id that needs to be a positive digit excluding 0. Database entries usually start with a 0 when it comes to primary ID keys and therefore our regular expression constraints for the id fields looks a little more complicated. Basically we tell the router that the parameter id has to start with an integer between 1 and 9, that’s the [1-9] part, and after that zero or more digits can follow (that’s the \d* part).

The route will call the same controller like the parent route but it will call the detailAction() instead. Go to your browser and request the URL http://localhost:8080/blog/2. You’ll see the following error message:

1
2
3
4
5
6
7
8
9
 A 404 error occurred

 Page not found.
 The requested controller was unable to dispatch the request.

 Controller:
 Blog\Controller\List

 No Exception available

This is due to the fact that the controller tries to access the detailAction() which does not yet exist. Let’s go ahead and create this action now. Go to your ListController and add the action. Return an empty ViewModel and then refresh the page.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 <?php
 // FileName: /module/Blog/src/Blog/Controller/ListController.php
 namespace Blog\Controller;

 use Blog\Service\PostServiceInterface;
 use Zend\Mvc\Controller\AbstractActionController;
 use Zend\View\Model\ViewModel;

 class ListController extends AbstractActionController
 {
     /**
      * @var \Blog\Service\PostServiceInterface
      */
     protected $postService;

     public function __construct(PostServiceInterface $postService)
     {
         $this->postService = $postService;
     }

     public function indexAction()
     {
         return new ViewModel(array(
             'posts' => $this->postService->findAllPosts()
         ));
     }

     public function detailAction()
     {
         return new ViewModel();
     }
 }

Now you’ll see the all familiar message that a template was unable to be rendered. Let’s create this template now and assume that we will get one Post-Object passed to the template to see the details of our blog. Create a new view file under /view/blog/list/detail.phtml:

1
2
3
4
5
6
7
8
9
 <!-- FileName: /module/Blog/view/blog/list/detail.phtml -->
 <h1>Post Details</h1>

 <dl>
     <dt>Post Title</dt>
     <dd><?php echo $this->escapeHtml($this->post->getTitle());?></dd>
     <dt>Post Text</dt>
     <dd><?php echo $this->escapeHtml($this->post->getText());?></dd>
 </dl>

Looking at this template we’re expecting the variable $this->post to be an instance of our Post-Model. Let’s now modify our ListController so that a Post will be passed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 <?php
 // FileName: /module/Blog/src/Blog/Controller/ListController.php
 namespace Blog\Controller;

 use Blog\Service\PostServiceInterface;
 use Zend\Mvc\Controller\AbstractActionController;
 use Zend\View\Model\ViewModel;

 class ListController extends AbstractActionController
 {
     /**
      * @var \Blog\Service\PostServiceInterface
      */
     protected $postService;

     public function __construct(PostServiceInterface $postService)
     {
         $this->postService = $postService;
     }

     public function indexAction()
     {
         return new ViewModel(array(
             'posts' => $this->postService->findAllPosts()
         ));
     }

     public function detailAction()
     {
         $id = $this->params()->fromRoute('id');

         return new ViewModel(array(
             'post' => $this->postService->findPost($id)
         ));
     }
 }

If you refresh your application now you’ll see the details for our Post to be displayed. However, there is one little Problem with what we have done. While we do have our Service set up to throw an \InvalidArgumentException whenever no Post matching a given id is found, we don’t make use of this just yet. Go to your browser and open the URL http://localhost:8080/blog/99. You will see the following error message:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 An error occurred
 An error occurred during execution; please try again later.

 Additional information:
 InvalidArgumentException

 File:
 {rootPath}/module/Blog/src/Blog/Service/PostService.php:40

 Message:
 Could not find row 99

This is kind of ugly, so our ListController should be prepared to do something whenever an InvalidArgumentException is thrown by the PostService. Whenever an invalid Post is requested we want the User to be redirected to the Post-Overview. Let’s do this by putting the call against the PostService in a try-catch statement.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 <?php
 // FileName: /module/Blog/src/Blog/Controller/ListController.php
 namespace Blog\Controller;

 use Blog\Service\PostServiceInterface;
 use Zend\Mvc\Controller\AbstractActionController;
 use Zend\View\Model\ViewModel;

 class ListController extends AbstractActionController
 {
     /**
      * @var \Blog\Service\PostServiceInterface
      */
     protected $postService;

     public function __construct(PostServiceInterface $postService)
     {
         $this->postService = $postService;
     }

     public function indexAction()
     {
         return new ViewModel(array(
             'posts' => $this->postService->findAllPosts()
         ));
     }

     public function detailAction()
     {
         $id = $this->params()->fromRoute('id');

         try {
             $post = $this->postService->findPost($id);
         } catch (\InvalidArgumentException $ex) {
             return $this->redirect()->toRoute('blog');
         }

         return new ViewModel(array(
             'post' => $post
         ));
     }
 }

Now whenever you access an invalid id you’ll be redirected to the route blog which is our list of blog posts, perfect!

Copyright

© 2006-2018 by Zend, a Rogue Wave Company. Made with by awesome contributors.

This website is built using zend-expressive and it runs on PHP 7.

Contacts