Custom Routing in Zend Framework 2

December 9th, 2012 by Cosmin Harangus Leave a reply »

Routing in Zend Framework 2 is very simple to use and configure if your needs are limited to only the classes provided by the MVC component.

You can find some clear examples and a good tutorial on how to configure routes in the ZF2 documentation. There you will also find more information about some of the move advanced topics discussed in this article.

However, if you are looking to create a custom route that does a bit more than just matching a particular URL, you may find yourself in a pickle.

Recently I wanted to create a custom route that can tell me if the provided URL can be found in a database table.

I’ve used my old friend Google and searched the web and some of my favorite blogs for answers to how I could get an instance of the ServiceLocator in a custom route but without any success.

I did find a few links where the developers solution was to add a static variable on the route class and set the ServiceLocator class on bootstrap, but that was in my opinion a bad practice, especially since ZF2 is all about Dependency Injection and best practices.

OK… so how do you do it?

Before we answer that let’s talk a little about how we can create a custom route class and how we can use it in your application.

Create Custom Routes

In order to create a custom http route we have to either implement \Zend\Mvc\Router\Http\RouteInterface or extend one of of the \Zend\Mvc\Router\Http\* classes, depending on what we need to do.

For example to create the route described above we will be implementing RouteInterface and add our custom functionality:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php
namespace Application\Router;
use Zend\Mvc\Router\Http\RouteInterface;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Stdlib\ArrayUtils;
use Zend\Mvc\Router\Exception\InvalidArgumentException;

class Page implements RouteInterface
{

    protected $defaults = array();

    /**
     * Create a new page route.
     */

    public function __construct(array $defaults = array())
    {
        $this->defaults = $defaults;
    }

    /**
     * Create a new route with given options.
     */

    public static function factory($options = array())
    {
        if ($options instanceof \Traversable) {
            $options = ArrayUtils::iteratorToArray($options);
        } elseif (!is_array($options)) {
            throw new InvalidArgumentException(__METHOD__ . ' expects an array or Traversable set of options');
        }

        if (!isset($options['defaults'])) {
            $options['defaults'] = array();
        }

        return new static($options['defaults']);
    }


    /**
     * Match a given request.
     */

    public function match(Request $request, $pathOffset = null)
    {
        //@todo test the Request object and return a \Zend\Mvc\Router\RouteMatch instance
        return null;
    }

    /**
     * Assemble the route.
     */

    public function assemble(array $params = array(), array $options = array())
    {
        //@todo assemple the route and return the URL as string
        return '';
    }

    /**
     * Get a list of parameters used while assembling.
     */

    public function getAssembledParams()
    {
        return array();
    }

}

 

The above class can be used as starting point for any custom route you need, though it doesn’t really do anything yet. We will need to modify the match function and return an instance of \Zend\Mvc\Router\RouteMatch class if want the router to pick this route for the current request.

Apart from the current Request object the match function may also receive a path offset variable if the route is a child of a Part route. The path offset will let you know what is the index in the URL from which you should start processing.

Accessing the service locator

The Router in ZF2 uses an implementation of the \Zend\ServiceManager\AbstractPluginManager named RoutePluginManager in order to load each route class. This is basically a service locator that contains an invokable entry for each route class. The class also implements the ServiceLocatorAwareInterface, but it doesn’t have a service locator instance set by default.

First thing we need to do to access the service locator in a route is to implement the \Zend\ServiceManager\ServiceLocatorAwareInterface in order to get an instance of the RoutePluginManager. This way when the route is loaded the setServiceLocator method will be called and the service locator (the RoutePluginManager in our case) that loads the class will be set in the route.

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
namespace Application\Router;
use Zend\Mvc\Router\Http\RouteInterface;
// ...
use Zend\ServiceManager\ServiceLocatorAwareInterface;

class Page implements RouteInterface, ServiceLocatorAwareInterface
{

    protected $routePluginManager = null;

    /**
     * Set service locator
     *
     * @param ServiceLocatorInterface $routePluginManager
     */

