yiisoft / yii2

Yii 2: The Fast, Secure and Professional PHP Framework
http://www.yiiframework.com
BSD 3-Clause "New" or "Revised" License
14.23k stars 6.91k forks source link

EVENT_AFTER_REQUEST triggered AFTER sending response in Pjax requests #17355

Closed jhon100 closed 5 years ago

jhon100 commented 5 years ago

The docs for yii\web\Application says that:

EVENT_AFTER_REQUEST is: 'An event raised after the application successfully handles a request (before the response is sent out)'

However in a pjax request, the event is being triggered AFTER the response is sent out. You can test it with this code:

        \yii\base\Event::on('*', '*', function ($event) {
            \Yii::debug('trigger event: ' . $event->name);
        });

Do a request, you will see in debug:

ajax or normal request trigger event: afterRequest trigger event: beforeSend trigger event: afterPrepare trigger event: afterSend

pjax request trigger event: beforeSend trigger event: afterPrepare trigger event: afterSend trigger event: afterRequest

So, you'll fail to change something in response (like cookies) if you are listening to this event and the request is Pjax.

Executing step by step I can see this:

ajax or normal request ...yii\web\Application run method (all code inside run method is executed)

pjax request ...yii\web\Application run method

            $this->state = self::STATE_BEFORE_REQUEST;
            $this->trigger(self::EVENT_BEFORE_REQUEST);

            $this->state = self::STATE_HANDLING_REQUEST;
            $response = $this->handleRequest($this->getRequest()); 
    //>>>>>  the code below is not executed
            $this->state = self::STATE_AFTER_REQUEST;
            $this->trigger(self::EVENT_AFTER_REQUEST);

            $this->state = self::STATE_SENDING_RESPONSE;
            $response->send();

            $this->state = self::STATE_END;

            return $response->exitStatus;

For Pjax, the following calls are made after '$response = $this->handleRequest($this->getRequest());'

for brevity some calls are omitted go to yii\base\View phprenderPhpFile() _ calls require $file; go to view file calls Pjax::end() go to yii\base\Widget\ end() calls $result = $widget->run(); go to yii\widgets\Pjax run() _ calls $response->send(); // no EVENT_AFTERREQUEST triggered calls Yii::$app->end(); go to yii\base\Application end() _ calls $this->trigger(self::EVENT_AFTERREQUEST); calls $response->send(); // again go to \yii2\web\Response.php send()

        if ($this->isSent) {//true
            return;
        }

go to \yii\base\Application.php _ calls exit($status);

Suggestion Maybe add Yii::$app->trigger(Yii::$app::EVENT_AFTER_REQUEST);in yii\ widgets\Pjax run () before $response->send (); be enough?. In this case it would be interesting some logic to prevent the event from being triggered twice ...

Additional info

Q A
Yii version 2.0.20
PHP version 7.1.17
Operating system Windows 10
samdark commented 5 years ago

Indeed, order of events should be the same.

samdark commented 5 years ago

Do you want to try fixing it?

jhon100 commented 5 years ago

Well, as I mentioned above, when the request is normal or ajax, the event is fired from the run() function of yii\web\Application.

On the other hand, if the request is Pjax, the event is triggered from the end() function of yii yii\base\Application, but only under this conditions:

   public function end($status = 0, $response = null)
    {
        if ($this->state === self::STATE_BEFORE_REQUEST || $this->state === self::STATE_HANDLING_REQUEST) {
            $this->state = self::STATE_AFTER_REQUEST;
            $this->trigger(self::EVENT_AFTER_REQUEST);
        }

So in the same way as in the run() function of yii\web\Application, before firing the event I set the variable 'state'. (This ensures that the event will not be fired twice)

yii\widgets\Pjax.php

    public function run()
    {
    [...]
        Yii::$app->state = Yii::$app::STATE_AFTER_REQUEST;
        Yii::$app->trigger(Yii::$app::EVENT_AFTER_REQUEST);
    [...]
        $response->send();

        Yii::$app->end();
    }

Now the order of events is the same regardless of the type of request, but I'm not familiar with the core code, so I don't know if this change can affect anything else or if some other widget has the same problem. I did a search for $response->send() and I found the codes below that, apparently, also send the response without first triggering EVENT_AFTER_REQUEST.

\yii\filters\HostControl > function denyAccess($action) \yii\web\ErrorHandler > function renderException($exception)

So I don't know if the problem is unique to Pjax or also in other parts of the framework that call response ->send() without first firing EVENT_AFTER_REQUEST

kamarton commented 5 years ago

Verified in 2.0.24: this bug exists.

Events run as described below:

ajax or normal request trigger event: afterRequest trigger event: beforeSend trigger event: afterPrepare trigger event: afterSend

pjax request trigger event: beforeSend trigger event: afterPrepare trigger event: afterSend trigger event: afterRequest

kamarton commented 5 years ago

This causes the problem: https://github.com/yiisoft/yii2/blob/90d46298addda275b5ad5afb4e399b768083b5e6/framework/widgets/Pjax.php#L164-L173

I find this solution a bit weird :)

kamarton commented 5 years ago

I'm working on this bug.

kamarton commented 5 years ago

@jhon100 plase test this fix: https://github.com/yiisoft/yii2/pull/17526