TYPO3

Life without $GLOBALS[‘TSFE’] in TYPO3 v13 and v14

5 min read

Goodbye TSFE

For almost twenty years, every TYPO3 developer had one friend who was always there: $GLOBALS['TSFE']. Page id, language, TypoScript, the current content object, header data, cache settings. Everything was in that one object, reachable from anywhere.

That friend is gone. TYPO3 13.4 deprecated the TypoScriptFrontendController and $GLOBALS['TSFE'] (Deprecation #105230), and TYPO3 14.0 removed the global and all ways to get the instance (Breaking #107831). In the 14.3.9 installation I work with, the class file is not even in cms-frontend any more. Old extension code that touches it now ends with a fatal error.

The good news: almost everything you needed is now a PSR-7 request attribute. This post shows the replacements I use most, in an Extbase controller, a middleware and a ViewHelper.

The mapping table

These come from the changelog entry Breaking #102621, which lists every TSFE member and its replacement. The ones I meet in nearly every upgrade:

  • $GLOBALS['TSFE']->id → $request->getAttribute('frontend.page.information')->getId()
  • $GLOBALS['TSFE']->page → ...->getPageRecord() on the same object
  • $GLOBALS['TSFE']->rootLine → ...->getRootLine()
  • $GLOBALS['TSFE']->getLanguage() → $request->getAttribute('language')
  • $GLOBALS['TSFE']->getSite() → $request->getAttribute('site')
  • $GLOBALS['TSFE']->getPageArguments() → $request->getAttribute('routing')
  • $GLOBALS['TSFE']->config['config'] → $request->getAttribute('frontend.typoscript')->getConfigArray()
  • $GLOBALS['TSFE']->tmpl->setup → $request->getAttribute('frontend.typoscript')->getSetupArray()
  • set_no_cache() → $request->getAttribute('frontend.cache.instruction')->disableCache('reason')
  • additionalHeaderData[] → PageRenderer::addHeaderData()

The pattern is always the same: find where you can get the request, then ask it.

In an Extbase controller

Before, in a typical old controller:

public function listAction(): ResponseInterface
{
    $pageId = (int)$GLOBALS['TSFE']->id;
    $languageId = $GLOBALS['TSFE']->getLanguage()->getLanguageId();
    $items = $this->itemRepository->findByPageAndLanguage($pageId, $languageId);
    $this->view->assign('items', $items);
    return $this->htmlResponse();
}

After. The Extbase request is a full PSR-7 request, so the attributes are right there:

use TYPO3CMSCoreSiteEntitySiteLanguage;
use TYPO3CMSFrontendPagePageInformation;

public function listAction(): ResponseInterface
{
    /** @var PageInformation $pageInformation */
    $pageInformation = $this->request->getAttribute('frontend.page.information');
    /** @var SiteLanguage $language */
    $language = $this->request->getAttribute('language');

    $items = $this->itemRepository->findByPageAndLanguage(
        $pageInformation->getId(),
        $language->getLanguageId()
    );
    $this->view->assign('items', $items);
    return $this->htmlResponse();
}

This works the same in 13.4 and 14.3, so you can migrate before you upgrade. That is the order I recommend: clean up on v13 with the deprecation log open, then jump.

In a middleware

Middlewares are where people get surprised. An attribute exists only after the middleware that sets it. frontend.page.information is set by the page resolver, and frontend.typoscript by typo3/cms-frontend/prepare-tsfe-rendering. So register your middleware after that one:

<?php
// Configuration/RequestMiddlewares.php
return [
    'frontend' => [
        'myvendor/my-ext/page-header' => [
            'target' => MyVendorMyExtMiddlewarePageHeaderMiddleware::class,
            'after' => [
                'typo3/cms-frontend/prepare-tsfe-rendering',
            ],
        ],
    ],
];
<?php

declare(strict_types=1);

namespace MyVendorMyExtMiddleware;

use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;
use PsrHttpServerMiddlewareInterface;
use PsrHttpServerRequestHandlerInterface;
use TYPO3CMSCoreRoutingPageArguments;
use TYPO3CMSFrontendPagePageInformation;

final class PageHeaderMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $response = $handler->handle($request);

        $pageInformation = $request->getAttribute('frontend.page.information');
        $routing = $request->getAttribute('routing');
        if (!$pageInformation instanceof PageInformation || !$routing instanceof PageArguments) {
            return $response;
        }

        $config = $request->getAttribute('frontend.typoscript')?->getConfigArray() ?? [];
        if ((bool)($config['tx_myext.']['sendPageHeader'] ?? false)) {
            $response = $response
                ->withHeader('X-Page-Id', (string)$pageInformation->getId())
                ->withHeader('X-Page-Type', $routing->getPageType());
        }
        return $response;
    }
}

One trap here: I used getConfigArray() on purpose. The full setup array from getSetupArray() is only built when TYPO3 really renders something. When the page comes completely from the cache, calling it throws a RuntimeException. The config. part is always available after the prepare middleware, so a middleware should read its switches from there.

In a ViewHelper

ViewHelpers have no $this->request. In TYPO3 13 you could still call $this->renderingContext->getRequest(), but that was deprecated in 13.3 (Deprecation #104684) and is gone in 14. The way that works in both versions is the attribute API of the rendering context:

<?php

declare(strict_types=1);

namespace MyVendorMyExtViewHelpers;

use PsrHttpMessageServerRequestInterface;
use TYPO3CMSCoreSiteEntitySiteLanguage;
use TYPO3FluidFluidCoreViewHelperAbstractViewHelper;

/**
 * Usage: {my:languageCode()} renders e.g. "en-US"
 */
final class LanguageCodeViewHelper extends AbstractViewHelper
{
    public function render(): string
    {
        if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
            return '';
        }
        $request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
        $language = $request->getAttribute('language');

        return $language instanceof SiteLanguage
            ? $language->getLocale()->getName()
            : '';
    }
}

Before, this ViewHelper was one line with $GLOBALS['TSFE']->getLanguage(). Now it is a few lines longer, but it also works when the ViewHelper is rendered in a context without a frontend request, for example in a mail template from a CLI command. It just returns an empty string instead of crashing.

What about the content object?

$GLOBALS['TSFE']->cObj is another classic. The changelog advice is to create your own ContentObjectRenderer, call setRequest($request), and start() it with the page record from frontend.page.information. In Extbase controllers you rarely need it at all; most of the time a typolink is better done with the LinkFactory or a ViewHelper in the template.

Testing code that reads request attributes

A nice side effect: code that takes the request as input is easy to test. In a unit test you can build a ServerRequest, add a PageInformation object (its setId() is marked internal for the core, but fine in a test) and a SiteLanguage as attributes, and call your middleware or service directly. With $GLOBALS['TSFE'] you had to fake a global god object first, and most people simply did not write the test.

How I find all the places

  1. On v13, enable the deprecation log and click through the site. TSFE access logs a lot.
  2. Run the Extension Scanner in the upgrade module of the install tool. It knows these patterns.
  3. Search the code: grep -rn "TSFE|getTypoScriptFrontendController|frontend.controller" Classes/. Also check TypoScript conditions: getTSFE() in conditions was removed in v14 too (Breaking #107473).
  4. Run Rector with the TYPO3 rule sets for the simple cases, and review every change by hand.

The removal felt painful at first. But after a few extensions I started to like it: every dependency is now visible in the method signature or the request, and tests became easier because I can pass a request with exactly the attributes I need. Feedbacks are the champion’s breakfast, so tell me in the comments which TSFE call gave you the most trouble.

Further reading

Leave a comment

Your email address will not be published.