In the last years TYPO3 has implemented more and more standards from the rich PHP world to prevent reinventing solutions that have been solved elsewhere in an already sufficient way. After all TYPO3 is a Content Management System and should focus on that. It makes a lot of sense to look at the PHP Standards Recommendations made by the PHP Framework Interop Group. While switching to the CGL of PSR-2 as well as adapting the autoloading standards already feels like an eternity ago, there are still constantly adaptions and improvements in many different areas.

With TYPO3 7 the compatibility with PSR-7 request and response objects was (at least for the backend) implemented (Patch). This is now the foundation of the next important steps towards compliance with the PSRs. During the development of TYPO3 9 LTS support for PSR-15 RequestHandlers and Middlewares was implemented (Patch). Afterwards more and more functionality was refactored to middlewares.

In this blog post we will first look at the concept of middleware components and then look at the implementation in TYPO3. We will also learn how we can use middlewares in our own extensions.

The concept of middlewares

The concept of middlewares in a web application is not new. Generally speaking it is the separation of different tasks that might be necessary during the processing of a request. The middleware tasks or components are executed in a specific order and may return a response object, throw an exception or simply pass the request to the next middleware. In the words of the PSR-15 meta document:

  • Producing a response on its own. If specific request conditions are met, the
    middleware can produce and return a response.

  • Returning the result of the request handler. In cases where the middleware
    cannot produce its own response, it can delegate to the request handler to
    produce one [...]

  • Manipulating and returning the response produced by the request handler. In
    some cases, the middleware may be interested in manipulating the response
    the request handler returns [...]. In such cases, the middleware will capture the response
    returned by the request handler, and return a transformed response on
    completion.

To concentrate on the main aspect of the application certain checks whether a request is qualified to be processed by the application in the first place can be encapsulated in middleware. This can be used for authentication, cookie handling, validation, robot detection, etc. It can also be used for tasks that are bloating your application bootstrap. Using dedicated middleware for certain tasks can improve the structure and maintainability of a project a lot.

Top

The PSR-15 Interfaces

As usual the PSR provides an interface that compliant code has to implement. In case of PSR-15 two interfaces are involved (see the PSR-15 Interface Description):

1. The Psr\Http\Server\RequestHandlerInterface

namespace Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
 * Handles a server request and produces a response
 *
 * An HTTP request handler process an HTTP request in order to produce an
 * HTTP response.
 */
interface RequestHandlerInterface
{
    /**
     * Handles a request and produces a response
     *
     * May call other collaborating code to generate the response.
     */
    public function handle(ServerRequestInterface $request): ResponseInterface;
}

2. The Psr\Http\Server\MiddlewareInterface

namespace Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
 * Participant in processing a server request and response
 *
 * An HTTP middleware component participates in processing an HTTP message:
 * by acting on the request, generating the response, or forwarding the
 * request to a subsequent middleware and possibly acting on its response.
 */
