Blog

Using Configuration-Driven Routes in Expressive

Expressive 1 used configuration-driven pipelines and routing; Expressive 2 switches to use programmatic pipelines and routes instead. The programmatic approach was chosen as many developers have indicated they find it easier to understand and easier to read, and ensures they do not have any configuration conflicts.

However, there are times you may want to use configuration. For example, when you are writing re-usable modules, it's often easier to provide configuration for routed middleware, than to expect users to cut-and-paste examples, or use features such as delegator factories.

Fortunately, starting in Expressive 2, we offer a couple different mechanisms to support configuration-driven pipelines and routing.

Configuration Only

By default in Expressive 2, and if you run the expressive-pipeline-from-config tool to migrate from v1 to v2, we enable a specific flag to force usage of programmatic pipelines:

// Within config/autoload/zend-expressive.global.php in v2,
// and config/autoload/programmatic-pipeline.global.php for v1 projects that
// migrate using the tooling:
return [
    'zend-expressive' => [
        'programmatic_pipeline' => true,
    ],
];

By removing this setting, or toggling it to false, you can go back to the original Expressive 1 behavior whereby the pipeline and routing are completely generated by configuration. You can read the documentation on the ApplicationFactory for details on how to configure the pipeline and routes in this situation.

Beware!

If you also have programmatic declarations in your config/pipeline.php and/or config/routes.php files, and these are still included from your public/index.php, you may run into conflicts when you disable programmatic pipelines! Comment out the require lines in your public/index.php after toggling the configuration value to be safe!

The key advantage to using configuration is that you can override configuration by providing config/autoload/*.local.php files; this gives the ability to substitute different middleware when desired. That said, if you use arrays of middleware to create custom pipelines, configuration overriding may not work as expected.

Selective Configuration

There are a few drawbacks to going configuration-only:

  • Most pipelines will be static.
  • Configuration is more verbose than programmatic declarations.

Fortunately, starting with Expressive 2, you can combine the two approaches, due to the addition of two methods to Zend\Expressive\Application:

public function injectPipelineFromConfig(array $config = null) : void;
public function injectRoutesFromConfig(array $config = null) : void;

(In each case, if passed no values, they will use the config service composed in the container the Application instance uses.)

In the case of injectPipelineFromConfig(), the method pulls the middleware_pipeline value from the passed configuration; injectRoutesFromConfig() pulls from the routes value.

Where would you use this?

One place to use it is when modules provide routing in their ConfigProvider. For instance, let's say I have a BooksApi\ConfigProvider class that returns a routes key with the default set of routes I feel should be defined:

<?php
// in src/BooksApi/ConfigProvider.php:

namespace BooksApi;

class ConfigProvider
{
    public function __invoke() : array
    {
        return [
            'dependencies' => $this->getDependencies(),
            'routes'       => $this->getRoutes(),
        ];
    }

    public function getDependencies() : array
    {
        // ...
    }

    public function getRoutes() : array
    {
        return [
            [
                'name'            => 'books'
                'path'            => '/api/books',
                'middleware'      => Action\ListBooks::class,
                'allowed_methods' => ['GET'],
            ],
            [
                'path'            => '/api/books',
                'middleware'      => Action\CreateBook::class,
                'allowed_methods' => ['POST'],
            ],
            [
                'name'            => 'book'
                'path'            => '/api/books/{id:\d+}',
                'middleware'      => Action\DisplayBook::class,
                'allowed_methods' => ['GET'],
            ],
            [
                'path'            => '/api/books/{id:\d+}',
                'middleware'      => Action\UpdateBook::class,
                'allowed_methods' => ['PATCH'],
            ],
            [
                'path'            => '/api/books/{id:\d+}',
                'middleware'      => Action\DeleteBook::class,
                'allowed_methods' => ['DELETE'],
            ],
        ];
    }
}

If I, as an application developer, feel those defaults do not conflict with my application, I could do the following within my config/routes.php file:

<?php
// config/routes.php:

$app->get('/', App\HomePageAction::class, 'home');

$app->injectRoutesFromConfig((new BooksApi\ConfigProvider())());

// ...

By invoking the BooksApi\ConfigProvider, I can be assured I'm only injecting those routes defined by that given module, and not all routes defined anywhere in my configuration. I've also saved myself a fair bit of copy-pasta!

Caution: pipelines

We do not recommend mixing programmatic and configuration-driven pipelines, due to issues of ordering.

When you create a programmatic pipeline, the pipeline is created in exactly the order in which you declare it:

$app->pipe(OriginalMessages::class);
$app->pipe(XClacksOverhead::class);
$app->pipe(ErrorHandler::class);
$app->pipe(ServerUrlMiddleware::class);
$app->pipeRoutingMiddleware();
$app->pipe(ImplicitHeadMiddleware::class);
$app->pipe(ImplicitOptionsMiddleware::class);
$app->pipe(UrlHelperMiddleware::class);
$app->pipeDispatchMiddleware();
$app->pipe(NotFoundHandler::class);

In other words, when you look at the pipeline, you know immediately what the outermost middleware is, and the path to the innermost middleware.

Configuration-driven middleware allows you to specify priority values to specify the order in which middleware is piped; higher values are piped earlies, lowest (including negative!) values are piped last.

What happens when you mix the systems? It depends on when you inject configuration-driven middleware:

// Middleware from configuration applies first:
$app->injectPipelineFromConfig($pipelineConfig)
$app->pipe(/* ... */);

// Middleware from configuration applies last:
$app->pipe(/* ... */);
$app->injectPipelineFromConfig($pipelineConfig)

// Or mix it up?
$app->pipe(/* ... */);
$app->injectPipelineFromConfig($pipelineConfig)
$app->pipe(/* ... */);

This can lead to some tricky situations. We suggest sticking to one or the other, to ensure you can fully visualize the entire pipeline at once.

Summary

The new Application::injectRoutesFromConfig() method offered in Expressive 2 provides you with a useful tool for providing routing within your Expressive modules.

This is not the only way to provide routing, however. We detail another approach to autowiring routes in the manual that provides a way to keep the programmatic approach, by decorating instantiation of the Application instance.

We hope this opens some creative routing possibilities for Expressive developers, particularly those creating reusable modules!

SHARE:

Copyright

© 2006-2017 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