Modular applications

Zend Framework 2+ applications have a concept of modules, independent units that can provide configuration, services, and hooks into its MVC lifecycle. This functionality is provided by zend-modulemanager.

Expressive provides similar functionality by incorporating two packages within the default skeleton application:

These features allow you to install packages via composer and expose their configuration — which may include dependency information — to your application.

Making your application modular

When using the Expressive installer via the skeleton application, the first question asked is the installation type, which includes the options:

We recommend choosing the "Modular" option from the outset.

If you do not, you can still create and use modules in your application; however, the initial "App" module will not be modular.

Module structure

Expressive does not force you to use any particular structure for your module; its only requirement is to expose default configuration using a "config provider", which is simply an invokable class that returns a configuration array.

We generally recommend that a module have a PSR-4 structure, and that the module contain a src/ directory at the minimum, along with directories for other module-specific content, such as templates, tests, and assets:

src/
  Acme/
    src/
      ConfigProvider.php
      Container/
        VerifyUserFactory.php
      Helper/
        AuthorizationHelper.php
      Middleware/
        VerifyUser.php
    templates/
      verify-user.php
    test/
      Helper/
        AuthorizationHelperTest.php
      Middleware/
        VerifyUserTest.php

If you use the above structure, you would then add an entry in your composer.json file to provide autoloading:

"autoload": {
    "psr-4": {
        "Acme\\": "src/Acme/src/"
    }
}

Don't forget to execute composer dump-autoload after making the change!

Creating and enabling a module

The only requirement for creating a module is that you define a "config provider", which is simply an invokable class that returns a configuration array.

Generally, a config provider will return dependency information, and module-specific configuration:

namespace Acme;

class ConfigProvider
{
    public function __invoke()
    {
        return [
            'dependencies' => $this->getDependencies(),
            'acme' => [
                'some-setting' => 'default value',
            ],
            'templates' => [
                'paths' => [
                    'acme' => [__DIR__ . '/../templates'],
                ],
            ]
        ];
    }

    public function getDependencies()
    {
        return [
            'invokables' => [
                Helper\AuthorizationHelper::class => Helper\AuthorizationHelper::class,
            ],
            'factories' => [
                Middleware\VerifyUser::class => Container\VerifyUserFactory::class,
            ],
        ];
    }
}

You would then add the config provider to the top (or towards the top) of your config/config.php:

$aggregator = new ConfigAggregator([
    Acme\ConfigProvider::class,
    /* ... */

This approach allows your config/autoload/* files to take precedence over the module configuration, allowing you to override the values.

Caching configuration

In order to provide configuration caching, two things must occur:

The config_cache_enabled key can be defined in any of your configuration providers, including the autoloaded configuration files. We recommend defining them in two locations:

// config/autoload/global.php

return [
    'config_cache_enabled' => true,
    /* ... */
];

// config/autoload/local.php

return [
    'config_cache_enabled' => false, // <- development!
    /* ... */
];

You would then alter your config/config.php file to add the second argument. The following example builds on the previous, and demonstrates having the AppConfig entry enabled. The configuration will be cached to data/config-cache.php in the application root:

$configManager = new ConfigManager([
    App\AppConfig::class,
    new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
], 'data/config-cache.php');

When the configuration cache path is present, if the config_cache_enabled flag is enabled, then configuration will be read from the cached configuration, instead of parsing and merging the various configuration sources.

Final notes

This approach may look simple, but it is flexible and powerful:

For more details, please refer to the zend-config-aggregator documentation.