interface MiddlewareInterface
{
    /**
     * Process an incoming server request
     *
     * Processes an incoming server request in order to produce a response.
     * If unable to produce the response itself, it may delegate to the provided
     * request handler to do so.
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
}

We see that a middleware implementation expects a instance of the RequestHandlerInterface for processing a request. This is because the underlying idea for the middleware is to call the RequestHandler for the response object (which is not passed to the process method) if the middleware itself does not create a response on its own. In which case the RequestHandler might not be used at all.

This approach is called "Single Pass" in opposition to the "Double Pass" approach where the request and the response objects are both passed as arguments to each middleware. Because in the PSR-15 interface only the request object is passed to the middleware it is called the "Single Pass" approach. During the development of PSR-15 a lot of discussion was about what approach is superior and why. Parts of that discussion can be watched in the following very interesting YouTube video from 2016 when PSR-15 was still a draft, where the people involved have a in-depth exchange on the topic.

In the end a middleware component can either create a response on its own or delegate the response generation to the RequestHandler, which could be the application itself or just the next middleware in line. This means that one way of achieving a chain of middlewares is to wrap every middleware in a class implementing the RequestHandlerInterface, as it is done in the TYPO3 Core. So let's have a look at that next.

Top

PSR-15 in the TYPO3 Core

Thanks to the TYPO3 PSR-15 Initiative the first patch in this area hit the core at February 6th and was part of the TYPO3 v9.2  release. The PSR-15 support was already mentioned in the release notes.

We have to stop for a moment and thank two people in particular: Benni Mack (@bennimack on Twitter) and Benjamin Franzke (@bnfrk on Twitter) did a lot of the heavy lifting in this area and I want to thank both of them for their efforts and achievements. Feel free to drop them a "thank you" for their work as well and please tell me if I forgot a person that is also due for special credit!

Now that this is out of the way, let's dive into it!

Top

Implementation in the TYPO3 Bootstrap

Very early during the TYPO3 bootstrap an Application class is instantiated matching the current request (either backend, frontend, install or command). These Applications have been enriched with two new classes: The MiddlewareDispatcher and the MiddlewareStackResolver. Let's summarize briefly what these classes do:

Since it makes a difference for TYPO3 if a request is asking for something in the context of the frontend or the backend two seperate "stacks" of middlewares were introduced: frontend and backend. The MiddlewareStackResolver finds the correct stack for the current request. A stack is an array of middleware FQCNs (or instantiated classes) collected from all extensions and ordered according to the configuration (more on that in a minute).

The stack is passed alongside the current context-aware RequestHandler to the MiddlewareDipatcher, a class that wraps each Middleware in a class implementing the PSR-15 RequestHandlerInterface and then asks the first Middleware for a response object with the next middleware as a fallback. The fallback itself has the next middleware as its fallback and so on until at the very end of that chain the initial RequestHandler functions as the last fallback class to be asked for a response object.

Sounds pretty complicated but if you take some time reading the actual code (and especially the unit tests) the implemented approach becomes a lot clearer.

And after all it is more important to understand how to use the middlewares to your benefit than to fully understand the implementation details in the core.

Top

Registration of Middlewares

For registering a middleware an extension needs to provide a specific file:

my_ext/Configuration/RequestMiddlewares.php

This file needs to return an array with the middleware configuration. Let's look at an example:

return [
    'frontend' => [
        'myext-awesome-identifier' => [
            'target' => \Vendor\MyExt\Middleware\AwesomeMiddleware::class,
            'before' => [
                'another-middleware-identifier',
            ],
            'after' => [
                'yet-another-middleware-identifier',
            ],
        ]
    ]
];

The 'frontend' in line 2 is the name of the stack the middleware should be respected in. As mentioned above this is at time of writing either frontend or backend. Next is an identifier for the middleware, followed by the FQCN as the 'target'. If it is important that the middleware is executed before or after another middleware this can be specified as well.

To help keeping track of all the middlewares the configuration module in the backend shows a list of all registered middlewares in the already resolved order:

Last but not least we also can deactivate existing middlewares (patch). This means we have full control over the middleware stack in our projects. If a middleware is not needed, we can just deactivate it which is a performance gain. If we want to do things differently, we can deactivate the middleware shipped by the core and register our own implementation. Deactivation also happens in RequestMiddlewares.php:

return [
  'backend' => [
    'middleware-identifier' => [
      'disabled' => true
    ]
  ]
];

Next, we look at an additional feature implemented in this area.

Top

Enriched Request Object

Another thing worth mentioning in this context is the addition of \TYPO3\CMS\Core\Http\NormalizedParams (patch, feature RST). This class functions as an API for accessing server variables that may have been modified by TYPO3. It is an replacement of the GeneralUtility::getIndpEnv() API and provides access to all the parameters like TYPO3_PORTTYPO3_SITE_URL, TYPO3_SSL and many more. It is added to the request object early on through ... of course ... a middleware.

The usage in middlewares (and everywhere else where the PSR-7 request object is available) works like this:

/** @var NormalizedParams $normalizedParams */
$normalizedParams = $request->getAttribute('normalizedParams');
$typo3SiteUrl = $normalizedParams->getSiteUrl(); // Same as GeneralUtility::getIndpEnv('TYPO3_SITE_URL')

The mapping from the old GeneralUtility::getIndpEnv() to the new object looks like this:

