diff --git a/CHANGELOG.md b/CHANGELOG.md index 772d21b2..03393b86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Fifth release candidate. - [#238](https://github.com/zendframework/zend-expressive/pull/238) adds a cookbook recipe detailing how to handle serving an Expressive application from a subdirectory of your web root. +- [#239](https://github.com/zendframework/zend-expressive/pull/239) adds a + cookbook recipe detailing how to create modular Expressive applications. ### Deprecated diff --git a/doc/book/cookbook/bookdown.json b/doc/book/cookbook/bookdown.json index b0700370..0fd30aa6 100644 --- a/doc/book/cookbook/bookdown.json +++ b/doc/book/cookbook/bookdown.json @@ -6,6 +6,7 @@ {"Setting custom 404 page handling": "custom-404-page-handling.md"}, {"Registering custom view helpers when using zend-view": "using-custom-view-helpers.md"}, {"Using zend-form view helpers": "using-zend-form-view-helpers.md"}, - {"Using Expressive from a subdirectory": "using-a-base-path.md"} + {"Using Expressive from a subdirectory": "using-a-base-path.md"}, + {"Building modular applications": "modular-layout.md"} ] } diff --git a/doc/book/cookbook/modular-layout.md b/doc/book/cookbook/modular-layout.md new file mode 100644 index 00000000..12f2301e --- /dev/null +++ b/doc/book/cookbook/modular-layout.md @@ -0,0 +1,157 @@ +# How can I make my application modular? + +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. + +While zend-modulemanager could be used with Expressive, we suggest another +approach: modules that are based only on configuration. This powerful approach +doesn't affect performance, and offers extensive flexibility: each module can +provide its own services (with factories), default configuration, and routes. + +This cookbook will show how to organize modules using +[mtymek/expressive-config-manager](https://github.com/mtymek/expressive-config-manager), +a lightweight library that aggregates and merges configuration, optionally caching it. + +## Install the configuration manager + +The configuration manager is available in Packagist: + +```bash +$ composer require mtymek/expressive-config-manager +``` + +## Generate your config + +The default Expressive skeleton installs a `config/config.php` file, which +aggregates all configuration. When using the configuration manager, you will +need to replace the contents of that file with the following code: + +```php +getMergedConfig()); +``` + +If you open your application in a browser, it should still work in exactly the +same way as it was before. Now you can start adding your modules. + +## First module + +`ConfigManager` 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. + +For instance, this is how your module could provide its own routes: + +```php +namespace App; + +class AppConfig +{ + public function __invoke() + { + return [ + 'routes' => [ + [ + 'name' => 'api.list-transactions', + 'path' => '/api/transactions', + 'middleware' => App\Action\ListTransactionsAction::class, + 'allowed_methods' => ['GET'], + ], + [ + 'name' => 'api.refund-transaction', + 'path' => '/api/refund', + 'middleware' => App\Action\RefundAction::class, + 'allowed_methods' => ['POST'], + ], + ], + ]; + } +} +``` + +## Enabling the module + +Finally, you can enable your module by adding a reference to your config class +within the arguments of the `ConfigManager` constructor in the `config/config.php` +file: + +```php +$configManager = new ConfigManager([ + App\AppConfig::class, + new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), +]); +``` + +## Caching configuration + +In order to provide configuration caching, two things must occur: + +- First, you must define a `config_cache_enabled` key in your configuration + somewhere. +- Second, you must pass a second argument to the `ConfigManager`, the location + of the cache file to use. + +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` should define the value to `true`, as the + production setting. +- `config/autoload/local.php` should also define the setting, and use a value + appropriate to the current environment. In development, for instance, this + would be `false`. + +```php +// 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: + +```php +$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: + +- You pass a list of config providers to the `ConfigManager` constructor. +- Configuration is merged in the same order as it is passed, with later entries + having precedence. +- You can override module configuration using `*.global.php` and `*.local.php` files. +- If cached config is found, `ConfigManager` does not iterate over provider list. + +For more details, please refer to the +[Config Manager Documentation](https://github.com/mtymek/expressive-config-manager#expressive-configuration-manager). diff --git a/mkdocs.yml b/mkdocs.yml index 5b0e81aa..c65fe2c3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,7 +13,7 @@ pages: - { Helpers: [{ Introduction: helpers/intro.md }, { UrlHelper: helpers/url-helper.md }, { ServerUrlHelper: helpers/server-url-helper.md }] } - { Emitters: emitters.md } - { Examples: usage-examples.md } - - { Cookbook: [{ 'Prepending a common path to all routes': cookbook/common-prefix-for-routes.md }, { 'Route-specific middleware pipelines': cookbook/route-specific-pipeline.md }, { 'Setting custom 404 page handling': cookbook/custom-404-page-handling.md }, { 'Registering custom view helpers when using zend-view': cookbook/using-custom-view-helpers.md }, { 'Using zend-form view helpers': cookbook/using-zend-form-view-helpers.md }, { 'Using Expressive from a subdirectory': cookbook/using-a-base-path.md }] } + - { Cookbook: [{ 'Prepending a common path to all routes': cookbook/common-prefix-for-routes.md }, { 'Route-specific middleware pipelines': cookbook/route-specific-pipeline.md }, { 'Setting custom 404 page handling': cookbook/custom-404-page-handling.md }, { 'Registering custom view helpers when using zend-view': cookbook/using-custom-view-helpers.md }, { 'Using zend-form view helpers': cookbook/using-zend-form-view-helpers.md }, { 'Using Expressive from a subdirectory': cookbook/using-a-base-path.md }, { 'Building modular applications': cookbook/modular-layout.md }] } - { 'Expressive Projects': expressive-projects.md } site_name: zend-expressive site_description: 'zend-expressive: PSR-7 Middleware Microframework'