Welcome to Abdul Malik Ikhsan's Blog

Using Routed Middleware class as Controller with multi actions in Expressive

Posted in Tutorial PHP, Zend Framework by samsonasik on January 3, 2016

Note: this post is now part of Zend\Expressive cookbook.

multi-action-1-middleware
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:

  1. AlbumPageIndex
  2. AlbumPageEdit
  3. 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 😉

Tagged with:

11 Responses

Subscribe to comments with RSS.

  1. Witold Wasiczko said, on January 3, 2016 at 12:49 am

    Nice idea. You can improve this abstract class and prepare html response by default.

  2. weierophinney said, on January 7, 2016 at 3:51 am

    Would you mind contributing this as a recipe for the cookbook, please?

  3. stormfly-pl said, on January 7, 2016 at 6:54 pm

    Request, Response and $next better move to private/protected and then you don’t need duplicate this params in every action

    • stormfly-pl said, on January 7, 2016 at 7:00 pm

      I’ve also have question: why you check params in the reflection class? Not better way is add interfaces into __invoke() method?

      • samsonasik said, on January 8, 2016 at 3:43 am

        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.

    • samsonasik said, on January 8, 2016 at 3:39 am

      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.

  4. Jakub Igla said, on January 8, 2016 at 12:05 am

    This can solve so many problems…

  5. syaiful Bahri said, on May 18, 2016 at 2:01 pm

    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);
    }
    }
    “`

  6. […] 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 […]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: