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 a middleware has the following roles:
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 aspects of any web application, certain checks whether a request is qualified to be processed by the application in the first place can be encapsulated in middlewares. This includes authentication, cookie handling, validation, robot detection, etc. It can also be used for tasks that are bloating the application's bootstrap. Using dedicated middlewares for certain tasks can improve the structure and maintainability of a project a lot.
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 an instance of the RequestHandlerInterface
for processing a request. This is because the underlying idea for the middleware is to ask the $handler
for the response object if the middleware itself does not create a response itself. This means: if the middleware does create and return a response the $handler
might not be used at all. The $handler
is most likely the next middleware in line or (at the very end of the line) the application itself.
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 there was much discussion about what approach would be superior and what would be the reasoning behind it. Parts of that discussion can be retraced by watching the following YouTube video from 2016. At this point in time PSR-15 was still in a draft stage, but the people involved were already in an in-depth exchange about 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.
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!
Now that this is out of the way, let's dive into it!
Implementation in the TYPO3 Bootstrap
Very early during the TYPO3 bootstrap an Application
class is instantiated matching the current request being either a backend or a frontend request (technically there are install and command line applications as well but we will focus on frontend and backend for the rest of the article). 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 a little bit complicated but if you are interested and take some time reading the actual code (and especially the unit tests) the implemented approach becomes a lot clearer.
That being said, it is more important to understand how to use the middlewares to your benefit than to fully understand the implementation details in the core.
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 (part of the system extension lowlevel
) 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.
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_PORT
, TYPO3_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.
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. In case of the NormalizedParams
this should happen as early as possible (so that the middlewares further down the line can take advantage of the enhaced request). The middleware is therefore registered very early as well as in both middleware stacks. Have a look in ext: backend for the backend stack registration and in ext:frontend for the frontend stack registration.
Let's take a quick 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.
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 :)
Further Information
- Feature RST for PSR-15 support in TYPO3
- Feature RST for enriching the PSR-7 Request object with normalized server parameters
- Collection of PSR-15 Middlewares on GitHub
- Official TYPO3 Documentation
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.