Caution
The documentation you are viewing is for an older version of this component.
Switch to the latest (v3) version.
Cookbook
Handling multiple routes in a single class
Typically, in Expressive, we would define one middleware class per route. For a standard CRUD-style application, however, this leads to multiple related classes:
- AlbumPageIndex
- AlbumPageEdit
- AlbumPageAdd
If you are familiar with frameworks that provide controllers capable of handling multiple "actions", such as those found in Zend Framework 1 and 2, Symfony, CodeIgniter, CakePHP, and other popular frameworks, you may want to apply a similar pattern when using Expressive.
In other words, what if we want to use only one middleware class to facilitate all three of the above?
In the following example, we'll use an action
routing parameter which our
middleware class will use in order to determine which internal method to invoke.
Consider the following route configuration:
return [
/* ... */
'routes' => [
/* ... */
[
'name' => 'album',
'path' => '/album[/{action:add|edit}[/{id}]]',
'middleware' => Album\Action\AlbumPage::class,
'allowed_methods' => ['GET'],
],
/* ... */
],
];
The above defines a route that will match any of the following:
/album
/album/add
/album/edit/3
The action
attribute can thus be one of add
or edit
, and we can optionally
also receive an id
attribute (in the latter example, it would be 3
).
Routing definitions may vary
Depending on the router you chose when starting your project, your routing definition may differ. The above example uses the default
FastRoute
implementation.
We might then implement Album\Action\AlbumPage
as follows:
namespace Album\Action;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Expressive\Template\TemplateRendererInterface;
class AlbumPage
{
private $template;
public function __construct(TemplateRendererInterface $template)
{
$this->template = $template;
}
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
callable $next = null
) {
switch ($request->getAttribute('action', 'index')) {
case 'index':
return $this->indexAction($request, $response, $next);
case 'add':
return $this->addAction($request, $response, $next);
case 'edit':
return $this->editAction($request, $response, $next);
default:
// Invalid; thus, a 404!
return $response->withStatus(404);
}
}
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', false);
if (! $id) {
throw new \InvalidArgumentException('id parameter must be provided');
}
return new HtmlResponse(
$this->template->render('album::album-page-edit', ['id' => $id])
);
}
}
This allows us to have the same dependencies for a set of related actions, and, if desired, even have common internal methods each can utilize.
This approach is reasonable, but requires that I create a similar __invoke()
implementation every time I want to accomplish a similar workflow. Let's create
a generic implementation, via an AbstractPage
class:
namespace App\Action;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
abstract class AbstractPage
{
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
callable $next = null
) {
$action = $request->getAttribute('action', 'index') . 'Action';
if (! method_exists($this, $action)) {
return $response->withStatus(404);
}
return $this->$action($request, $response, $next);
}
}
The above abstract class pulls the action
attribute on invocation, and
concatenates it with the word Action
. It then uses this value to determine if
a corresponding method exists in the current class, and, if so, calls it with
the arguments it received; otherwise, it returns a 404 response.
Invoking the error stack
Instead of returning a 404 response, you could also invoke
$next()
with an error:return $next($request, $response, new NotFoundError());
This will then invoke the first error handler middleware capable of handling the error.
Our original AlbumPage
implementation could then be modified to extend
AbstractPage
:
namespace Album\Action;
use App\Action\AbstractPage;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Expressive\Template\TemplateRendererInterface;
class AlbumPage extends AbstractPage
{
private $template;
public function __construct(TemplateRendererInterface $template)
{
$this->template = $template;
}
public function indexAction( /* ... */ ) { /* ... */ }
public function addAction( /* ... */ ) { /* ... */ }
public function editAction( /* ... */ ) { /* ... */ }
}
Or use a trait
As an alternative to an abstract class, you could define the
__invoke()
logic in a trait, which you then compose into your middleware:namespace App\Action; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; trait ActionBasedInvocation { public function __invoke( ServerRequestInterface $request, ResponseInterface $response, callable $next = null ) { $action = $request->getAttribute('action', 'index') . 'Action'; if (! method_exists($this, $action)) { return $response->withStatus(404); } return $this->$action($request, $response, $next); } }
You would then compose it into a class as follows:
namespace Album\Action; use App\Action\ActionBasedInvocation; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Zend\Diactoros\Response\HtmlResponse; use Zend\Expressive\Template\TemplateRendererInterface; class AlbumPage { use ActionBasedInvocation; private $template; public function __construct(TemplateRendererInterface $template) { $this->template = $template; } public function indexAction( /* ... */ ) { /* ... */ } public function addAction( /* ... */ ) { /* ... */ } public function editAction( /* ... */ ) { /* ... */ } }
Found a mistake or want to contribute to the documentation? Edit this page on GitHub!