woohoolabs / harmony

A simple and flexible PHP middleware dispatcher based on PSR-7, PSR-11, and PSR-15
MIT License
164 stars 16 forks source link

Add better support for dispatching middleware conditionally #11

Closed kocsismate closed 8 years ago

kocsismate commented 8 years ago

After a discussion in https://github.com/php-http/http-kernel/pull/1, I'd want to add native support for dispatching middleware conditionally in Harmony 3.0.

After experimenting a little bit with the idea, I found that a really elegant way to enable this functionality is to use a Condition object - a special kind of middleware - which is responsible for invoking a callable if a certain condition is met. When invoked, this callable receives the current instance of the framework (Harmony class) as its single parameter. This looks like the following:

$harmony = new Harmony($request, $response);
$harmony
    ->add(
        new HttpMethodCondition(
            ["DELETE", "PATCH", "POST", "PUT"],
            function (Harmony $harmony) {
                $harmony->insert(new CsrfMiddleware());
            }
        )
    )
    ->add(new FastRouteMiddleware($router))
    ->add(new DispatcherMiddleware())
    ->addFinal(new DiactorosResponderMiddleware(new SapiEmitter()));

$harmony();

What exactly happens here? First, all the middleware are added to Harmony via the add() and addFinal() methods. Then, they are executed each after each: as the first middleware is a Condition, it evaluates first whether the current HTTP method has side-effects. If so, the CsrfMiddleware gets inserted via the insert() method right after the currently evaluated middleware (which is now the CsrfMiddleware).

Then the execution is continued either with CsrfMiddleware or FastRouteMiddleware, and then with DispatcherMiddleware. Finally, the HTTP response gets emitted by DiactorosResponderMiddleware.

With this implementation, you can even remove middleware at your own will or do other nasty tricks (however I am not sure it is a really good idea to do such things):

$apiMiddleware = new PathCondition(["/api/*"], function (Harmony $harmony) {
    $harmony
        ->remove("csrf_middleware")
        ->insert(new BasicAuthenticationMiddleware())
        ->add(new CorsMiddleware());
});

$frontendMiddleware = new PathCondition(["/frontend/*"], function (Harmony $harmony) {
    $harmony
        ->insert(new SessionAuthenticationMiddleware());
});

$harmony = new Harmony($request, $response);
$harmony
    ->add(new FastRouteMiddleware($router))
    ->add($apiMiddleware)
    ->add($frontendMiddleware)
    ->add(new CsrfMiddleware(), "csrf_middleware");
    ->add(new DispatcherMiddleware())
    ->addFinal(new DiactorosResponderMiddleware(new SapiEmitter()));

$harmony();

The advantages of this idea is that it is very easy to implement, it is trivial to define custom and complex conditions, and none of the unneeded middleware will be instantiated. The only downside I can see is that the API of the framework becomes a little bit more complex: we have both an add() and an insert() methods (or methods with more intention-revealing names).

So now, I'd like to gather some feedback about my idea, so I would be happy if you could provide your possible use-cases or collect some pros and cons :)

kocsismate commented 8 years ago

Another downside I've just become aware is that Condition objects can't be reused: they have to instantiated as many times as we need them. However, in my opinion it isn't really difficult to fix this problem.

sagikazarmark commented 8 years ago

Hm, why not just dispatch the middleware if the condition is met? Why need to insert the middleware into the chain? I think it cause quite a few number of confusion.

One example: you add a middleware which formats the output as JSON if the path starts with /api, but the action serving the content does a subrequest, which is not under API, but harmony already contains the output formatting middleware. What would happen then?

Also, insert where? If you just dispatch the middleware when the condition is met you can be sure of the order.

Not to mention, that adhering the middleware chain AFTER building it is probably not a good idea, could cause some confusion as well.

Or am I missing something?

kocsismate commented 8 years ago

Thank you very much for your feedback! It was really helpful, but I only had time today to rework my implementation and answer your message. So now my first example looks like the following:

$harmony = new Harmony($request, $response);
$harmony
    ->addCondition(
        new HttpMethodCondition(["POST"]),
        function (Harmony $harmony) {
            $harmony
                ->addMiddleware(new BodyParserMiddleware())  
                ->addMiddleware(new CsrfMiddleware());
        }
    )
    ->addMiddleware(new FastRouteMiddleware($router))
    ->addMiddleware(new DispatcherMiddleware())
    ->addFinalMiddleware(new DiactorosResponderMiddleware(new SapiEmitter()));

$harmony();

Notable changes:

kocsismate commented 8 years ago

I am closing this issue because in my opinion, I fixed the issues mentioned. If you have any concerns or objections, feel free to reopen it.