Container-interop + PSR7 = cross-framework module system

2 weeks ago, PSR-7 was finally accepted. It is an absolutely huge step towards framework interoperability. At last, we can build Middlewares, with applications that share a common domain side-by-side.

This code snippet is from Zend Stratigility, a middleware system based on PSR-7:

$app->pipe('/api', $apiMiddleware);
$app->pipe('/docs', $apiDocMiddleware);
$app->pipe('/files', $filesMiddleware);

It is easy to see how each middleware is actually a kind of "module" of a bigger application. Each middleware could be written using a different router. Maybe the "/api" part will be written using Slim 3 and the "/docs" part using ZF2. Maybe the "/files" part will use Silex, etc...

So far, those modules live side-by-side but they share nothing. Each module will probably have its own dependency injection container, with its own instances. So if "/api" and "/docs" both use a logger (hopefully PSR-3 compatible), they cannot share the same instance of a logger.

This is where container-interop kicks in. Container-interop is the way those modules can share container entries (therefore share a logger, a cache service, a database connection, ...) Just like PSR-7, this is a paradigm shift. PSR-7 middlewares allow an application to have more than one router. Container-interop allows an application to have more than one container.

Framework agnostic modules?

If you have a look at Symfony bundles or Zend modules, these modules generally provide routes and services (i.e. container entries). Routes are tightly coupled to the router used by the framework, and services are tightly coupled to the container used by the framework. Both container-interop and PSR-7 allow us to go framework agnostic.

Let's take a look at what a cross-framework module could look like:

interface ModuleInterface {
    /**
     * You can return a container if the module provides one.
     * Notice how the root container is passed as a parameter.
     * This allows the container to use the root container
     * for delegate lookup.
     * @see https://github.com/container-interop/fig-standards/blob/master/proposed/container-meta.md#7-delegate-lookup-feature
     *
     * The module can return "null" if it does not provide a container.
     *
     * @param ContainerInterface $rootContainer
     * @return ContainerInterface|null
     */
    function getContainer(ContainerInterface $rootContainer);

    /**
     * You can return a Zend\Stratigility\MiddlewareInterface middleware.
     *
     * @see https://github.com/zendframework/zend-stratigility#middleware
     *
     * @return MiddlewareInterface
     */
    function getHttpMiddleware();
}

This module acts as a factory for a PSR-7 compatible middleware/router and a container. The module can:

  • access the other modules entries (through the root container)
  • provide its own entries (actually, provide its own container)
  • provide its own routes (using the middleware)

So basically anything that is expected from a full-stack framework module, but in a framework agnostic way!

The future

In the near future, we will be implementing this ModuleInterface in Harmony, the upcoming framework-agnostic web-based development environment. We plan to implement a plugin system where plugins can be developed using any router, and any container, thanks to PSR-7 and ContainerInterop.

And of course, we are going to propose to move the container-interop project as an official PSR!

Stay tuned and do not hesitate to drop a comment on Twitter: @david_negrier !

Disclaimer: this idea is not entirely new. It was first proposed by Matthieu Napoli here and here, based on StackPHP that was the pre-PSR7 option for HTTP middlewares. It was further enhanced here by myself, still using StackPHP.