yiisoft / yii-web

Yii web components
https://www.yiiframework.com/
BSD 3-Clause "New" or "Revised" License
78 stars 46 forks source link

Support PSR-15 HTTP Middleware #11

Closed klimov-paul closed 5 years ago

klimov-paul commented 6 years ago

Introduce the suport for PSR-15 HTTP Middleware.

Proposed PSR draft: https://github.com/php-fig/fig-standards/tree/master/proposed/http-handlers

Unofficial, but widely-used, PSR implementation is the http-interop/http-server-middleware: https://github.com/http-interop/http-server-middleware

In order to provide the support there should be abilty to setup handlers matching the Psr\Http\Server\MiddlewareInterface:

interface MiddlewareInterface
{
    /**
     * Process an incoming server request and return a response, optionally delegating
     * response creation to a handler.
     *
     * @param ServerRequestInterface $request
     * @param RequestHandlerInterface $handler
     *
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
}
samdark commented 6 years ago

PSR-15 will be voted for in January: https://twitter.com/nauleyco/status/946880932706840576

samdark commented 6 years ago

So we should rely on final "http-interop/http-server-middleware": "^1.0.1". package.

samdark commented 6 years ago

It would be good to check https://framework.zend.com/blog/2017-12-14-expressive-3-dev.html as well.

klimov-paul commented 6 years ago

Looking at MiddlewareInterface::process() I am not sure in which way it should be integrated. This method looks like Middleware handler always produces a final response to be send back to the client.

// we have PSR-compatible server request passing to handler and gain response in return without any conditions or other posibilities
$psrResponse = $middleware->process(Yii::$app->request, $someMiddlewareRequestHandler);

It looks like middleware is some final request handler. At least several middlewares can not be processed in the chain or stack. Each middleware handler call produces brand new response object and PSR does not provide any way to merge them.

All this does not look matching 'middleware' term to me. What exactly do I miss?

rob006 commented 6 years ago

@klimov-paul It works like:

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) {
    // do something with $request
    $response = $handler->process($request, $this->getNext());
    // do something with response
    return $response;
}

So it looks like onion - only middleware in the middle creates new response.

klimov-paul commented 6 years ago

So what? It still means that 2 separated handlers can not be run in chain. I can not setup 2 independent middlewares from different sources to handle sole request.

In the Yii scope PSR middleware is particlar implementation of controller action, which handlers incoming web request and generates web response. While I expect it to function like action filters, which can be applied for module, application or controller, and being independent allowing multiple middleware to function.

klimov-paul commented 6 years ago

Could someone post a link for several 3ed party middleware implementations, which are supposed to be integrated into PSR-7 compatible web application? Perhaps while reviewing the most popular middlewares I can understand the whole meaning of it.

klimov-paul commented 6 years ago

From what I see now, the middleware Yii integration is the matter of creation of predefined Action clases, in the following way:

class MiddlewareAction extends \yii\base\Action
{
    public $handler;
    public $middleware;

    public function run()
    {
        $middleware = Instance::ensure($this->middleware, MiddlewareInterface::class);
        $handler = Instance::ensure($this->handler, RequestHandlerInterface::class);
        return $middleware->process(Yii::$app->getRequest(), $handler);
    }
}

class RequestHandlerAction extends \yii\base\Action
{
    public $handler;

    public function run()
    {
        $handler = Instance::ensure($this->handler, RequestHandlerInterface::class);
        return $handler->handle(Yii::$app->getRequest());
    }
}

Such actions should function fine with 2.1 branch.

rob006 commented 6 years ago

It still means that 2 separated handlers can not be run in chain. I can not setup 2 independent middlewares from different sources to handle sole request.

You can do this with dispatcher that wraps separate middlewares and runs it in chain. See https://github.com/mindplay-dk/middleman/blob/master/src/Dispatcher.php

klimov-paul commented 6 years ago

So, it simply looses the previous response from the chain as I can see, returning only the last one. Does not seem right, to be honest. However, this means solution I have proposed earlier should do just fine.

rob006 commented 6 years ago

So, it simply looses the previous response from the chain

No, it's not. It returns response from previous middleware.

klimov-paul commented 6 years ago

Can you point me to the place, where 2 responses from 2 different handlers are merged?

rob006 commented 6 years ago

They're never merged, because there is no 2 responses.

In this case response is created in "view", passed to "LastVisited" middleware, which passes it to "Authentication", which passes it to "Session", which passes it to "Common". Each middleware can modify response by with*() method, but this is still the same response (or at least clone of base response created in "view" - center of "onion").

klimov-paul commented 6 years ago

This means particular handler should be designed in the mean of usage another handler inside it. E.g. this will work only particular middleware is aware of another middleware in the chain, creating explicit dependency. I would say this is not very practical, but it also means we should not implement middleware stack or chain inside the framework- that makes our life easier. So indeed all we need is creation of 2 dedicated actions and perhaps an action filter.

rob006 commented 6 years ago

This means particular handler should be designed in the mean of usage another handler inside it. E.g. this will work only particular middleware is aware of another middleware in the chain, creating explicit dependency.

You're wrong. You have a working solution (middleman) that is able to create chain with random middlewares - try to read its code more carefully or at least run it.

klimov-paul commented 6 years ago

You're wrong. You have a working solution (middleman) that is able to create chain with random middlewares - try to read its code more carefully or at least run it.

Sorry but I can not see it. THe only assumption (or clue) which I can imagine i that particular RequestHandlerInterface instance may return null instead of Response instance indicating that request should proceed further. Otherwise middleware stack calls are impossible without response loss.

klimov-paul commented 6 years ago

I think I understand. So $handler at Middleware::process() method is supposed to be a fallback, which is called only in case Middleware can not resolve request on its own. E.g.:

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler)
{
    if (some condition) {
        $response = new MiddlewareResponse();
        // ....
        retrun $response;
    }

    return $handler->handle($request);
}

Thus middleman creates an atrificial wrapper matching RequestHandlerInterface around any MiddlewareInterface instance passed in its stack.

Jesus, this is a insanity. Only some Symfony-infested mind could create just a twisted solution for the simpliest task.

klimov-paul commented 6 years ago

Summarizing: Middleware is just an analog for the current action filters system, which uses callback-like approach instead of events for the same goal. E.g. RequestHandlerInterface should wrap Application::runAction() and be passed to developer-defined middleware. Particular middleware may either compose its own response and use passed handler as a fallback (matches ActionFilter::beforeAction()) or invoke handler and modify response created by it (matches ActionFilter::afterAction()).

Although middleware can be put inside ActionFilter it will unlikely be an acceptable solution, because in this case developer will have to decide whether to run middleware at ActionFilter::beforeAction() or ActionFilter::afterAction(). Thus it seems that current Yii filter system should be dropped and rewritten as middleware. However, this eliminates the ability of usage action filters for the console requests.

klimov-paul commented 6 years ago

Relates to #15071, #13922, #10659

ElisDN commented 6 years ago

@klimov-paul, It is just functional approach.

Let's take a simple action:

$action = function ($request) {
    return new HtmlResponse('Hello!');
};

$response = $action($request);

For example, we need to add authentication and profiler around of the action.

We can define some middleware. They look like decorators:

// Redirects all guests to login page 
$auth = function ($request, callable $next) {
    if (!$request->...) {
        return new RedirectResponse('/login');
    }
    return $next($request);
}

// Runs the next middleware/action and merges custom header to its response
$profiler = function ($request, callable $next) {
    $start = microtime(true);
    $response = $next($request);
    $stop = microtime(true);
    return $response->withHeader('X-Profiler-Time', $stop - $start);
}

And we can construct a pipe with nested $next callbacks:

$pipeline = function ($request) {
    return $profiler($request, function ($request) {
        return $auth($request, function ($request) {
            return $action($request);
        });
    });
};

$response = $pipeline($request);

Or by Object Oriented style we can write a pipeline object with internal recursion for simplier usage:

$pipeline = new Pipeline();

$pipeline->pipe(new ProfilerMiddleware());
$pipeline->pipe(new AuthMiddleware());
$pipeline->pipe(new HelloAction());

$response = $pipeline($request);

And more theory and code I showed on http://www.elisdn.ru/blog/115/psr7-framework-middleware screencast.

klimov-paul commented 6 years ago

Solution proposed at https://github.com/yiisoft/yii2/pull/15459

petrgrishin commented 5 years ago

Hi everyone!

My thoughts on the topic, example of use:

<?php
use PetrGrishin\Pipe\Pipe;

// Class name
$accessFiltres = [
    AccessFilterMiddleware::class,
];

// Or class name with constructor arguments
$accessFiltres = [
    [AccessFilterMiddleware::class, $paramMiddleware],
];

// Or closure function
$accessFiltres = [
    function (Request $request, Responce $response, Closure $next) {
        return $next($request, $response);
    }
];

// Start the process
Pipe::create($request, $response)
    ->through($accessFiltres)
    ->through($XSSFiltres)
    ->then(function (Request $request, Responce $response) {
        $response->runController($request);
    });

Example middleware:

<?php
class AccessFilterMiddleware {
    protected $paramMiddleware;

    public function __construct($paramMiddleware = null) {
        $this->paramMiddleware = $paramMiddleware;
    }

    public function __invoke(Request $request, Responce $response, Closure $next) {
        if ($request->isPost()) {
            $response->addError('Post is forbidden');
            return false;
        }
        return $next($request, $response);
    }
}

The implementation of the pipeline here https://github.com/petrgrishin/pipe/blob/master/src/Pipe.php