Using Routed Middleware class as Controller with multi actions in Expressive
Note: this post is now part of Zend\Expressive cookbook.
If you are familiar with frameworks with provide controller with multi actions functionality, like in Zend Framework 1 and 2, you may want to apply it when you use Zend\Expressive microframework as well. Usually, we need to define 1 routed middleware, 1 __invoke() with 3 parameters ( request, response, next ). If we need another specifics usage, we can create another routed middleware classes, for example:
- AlbumPageIndex
- AlbumPageEdit
- AlbumPageAdd
What if we want to use only one middleware class which facilitate 3 pages above? We can with make request attribute with ‘action’ key via route config, and validate it in __invoke()
method with ReflectionMethod.
Let say, we have the following route config:
// ... 'routes' => [ [ 'name' => 'album', 'path' => '/album[/:action][/:id]', 'middleware' => Album\Action\AlbumPage::class, 'allowed_methods' => ['GET'], ], ], // ...
To avoid repetitive code for modifying __invoke()
method, we can create an AbstractPage, like the following:
namespace App\Action; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use ReflectionMethod; abstract class AbstractPage { public function __invoke($request, $response, callable $next = null) { $action = $request->getAttribute('action', 'index') . 'Action'; if (method_exists($this, $action)) { $r = new ReflectionMethod($this, $action); $args = $r->getParameters(); if (count($args) === 3 && $args[0]->getType() == ServerRequestInterface::class && $args[1]->getType() == ResponseInterface::class && $args[2]->isCallable() && $args[2]->allowsNull() ) { return $this->$action($request, $response, $next); } } return $next($request, $response->withStatus(404), 'Page Not Found'); } }
In above abstract class with modified __invoke()
method, we check if the action attribute, which default is ‘index’ if not provided, have ‘Action’ suffix, and the the method is exists within the middleware class with 3 parameters with parameters with parameter 1 as ServerRequestInterface, parameter 2 as ResponseInterface, and parameter 3 is a callable and allows null, otherwise, it will response 404 page.
So, what we need to do in out routed middleware class is extends the AbstractPage we created:
namespace Album\Action; use App\Action\AbstractPage; use Zend\Diactoros\Response\HtmlResponse; use Zend\Expressive\Template; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; class AlbumPage extends AbstractPage { protected $template; // you need to inject via factory public function __construct(Template\TemplateRendererInterface $template) { $this->template = $template; } public function indexAction( ServerRequestInterface $request, ResponseInterface $response, callable $next = null ) { return new HtmlResponse($this->template->render('album::album-page')); } public function addAction( ServerRequestInterface $request, ResponseInterface $response, callable $next = null ) { return new HtmlResponse($this->template->render('album::album-page-add')); } public function editAction( ServerRequestInterface $request, ResponseInterface $response, callable $next = null ) { $id = $request->getAttribute('id'); if ($id === null) { throw new \InvalidArgumentException('id parameter must be provided'); } return new HtmlResponse( $this->template->render('album::album-page-edit', ['id' => $id]) ); } }
The rest is just create the view. Done 😉
Nice idea. You can improve this abstract class and prepare html response by default.
Would you mind contributing this as a recipe for the cookbook, please?
yes, will do 😉
PR applied: https://github.com/zendframework/zend-expressive/pull/262
Request, Response and $next better move to private/protected and then you don’t need duplicate this params in every action
I’ve also have question: why you check params in the reflection class? Not better way is add interfaces into __invoke() method?
add interfaces in `invoke()` doesn’t has any meaning on calling other method and match that they have 3 argument with each parameter match each type checked. `__invoke()` will always respect the interface whenever we add type hint or not.
did you a mean properties? if yes, then it is NOT the way it works, request, response, and next is used on it’s executed via `__invoke()` and it is a required parameter, when we return $next, it will call post_routing middlewares.
This can solve so many problems…
that’s great idea, i try Expressive and it look promissing. It also possible to map the request bassed http method, it maybe come in handy for anyone:
“`
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
trait DispatchMethod
{
/**
* Dispatch the request to our class method based http method
*
* @param Psr\Http\Message\ServerRequestInterface $request
* @param Psr\Http\Message\ResponseInterface $response
* @param callable $next
* @return Psr\Http\Message\ResponseInterface
*/
public function __invoke(Request $request, Response $response, callable $next)
{
if (method_exists($this, $method = strtolower($request->getMethod()))) {
return $this->$method($request, $response, $next);
}
return $next($request, $response);
}
}
“`
[…] use different middlewares every time. Abdul wrote a great article on this subject that you can find here, which also became part of Expressive’s cookbook some time […]