zend-framework2zend-framework-routing

zf2 router: One module makes the router unable to find another module. How can I make both work?


We're just starting out with zf2, so someone else made a Thumbnail server module, and then I added a Lookup server module. I call these servers because they're both RESTful apis. Initially they seemed to work together, but someone made some changes to my module, and now the Thumbnail server won't work unless Lookup is removed from the application.config.php list of modules. The Lookup server works regardless. Looking over the code, I don't see how the changes made to Lookup would affect Thumbnail. The error I'm getting looks like this:

<h1>A 404 error occurred</h1>
<h2>Page not found.</h2>
<p>The requested controller was unable to dispatch the request.</p>
<dl>
<dt>Controller:</dt>
<dd>Lookup\Controller\Lookup</dd>
</dl>

Here's what application.config.php looks like:

<?php
return array(
    'modules' => array(
        'Application',
        'SanRestful',
        'Album',
        'AlbumRest',   
        'Thumbnail',         
        'Lookup',
        'AP_XmlStrategy',
    ),
    'module_listener_options' => array(
        'config_glob_paths'    => array(
            'config/autoload/{,*.}{global,local}.php',
            'config/autoload/{,*.}' . (getenv('APPLICATION_ENV') ?: 'production') . '.php',
        ),
        'module_paths' => array(
            './module',
            './vendor',
        ),
    ),
);

As you can see, there's the initial Album module, and a few other experimental ones. My Lookup module uses the excellent AP_XmlStrategy module by Allessandro Pietrobelli.

Below is the Thumbnail module.config.php. It has a constraint that's probably not being used because there are no arguments called "id", but that shouldn't mess things up, should it?

<?php
return array(
    'controllers' => array(
        'invokables' => array(
            'Thumbnail\Controller\Thumbnail' => 'Thumbnail\Controller\ThumbnailController',
        ),
    ),

    // The following section is new and should be added to your file
    'router' => array(
        'routes' => array(
            'thumbnail' => array(
                'type'    => 'segment',
                'options' => array(
                    'route'    => '/thumbnail[/:action][/:output]',
                    'constraints' => array(
                        'id'     => '[0-9]+',
                    ),
                    'defaults' => array(
                        'controller' => 'Thumbnail\Controller\Thumbnail',
                        'action'     => 'index',
                    ),
                ),
            ),
        ),
    ),

    'view_manager' => array(
        'template_path_stack' => array(
            'thumbnail' => __DIR__ . '/../view',
        ),
    ),
);

And the Lookup module.config.php, with identifiers obfuscated:

<?php
return array(
    'db' => array(
        'driver'         => 'Pdo',
        'dsn'            => 'pgsql:dbname=<dbname>;host=<host>;port=<port>',
        'username'       => '<username>',
        'password'       => '<password>',
        ),
    'service_manager' => array(
        'factories' => array(
            'Zend\Db\Adapter\Adapter'
                    => 'Zend\Db\Adapter\AdapterServiceFactory',
        ),
        'aliases' => array(
            'db' => 'Zend\Db\Adapter\Adapter',
        ),
    ),
    'controllers' => array(
        'invokables' => array(
            'Lookup\Controller\Lookup' => 'Lookup\Controller\LookupController',
        ),
    ),
    // The following section is new and should be added to your file
    'router' => array(
        'routes' => array(
            'lookup' => array(
                'type'    => 'segment',
                'options' => array(
                    'route'    => '[/:action][/:version][/:resource][/:code][/:resource_xref]',
                    'constraints' => array(
                        'action'         => '[a-zA-Z][a-zA-Z0-9_-]*',
                        'version'        => '[a-zA-Z][a-zA-Z0-9_-]*',
                        'resource'       => '[a-zA-Z][a-zA-Z0-9_-|\.]*',
                        'code'           => '[a-zA-Z][a-zA-Z0-9_-]*',
                        'resource_xref'  => '[a-zA-Z][a-zA-Z0-9_-|\.]*',
                    ),
                    'defaults' => array(
                        'controller' => 'Lookup\Controller\Lookup',
                        'action'     => 'index',
                    ),
                ),
            ),
        ),
    ),
    'view_manager' => array(
        'template_path_stack' => array(
            'lookup' => __DIR__ . '/../view',
        ),
        'strategies' => array(
            'ViewJsonStrategy',
            'ViewXmlStrategy',
        ),
    ),
);

Are there any obvious mistakes here that I'm missing?


Solution

  • Requests can be matched by multiple routes. An example: the url /foo can be matched by a literal route /foo and also the route [/:action]. To distinguish the two routes, the order how you configure them matters.

    What happens, the routes are matched in LIFO order, so the last route must be the most explicit. In the "/foo" example, this config will match the literal:

    'router' => array(
        'routes' => array(
            'test1' => array(
                'type'    => 'segment',
                'options' => array(
                    'route'    => '[/:action]',
                    'defaults' => array(
                        //
                    ),
                ),
            ),
            'test2' => array(
                'type'    => 'literal',
                'options' => array(
                    'route'    => '/foo',
                    'defaults' => array(
                        //
                    ),
                ),
            ),
        ),
    ),
    

    However, in the below config, the literal will never be matched, because [/:action] is a possible match for /foo.

    'router' => array(
        'routes' => array(
            'test2' => array(
                'type'    => 'literal',
                'options' => array(
                    'route'    => '/foo',
                    'defaults' => array(
                        //
                    ),
                ),
            ),
            'test1' => array(
                'type'    => 'segment',
                'options' => array(
                    'route'    => '[/:action]',
                    'defaults' => array(
                        //
                    ),
                ),
            ),
        ),
    ),
    

    Now take a look at your two modules. The first (Thumbnail) has a route /thumbnail[/:action][/:output]. it starts with a literal part. Then your second module (Lookup) has a route [/:action][/:version][/:resource][/:code][/:resource_xref].

    Now if you get back to the LIFO order, any route starting with /thumbnail will match already at the Lookup route.

    Solution

    There are two options. First is to swap the order of the modules. This is always an important order if you have inter-dependent relations between modules. Load Lookup first and then Thumnnail, so the thumbnail route is put later in the config. Thus, it matches first. Thus, your app works again.

    Then there is the second solution (and imho, the better). You have a "one route to rule them all" in the Lookup, which is not a really good practice. You can get into trouble exactly like you are now, having no idea what went wrong. Thus, specify as much as possible in your routes. Make the first part of the Lookup also literal (is /lookup[/:action][/:version][/:resource][/:code][/:resource_xref] not an option?). Or remove action as parameter and make those literal:

    'router' => array(
        'routes' => array(
            'view' => array(
                'type'    => 'segemnt',
                'options' => array(
                    'route'    => '/view[/:version][/:resource][/:code][/:resource_xref]',
                    'defaults' => array(
                        'action' => 'view',
                        //
                    ),
                ),
            ),
            'create' => array(
                'type'    => 'segment',
                'options' => array(
                    'route'    => '/create[/:version][/:resource][/:code][/:resource_xref]',
                    'defaults' => array(
                        'action' => 'create',
                        //
                    ),
                ),
            ),
            'update' => array(
                'type'    => 'segment',
                'options' => array(
                    'route'    => '/update[/:version][/:resource][/:code][/:resource_xref]',
                    'defaults' => array(
                        'action' => 'update',
                        //
                    ),
                ),
            ),
            // And so on
        ),
    ),
    

    This way, your lookup module has a fixed starting point and matches only if those first parts are in the request uri. Then your /thumbnail[/:action][/:output] is completely decoupled from the Lookup routes.