Project Scalability with Zend Expressive

21 July 2016 Comments

Warning! This post was published over 7 years ago, so it can contain outdated information. Bear this in mind when putting it into practice or leaving new comments.

I’ve been working with some different frameworks lately. One of them is Zend Expressive, and I’ve come to the conclusion that I don’t need to choose between different frameworks; depending on the project, Expressive always fits my needs and scales from small projects to bigger applications.

The Microframework approach

The first thing that one could see on the “Hello world” example is that Expressive seems like a typical microframework: you create an application object and register routes with it.

Something like this:

use zend\Expressive\AppFactory;

include __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();

$app->get('/greet/{name}', function ($request, $response, callable $out = null) {
    $response->getBody()->write(sprintf('Hello %s!!', $request->getAttribute('name')));
    return $response;
});
$app->('/about', function ($request, $response, callable $out = null) {
    ob_start();
    include 'templates/about.html';
    $response->getBody()->write(ob_get_clean());
    return $response;
});

$app->pipeRoutingMiddleware();
$app->pipeDispatchMiddleware();
$app->run();

This is a super simple example, which I feel is the best approach for prototyping and small applications. You can write an entire app in hours.

This is the so called “programmatic approach”, and you can read more about it here. This same approach is used by other microframeworks.

However, if your application grows, this could be hard to maintain.

  • What if your application has 100 different routes instead of 2?
  • What if you need to render dynamic templates?
  • What if you need to use complex services while dispatching your routes? They will need to be created somewhere.

In the past, you had to take this into account. A microframework was good for small projects (websites, blogs, and such), but for bigger projects, one would choose frameworks like Symfony, Laravel or Zend Framework with the MVC stack. This is no longer necessary with Zend Expressive.

Zend Expressive allows the previously described approach, but also other approaches reminiscent of these bigger frameworks. Some of the features that make Zend Expressive my framework of choice for any kind of project are:

  • Decoupled from specific implementations for dependency injection, templating, or routing
  • Supports a configuration-driven approach.
  • Supports complex dependency injection by using advanced dependency injection containers like zend-servicemanager.
  • Supports modular applications, easing code reuse.
  • It is based on middleware.
  • It is not hard to implement a mvc-like system with controllers.

Choosing implementations

The first thing that makes Expressive different from other frameworks is that it doesn’t come with its own implementation for everything. It doesn’t try to reinvent the wheel; instead, it relies on interfaces. You can use the component of your choice for routing, templating, and dependency injection. You could also implement your own if you prefer.

For example, by default, Expressive supports three routers: FastRoute, Aura router, and zend-router. In my website I needed a router with support for optional parameters at the beginning of the route, and none of them supports it.

My solution was integrating Slim 2 router, which allows that.

Something similar is possible while choosing the template renderer. Expressive comes with built-in support for Twig, Plates, and zend-view, but you can use whichever renderer you like. It wouldn’t be hard to integrate Laravel’s Blade, for example.

For dependency injection, Zend Expressive supports any container implementing Interop\Container\ContainerInterface, so there are plenty of options to choose from.

Configuration-driven applications

One of the best things about Zend Expressive is that you can move all the configuration to dynamic configuration files, including:

  • Definition of routes
  • Definition of middlewares to pipe
  • Definition of template renderer, router, and container implementations
  • Definition of configuration consumed by other services

This approach also allows to define environment-specific configuration, and override production configurations with local configurations, like disabling cache or having a more verbose error handler (which is not desired in production but will help during development).

In order to achieve this, instead of creating the application with the static factory (Zend\Expressive\AppFactory), you have to use the container factory (Zend\Expressive\Container\ApplicationFactory).

It is intended to be used with a dependency injection container, and a more complex process is used while creating the application.

It fetches the configuration of the application itself from a config service, which should contain an array (or ArrayObject) with keys like routes or middleware-pipeline.

For example, if we want to register the same routes defined in the first example via configuration, we need to define something like this.

return [

    'routes' => [
        [
            'name' => 'greet',
            'path' => '/greet/{name}',
            'middleware' => function ($request, $response, callable $out = null) {
                $response->getBody()->write(sprintf('Hello %s!!', $request->getAttribute('name')));
                return $response;
            },
            'allowed_methods' => ['GET'],
        ],
        [
            'name' => 'about',
            'path' => '/about',
            'middleware' => function ($request, $response, callable $out = null) {
                ob_start();
                include 'templates/about.html';
                $response->getBody()->write(ob_get_clean());
                return $response;
            },
            'allowed_methods' => ['GET'],
        ],
    ],

];

I have used middleware closures to demonstrate that any middleware used in the programmatic approach is valid here, but you should avoid it while working with the configuration-driven approach, since it will break the configuration caching process.

With this, the ApplicationFactory preregisters all the routes before returning the Application instance.

The same happens with non-routable middleware (we’ll talk about middleware later), and other configurations.