    public function setServiceLocator(ServiceLocatorInterface $routePluginManager)
    {
        $this->routePluginManager = $routePluginManager;
    }

    /**
     * Get service locator
     *
     * @return ServiceLocatorInterface
     */

    public function getServiceLocator()
    {
        return $this->routePluginManager;
    }

}

Please note that the $routePluginManager is an instance of the ServiceLocatorInterface, however it can only return instances of the defined route classes.

In order to make it have access to the application service locator instance we will have to extend the RouterFactory and set the instance of the service locator for the RoutePluginManager manually.

Create the following class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
namespace Application\Service;
use Zend\Mvc\Service\RouterFactory as DefaultRouterFactory;
use Zend\ServiceManager\ServiceLocatorInterface;
/**
 *
 * @author Cosmin Harangus <cosmin@around25.com>
 */

class RouterFactory extends DefaultRouterFactory
{
    public function createService(ServiceLocatorInterface $serviceLocator, $cName = null, $rName = null)
    {
        $router = parent::createService($serviceLocator, $cName, $rName);

        //get instance of the RoutePluginManager
        $routePluginManager = $router->getRoutePluginManager();
        //set the ServiceLocator for the RoutePluginManager so we can use it in the route
        $routePluginManager->setServiceLocator( $serviceLocator );

        return $router;
    }
}

The above class extends the default router factory and sets the service locator on the RoutePluginManager instance used by the router.
Now all we have to do is make sure that the router uses this factory instead of the default one. To do this we will alter our module.config.php file and add the following:

1
2
3
4
5
6
7
8
9
10
11
<?php
return array(

    'service_manager' => array(
        'factories' => array(
            'Router'        => 'Application\Service\RouterFactory',
        ),
    ),

    // ...
);

Now we should be able to have access to the service locator in the custom route like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
    // ...

    public function match(Request $request, $pathOffset = null)
    {
        // get the service locator
        $serviceLocator = $this->routePluginManager->getServiceLocator();

        //@todo do something with the request and return an RouteMatch instance

        return false;
    }

    // ...

I hope this article helps you avoid some headaches in the future.
I am not sure if there is any better way of doing this in ZF2. If there is please send us a comment or better yet integrate it in the official ZF2 documentation.

Share

8 comments

  1. Anna Harris says:

    I searched google and created custom route but it didn’t worked, I tried the way shown in your post and it worked very well, Thanks a lot.

  2. bhawani says:

    i want to resize the image in / create thumbnail image in zend framework 2.

  3. Jarle says:

    I made a variation of this that worked as expected using ZF2 version 2.1.5, but route matching fails on version 2.2.0 RC1. Any ideas what changes in the framework that causes mismatch?

  4. Jarle says:

    for the 2.1.5 vs 2.2.0 RC1 bug, the solution was to use Zend\Mvc\Router\Http\RouteMatch instead of Zend\Mvc\Router\RouteMatch when flagging that a route was matched.

  5. Chellai says:

    Hi,

    How will the application know about the Page Class? Can you help me out? We already implemented the RouterFactory. In there, we added the fetching of pages on the database. But what I wanted to do is check first if there’s a route matched from the routes config then if it fails, then that’s the only time it will fetch to the database. I am not really sure how the Page class works but I really think that’s what we need. Thanks.

  6. I suggest using the following module: https://github.com/avalanche123/Imagine

    You can get it from packagist as well.

  7. Doug Johnson says:

    Following Chellai’s question, How do we make the application aware of the page class? I tried using the Page as route type but the service manager cannot find it. Placing an entry in the standard service manager config doesn’t work either. Where do we register our custom router with the service manager? Thanks

  8. Mario says:

    Hi,

    @Chellai: the routes in modules.config.php are LIFO (see here http://framework.zend.com/manual/2.0/en/modules/zend.mvc.routing.html ). This explained concept is compatible with the “static” routes in modules.config.php.

    Everything works now.

Leave a Reply