Compose modular Expressive application with “ZF2 style” structure
Yes, there is a doc for compose modular application in Zend\Expressive. And No, I won’t debate about how non-shareable module shouldn’t be in other modules (only “root” module), which is on personal preference. In this post, I just want to show you how to compose modular application like the “ZF2” structure, with config, view, and class that require the config per-“module”, and that’s do-able!.
Let’s do it!:
1) Same as doc, requiring mtymek/expressive-config-manager
:
$ composer require mtymek/expressive-config-manager
2) Register your modules as namespace in composer autoload:
// composer.json // ... "autoload": { "psr-4": { "App\\": "src/App/", "Album\\": "src/Album/" } }, // ...
3) Run dump-autoload
$ composer dump-autoload
4) Let’s extract one by one per-module, “App” and “Album”
a. src/App/AppConfig.php
//src/App/AppConfig.php namespace App; class AppConfig { public function __invoke() { return include __DIR__ . '/config/expressive-config.php'; } }
b. src/Album/AlbumConfig.php
//src/Album/AlbumConfig.php namespace Album; class AlbumConfig { public function __invoke() { return include __DIR__ . '/config/expressive-config.php'; } }
c. src/App/config/expressive-config.php
You can move various arrays from config/autoload/routes.global.php
to src/App/config/expressive-config.php
:
return [ 'dependencies' => [ 'invokables' => [ App\Action\PingAction::class => App\Action\PingAction::class, ], 'factories' => [ App\Action\HomePageAction::class => App\Action\HomePageFactory::class, ], ], 'routes' => [ [ 'name' => 'home', 'path' => '/', 'middleware' => App\Action\HomePageAction::class, 'allowed_methods' => ['GET'], ], [ 'name' => 'api.ping', 'path' => '/api/ping', 'middleware' => App\Action\PingAction::class, 'allowed_methods' => ['GET'], ], ], ];
d. src/Album/config/expressive-config.php
Like App
config, you can define:
return [ 'dependencies' => [ 'factories' => [ Album\Action\AlbumPageAction::class => Album\Action\AlbumPageFactory::class, ], ], 'routes' => [ [ 'name' => 'album', 'path' => '/album', 'middleware' => Album\Action\AlbumPageAction::class, 'allowed_methods' => ['GET'], ], ], ];
5) Templates
As in the “App” and “Album” modules, at this case, we need template, we can define templates path in the config:
a. for App config
// src/App/config/expressive-config.php return [ // ... 'templates' => [ 'paths' => [ 'app' => [ __DIR__ . '/../templates/app'], ], ], ];
Note:
As it will redundant with “app” key registered in config/autoload/templates.global.php
, you need to remove “app” key under “paths” in config/autoload/templates.global.php
. Don’t forget to remove the “app” directory inside rootproject/templates to avoid confusion.
b. for Album config
// src/Album/config/expressive-config.php return [ // ... 'templates' => [ 'paths' => [ 'album' => [ __DIR__ . '/../templates/album'], ], ], ];
Now, you can have the following structure:
6) Register the “modules” in config/config.php
:
use Zend\Expressive\ConfigManager\ConfigManager; use Zend\Expressive\ConfigManager\PhpFileProvider; $configManager = new ConfigManager([ App\AppConfig::class, Album\AlbumConfig::class, // ... new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), ]); return new ArrayObject($configManager->getMergedConfig());
It’s done! You can continue with writing middlewares! Tired to follow the steps? Just clone and install from here: https://github.com/samsonasik/learn-expressive-modular
Using Expressive’s middleware in Penny Framework
If we are using Penny Framework, we can use Expressive‘s middleware. We can do that with passing callable $next
that handle request and response with Penny Event. The $next
is simply like the following:
$next = function($request, $response) use ($event) { $event->setRequest($request); $event->setResponse($response); };
and we are ready to go! Let’s try to do real example. We need to use los/basepath
that can apply “base path” in our application that point to classic-app/public
and we can to call: /classic-app/public
after our host.
$ composer require los/basepath
We need to invoke the middleware to make it called. So, if we do in ‘bootstrap’ event, we can do:
$eventManager->attach('bootstrap', function($event) { $next = function($request, $response) use ($event) { $event->setRequest($request); $event->setResponse($response); }; $basepathMiddleware = new \LosMiddleware\BasePath\BasePath( '/classic-app/public' ); $basepathMiddleware( $event->getRequest(), $event->getResponse(), $next ); });
This is the case if we are using penny skeleton app and we install under ‘classic-app’ directory, and the “front controller” is in public/index.php
.
For routed middleware, we can just do the register the middleware as controller:
// config/di.php // ... 'router' => function () { return \FastRoute\simpleDispatcher(function (\FastRoute\RouteCollector $r) { $r->addRoute( 'GET', '/', ['App\Controller\IndexController', 'index'] ); $r->addRoute( 'GET', '/api/ping', ['App\Controller\PingAction', '__invoke'] ); }); }, // ...
That’s it 😉
Middleware implementation in Penny Framework
Penny is a microframework that has different idea for accomplishing middleware implementation. If we found other frameworks that implements this signatures:
function ($request, $response, $next = null);
In Penny Framework, Middleware implemented via listener that has priority that we call before or/and after. If we need to initialize application flow, you may use ‘bootstrap’ event:
// config/di.php use App\Middleware\BootstrapMiddleware; use function DI\decorate; return [ 'event_manager' => decorate(function($eventManager, $container) { $eventManager->attach( 'bootstrap', [$container->get(BootstrapMiddleware::class), 'bootstrap'] ); // other attach here... return $eventManager; }), // other service definitions here... ];
We want the middleware executed in all application flow in all controller in the end? use ‘*’ and negative priority:
// config/di.php use App\Middleware\AllEnderMiddleware; use function DI\decorate; return [ 'event_manager' => decorate(function($eventManager, $container) { $eventManager->attach( '*', [$container->get(AllEnderMiddleware::class), 'end'], -1 ); // other attach here... return $eventManager; }), // other service definitions here... ];
Wanna handle for specific controller’s action ? Use following usage:
// config/di.php use App\Controller\IndexController; use App\Middleware\IndexMiddleware; use function DI\decorate; return [ 'event_manager' => decorate(function($eventManager, $container) { // before ... $eventManager->attach( IndexController::class.'.index', [$container->get(IndexMiddleware::class), 'pre'], 1 ); // after ... $eventManager->attach( IndexController::class.'.index', [$container->get(IndexMiddleware::class), 'post'], -1 ); // other attach here... return $eventManager; }), // other service definitions here... ];
If we use default Penny EventManager implementation, which is usage Zend Framework 2 EventManager, higher priority will be served first, lower priority will be served later.
If you are familiar with CakePHP, there is already a CakePHP Event implementation, that will serve lower priority first, higher later.
For callable listener, we may create listener service like the following:
namespace App\Middleware; class IndexMiddleware { public function pre($event) { $request = $event->getRequest(); $response = $event->getResponse(); // manipulate request or/and response here $request = $request->withAttribute('foo', 'fooValue'); // apply manipulated request or/and response $event->setRequest($request); $event->setResponse($response); } public function post($event) { $request = $event->getRequest(); $response = $event->getResponse(); // manipulate request or/and response here $response = $response->withHeader('X-Powered-By', 'JAVA'); // apply manipulated request or/and response $event->setRequest($request); $event->setResponse($response); } }
We can then register the middleware in our Service Container:
//config/di.php use App\Middleware\AllEnderMiddleware; use App\Middleware\BootstrapMiddleware; use App\Middleware\IndexMiddleware; use function DI\object; return [ // ... AllEnderMiddleware::class => object(AllEnderMiddleware::class), BootstrapMiddleware::class => object(BootstrapMiddleware::class), IndexMiddleware::class => object(IndexMiddleware::class), ];
2 comments