Benjamin Benjamin 07.11.2016

Slow Ajax requests in your Symfony application? Apply this simple fix

If you are using Sessions in PHP (the odds are very high) then you should know that write and read access to them is using pessimistic locking which means no two requests can run in parallel with the same session open.

Our blog post on Essential PHP Performance Optimizations mentioned blocking Sessions as one very common problem for performance in PHP applications. Multiple concurrent Ajax requests will wait for each other and make your application much slower than it must be.

The fix for this in pure PHP is to call session_write_close(), but in your Symfony 2 or 3 application this function is hidden below abstraction layers or might even be implemented differently.

First, because this problem is mainly present for Ajax requests we start with a generic solution that hooks into the Symfony Kernel Events to perform its job.

  1. Wait until all kernel.request events are processed, specifically those loading the session and the user object from it.
  2. Execute a listener as one of the last in kernel.request that checks if the Request is an Ajax request and if yes, calling to close the session.

EventListener that auto-closes Session on Ajax Requests

Here is a generic event listener that performs these steps:

<?php

namespace Acme\AppBundle\EventListener;

class AjaxSessionCloseListener
{
    public function onKernelRequest(GetResponseEvent $event)
    {
        $request = $event->getRequest();

        if (!$request->isXmlHttpRequest()) {
            return;
        }

        if (!$request->attributes->has('_route')) {
            return;
        }

        $session = $request->getSession();
        $session->save();
    }
}

This listener must run after Symfony's internal SessionListener and RouterListener so that the session object and current route name are available. The following XML service tag definition therefore includes a priority for the kernel.event_listener:

<service id="app.ajax_session_close_listener"
    class="Acme\AppBundle\EventListener\AjaxSessionCloseListener">
    <tag name="kernel.event_listener" event="kernel.request" priority="-255" />
</service>

Or if you prefer YAML:

services:
    app.ajax_session_close_listener:
        class: 'Acme\AppBundle\EventListener\AjaxSessionCloseListener'
        tags:
            - { name: "kernel.event_listener", event: "kernel.request", priority: -255 }

And you are good to go!

Before you roll this out: Are you sure all your ajax requests never write to the session? CSRF protection in Symfony Forms for example writes to the Session. If you have one or two controllers that are called with Ajax and do write to the sesison, then you should implement a blacklisting mechanism based on route names or you could introduce special _ajax_write attribute in route defaults and check for it in the Listener.