With this approach, the first thing we need to do is create the dependency injection container, which will be responsible of creating the application object. Something like this:

use Interop\Container\ContainerInterface;
use Zend\Expressive\Application;

/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
/** @var Application $app */
$app = $container->get(Application::class);
$app->run();

Note that Expressive projects can be easily created from scratch by using the skeleton installer, which in the process will ask you for the concrete implementations to use and initialize all the code above, including base configuration files.

While the configuration-driven approach would be my choice, it is also possible to take a hybrid approach between the programmatic and the config-driven options.

You can use the container factory so that the environment-specific configuration system is applied, and dependencies and service-specific configs are properly loaded, but still define routes programmatically.

use Interop\Container\ContainerInterface;
use Zend\Expressive\Application;

/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
/** @var Application $app */
$app = $container->get(Application::class);

// After getting the application from the container, it is still possible to define routes
$app->get('/home', ...);
$app->post('/contact', ...);

$app->run();

Complex dependency injection

We have already seen that Zend Expressive allows you to use the implementation of your choice for dependency injection.

In a medium project you can use a simple DI container, like pimple or aura DI. However, in a bigger project you will probably need a more advanced container, like php-di or zend-servicemanager. Expressive allows you to use any container that implements Interop\Container\ContainerInterface.

If none is defined, the static AppFactory uses zend-servicemanager, and the ApplicationFactory injects into the application the same container that was used to invoke it.

Other microframeworks assume that you are working on a small project, and you are a little bit limited by their DI implementation. Expressive gives you the freedom to use the implementation that best suits your needs.

Modular applications

For big projects, modularity is very important.

You can wrap classes, configuration, language files, templates, tests, etc, in a self-contained package that can be installed in other projects.

Other frameworks like Zend Framework and Symfony provide great solutions for this (modules and bundles), and it is easy to do this with Expressive too, while you can still work in a single-module way if the project doesn’t need more complexity.

In order to add modularity to an Expressive project, you can use the mtymek/expressive-config-manager package. It includes a simple class that is able to merge the configuration from many sources (while working with the configuration-driven approach), so that the application is created by being aware of all of those modules, and thus, get preconfigured services or override other configurations.

You just need to define the configuration of every module and an invokable class that provides the configuration itself.

Indeed, that package can be used in any project, not just Expressive-based projects, but it was built with Zend Expressive in mind.

The middleware paradigm

New microframeworks are working with the middleware paradigm. Expressive is one of them.

Middleware is a great way of defining pieces of code that are easy to test and reuse in other middleware-based projects.

If you need to learn more about middleware, this is a great article to read now.

Expressive uses middleware not only before dispatching a route, but also for the route itself, so, in the end, everything is middleware.

Also, unlike other microframeworks where you need to provide callables every time you need a middleware, Expressive supports passing service names, which makes it use the DI container in order to get the middleware instance. This improves performance and eases dependency injection.

This approach can be used both in the programmatic and config-driven approaches.

use Zend\ServiceManager\ServiceManager;

$sm = new ServiceManager([
    'invokables' => [
         \MyMiddleware::class => MyMiddleware::class,
    ],
]);

$app = AppFactory::create($sm);
$app->get('/home', \MyMiddleware::class);

// [...]

When this app is run and the “/home” route is dispatched, the middleware used to dispatch the request will be lazily fetched from the DI container and invoked.

This same thing could be done with the config-driven approach like this.

use Zend\Expressive\Application;
use Zend\ServiceManager\ServiceManager;

$sm = new ServiceManager([
    'services' => [
        'config' => [
            'routes' => [
                [
                    'name' => 'home',
                    'middleware' => \MyMiddleware::class,
                    'path' => '/home',
                    'allowed_methods' => ['GET'],
                ],
            ],
        ],
    ],
    'invokables' => [
         \MyMiddleware::class => MyMiddleware::class,
    ],
    'factories' => [
        Application::class => Zend\Expressive\Container\ApplicationFactory::class,
    ],
]);

$app = $sm->get(Application::class);

// [...]

Controller-style dispatch

The last thing we need so that Expressive is as similar as possible to bigger frameworks, is being able to dispatch similar requests with the same controller class, allowing us to share dependencies and not having to repeat the same creation process for different middlewares.

This is also possible by using Abdul’s implementation, which is now part of the official Expressive cookbook.

This approach is similar to that used in traditional full-stack MVC controllers.

You can also easily define rest controllers as I explained in my blog not so long ago.

However, this should be primarily used if you want to migrate an existing application to Zend Expressive. For new projects it’s better to have a single middleware to handle each route. That approach improves granularity, and makes middlewares easier to test.

The redundancy problem that is produced by this approach can be easily mitigated by reusing the same factory for all the middlewares.

Conclusion

In this article, I’ve attempted to demonstrate an undocumented feature of Zend Expressive: project scalability.

The project offers a low learning curve, allowing you to immediately start writing smaller projects, but provides a path for creating larger, maintainable applications.