  • SCRIPT_NAME is now ->getScriptName()
  • SCRIPT_FILENAME is now ->getScriptFilename()
  • REQUEST_URI is now ->getRequestUri()
  • TYPO3_REV_PROXY is now ->isBehindReverseProxy()
  • REMOTE_ADDR is now ->getRemoteAddress()
  • HTTP_HOST is now ->getHttpHost()
  • TYPO3_DOCUMENT_ROOT is now ->getDocumentRoot()
  • TYPO3_HOST_ONLY is now ->getRequestHostOnly()
  • TYPO3_PORT is now ->getRequestPort()
  • TYPO3_REQUEST_HOST is now ->getRequestHost()
  • TYPO3_REQUEST_URL is now ->getRequestUrl()
  • TYPO3_REQUEST_SCRIPT is now ->getRequestScript()
  • TYPO3_REQUEST_DIR is now ->getRequestDir()
  • TYPO3_SITE_URL is now ->getSiteUrl()
  • TYPO3_SITE_PATH is now ->getSitePath()
  • TYPO3_SITE_SCRIPT is now ->getSiteScript()
  • TYPO3_SSL is now ->isHttps()

Next, we look at a few examples to see how middlewares are actually used in the core.

Top

Examples

Since the introduction of the middleware support many tasks have been refactored to middlewares and we pick a few simple examples to illustrate the concept once more. Let's start with the above mentioned manipulation of the request object. Of course this should happen as early as possible (so that other middlewares can use it) as well as in both middleware stacks and this is exactly how the middleware is registered.

Let's take a look on the implementation:

/**
 * Adds an instance of TYPO3\CMS\Core\Http\NormalizedParams as
 * attribute to $request object
 *
 * @param ServerRequestInterface $request
 * @param RequestHandlerInterface $handler
 * @return ResponseInterface
 */
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
    $request = $request->withAttribute(
        'normalizedParams',
        new NormalizedParams(
            $request,
            $GLOBALS['TYPO3_CONF_VARS'],
            Environment::getCurrentScript(),
            Environment::getPublicPath()
        )
    );
    return $handler->handle($request);
}

This looks nice and clean. And we will actually see this in use in the following examples multiple times.

Since the introduction of the middleware support many tasks have been refactored to middlewares. Let's pick a few simple examples to illustrate the concept once more. With this patch a bunch of backend related tasks were refactored to middlewares. The LockBackendGuard and the ForcedHttpsBackendRedirector are good examples in my opinion.

Let's look at the registration in sysext backend first:

return [
    'backend' => [
        'typo3/cms-backend/locked-backend' => [
            'target' => \TYPO3\CMS\Backend\Middleware\LockedBackendGuard::class,
        ],
        'typo3/cms-backend/https-redirector' => [
            'target' => \TYPO3\CMS\Backend\Middleware\ForcedHttpsBackendRedirector::class,
            'after' => [
                'typo3/cms-backend/locked-backend'
            ]
        ]
    ]
];

This determines that the LockedBackendGuard middleware has to be executed first. This makes perfect sense, because if the backend is locked or the IP of the request is not whitelisted no further processing of the request needs to be done. So we expect the middleware in case of a locked backend or an IP mismatch to return some kind of response itself or to throw an exception instead of asking the next middleware in line.

To keep things simple I've shortened the following code snippet. Please have a look at the actual class for the complete implementation:

/**
 * Checks the client's IP address and if typo3conf/LOCK_BACKEND is available
 *
 * @param ServerRequestInterface $request
 * @param RequestHandlerInterface $handler
 * @return ResponseInterface
 */
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
    $redirectToUri = $this->checkLockedBackend();
    if (!empty($redirectToUri)) {
        return new RedirectResponse($redirectToUri, 302);
    }
    $this->validateVisitorsIpAgainstIpMaskList(
        $request->getAttribute('normalizedParams')->getRemoteAddress(),
        trim((string)$GLOBALS['TYPO3_CONF_VARS']['BE']['IPmaskList'])
    );

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

