Routes specific for Expressive modules

If an Expressive module needs to add specific routes to the Application, how it can do that? A first idea is to use an Application delegator, as reported here but I feel like an over engineered solution.

The simplest solution would be to create a config/routes.php file in the module and inform application developer to include it in the main /config/routes.php file to enable the module routes.


A first idea is to use an Application delegator, as reported here but I feel like an over engineered solution.

I actually have the same feeling about this. It makes it more complicated than should, probably even more so for beginners. What’s wrong with including several config/routes.php file?

1 Like

I’m on board with this idea. In particular, I think it may address the important question of: “Where are my routes defined?”

One reason we moved from config-based to programmatic routing was to make it easier (a) to write the routes, and (b) to see them all at once. Pushing them into delegator factories hides where routes are defined; you can’t tell even by looking at delegator factory configuration which delegators might be adding routes.

By having them in plain PHP files within a module, and using include statements within config/routes.php, you then know exactly where to look for route definitions.

1 Like

Introducing a convention on where to put routes can be useful, but complicates things by adding “yet another way” of doing this.

A delegator is a relatively simple solution: although it is indeed not easy to comprehend at first, it is one and relatively simple.

@ocramius What about the issue of route discovery by the developer? I admit to having been bit by this a couple times, as I didn’t know where a route was being registered. Using include files solves this rather nicely, but at the expense of another include per request.


The “easiest” way to achieve that, IMO, is still the merged config indeed - usually doesn’t happen at runtime anyway, once cached. The overhead is smaller than the delegators’, but it is less flexible and more error-prone.

The current expressive skeleton has no module structure though, so no module merged config.

Also, merged config is extrememely fragile, and allows for subtle BC breaks that are harder to detect than the ones in delegators (because we are using the API signature rather than config keys).

I thought I remember reading in the official documentation that the preferred way, currently, was to not include route configuration in a module, as that could cause all kinds of mayhem if different modules define the same, or several, routes. I get the sense of caution there and based on that, see that it makes sense to have them centrally located. I also see Marco’s point about delegators, and also the contrarian point for newer developers. Personally, I don’t see the harm in saying “we prefer you to put your routes in a routes config file”, so long as we clearly state why. Otherwise, maybe an addition to Zend Expressive Tooling to aid in route management. Having said that, I don’t know what that might involve.

@ocramius, I’ve read through, and the caution at the end is what stands out for me the most.

It’s fair in what it says, but it raises some interesting points. I know that Zend Framework is an open, flexible framework, and doesn’t hold nor attempt to enforce strong views or “one right way” in how things are or should be done.

Other frameworks do, but those frameworks target specific development models. ZF/E’s openness and flexibility, however, can make development difficult, as there are so many paths to choose, whether individually or when combined in various permutations. What’s more, some people just want to have that clarity (and some don’t).

Summing up, I don’t believe that we should tell developers how they should use the framework. But we should give some recommendations or guidelines as to how best to use it, along with the reasons why. Then they can make informed choices.

I have just found a valid reason for me to keep the PipelineAndRoutesDelegator. If I use that and I grab the application from the container, the pipeline and the routes are automatically configured. Without it you need to include these manually. This is not a problem when these are included in public/index.php, but it’s more difficult when doing integration/functional tests for the actions.

I remember now that I switched immediately to the delegator since this solved previously testing issues I had before. There are other ways to get the pipeline and all the routes during testing, but this was the cleanest way I could find so far.

Not sure if it helps but for component routes I took the approach of writing a one-for-all delegator which is opt in by way of config

namespace App\Delegator;

use Psr\Container\ContainerInterface;
use RuntimeException;
use Zend\Expressive\Application;

class InjectRoutesFromConfigDelegatorFactory
    public function __invoke(ContainerInterface $container, $serviceName, callable $callback) : Application {

        $app = $callback();
        if (! $app instanceof Application) {
            throw new RuntimeException(sprintf(
                '%s can only delegate to instances of %s, received %s',
                is_object($app) ? get_class($app) : gettype($app)
        if ($container->has('config')) {
            $providers = $container->get('config')[self::class] ?? [];
            if ($providers) {
                foreach ($providers as $configClass) {
                    if (! class_exists($configClass)) {
                    $app->injectRoutesFromConfig((new $configClass)());
    return $app;

Couple this with config in config/autoload/local.php

use App\Delegator\InjectRoutesFromConfigDelegatorFactory as RouteConfig;

return [
     RouteConfig::class => [
        // opt-in config providers containing 'routes' key
       // etc..

It’s also easy to add the key to dev config too and provide additional routes during testing. My main focus though was on not having to write a delegator per component, this solution fits the bill for the routing.

I think @matthew wrote (long after this thread was opened) IMO the most elegant solution to this. Read more about it here:

I have to agree, as author of the original reply, I can confirm I too have since adopted @matthew’s approach as my preferred solution.