We see what we expected. The Middleware returns a RedirectResponse if the backend is locked and a redirect URL could be found. Additionally checkLockedBackend() and validateVisitorsIpAgainstIpMaskList() can throw exceptions that are not being caught by the middleware itself.

If everything is fine, the output of the next middleware is returned with return $handler->handle($request);.

Next in line is (according to the configuration) the ForcedHttpsBackendRedirector. So let's look at it:

/**
 * @param ServerRequestInterface $request
 * @param RequestHandlerInterface $handler
 * @return ResponseInterface
 */
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
    if ((bool)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL'] && !$request->getAttribute('normalizedParams')->isHttps()) {
        if ((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSLPort']) {
            $sslPortSuffix = ':' . (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSLPort'];
        } else {
            $sslPortSuffix = '';
        }
        list(, $url) = explode('://', $request->getAttribute('normalizedParams')->getSiteUrl() . TYPO3_mainDir, 2);
        list($server, $address) = explode('/', $url, 2);
        return new RedirectResponse('https://' . $server . $sslPortSuffix . '/' . $address);
    }

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

We see the same approach: If the middlewares conditions are met the middleware returns a RedirectResponse to the HTTPS version of the current URL. Otherwise it passes to the next middleware in line. This continues through all middlewares until at the end of the line (in case of a backend request) the \TYPO3\CMS\Backend\Http\RequestHandler is asked for the response.

Note that a middleware does not have to create an own response even if their conditions are met. E.g. the preprocessing hooks for frontend requests are executed through a middleware in the frontend stack (see \TYPO3\CMS\Frontend\Middleware\PreprocessRequestHook). If we look at that implementation we see that this middleware never returns its own response but always delegates to the next in line after doing it's thing:

/**
 * Hook to preprocess the current request
 *
 * @param ServerRequestInterface $request
 * @param RequestHandlerInterface $handler
 * @return ResponseInterface
 */
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
    foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['preprocessRequest'] ?? [] as $hookFunction) {
        $hookParameters = [];
        GeneralUtility::callUserFunction($hookFunction, $hookParameters, $hookParameters);
    }
    return $handler->handle($request);
}

Another way to use middlewares is to enrich the response object. In such a case the middleware asks for the response first and adds to it if their conditions are met. As an example we can look at the \TYPO3\CMS\Frontend\Middleware\ContentLengthResponseHeader:

/**
 * Adds the content length
 *
 * @param ServerRequestInterface $request
 * @param RequestHandlerInterface $handler
 * @return ResponseInterface
 */
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
    $response = $handler->handle($request);
    if (
        !($response instanceof NullResponse)
        && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController
        && $GLOBALS['TSFE']->isOutputting()) {
        if (
                (!isset($GLOBALS['TSFE']->config['config']['enableContentLengthHeader']) || $GLOBALS['TSFE']->config['config']['enableContentLengthHeader'])
                && !$GLOBALS['TSFE']->isBackendUserLoggedIn() && !$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']
                && !$GLOBALS['TSFE']->config['config']['debug'] && !$GLOBALS['TSFE']->doWorkspacePreview()
            ) {
            $response = $response->withHeader('Content-Length', (string)$response->getBody()->getSize());
        }
    }
    return $response;
}

We see that after checking some conditions the content-length header is added to the response before it is returned.

But enough with the examples. I hope this post helped a little bit with the understanding of middlewares and could spread the knowledge that TYPO3 is PSR-15 ready. Please contact me if I made any mistakes in the post and keep in mind that this was written during extremly high temperatures :)

Top

Further Information

  • Feature RST for PSR-15 support in TYPO3
  • Feature RST for enriching the PSR-7 Request object with normalized server parameters

As usual, I thank you for reading and would appreciate your support over at Patreon. A big thank you to everybody who already supports the blog. Read their names here.