You can now become a hero and support this Blog at GitHub Sponsors or Patreon.

More about support

TYPO3 ships with an application framework that we all know and love: extbase. I published some articles recently regarding good practices when approaching applications with extbase (read about DTOs and bounded contexts) and realized that quite some things have changed and improved during the development of TYPO3 v10 and v11. Shout-out to Alexander Schnitzler (@Twitter) and Christian Kuhn (@Twitter) who pushed the development forward and did the majority of the work. Thanks guys!

This article is meant to provide an overview of things that have changed and might affect existing applications or provide new possibilities and flexibility for future development. There are links to all the patches and documentation files to dive deeper into any topic, that being said after reading this article we should be in good shape to get our applications running with TYPO3 11.

Let's dive into it.

ObjectManager must no longer be used

Since TYPO3 11

After lots of patches and refactoring, TYPO3\CMS\Extbase\Object\ObjectManager will finally retire with TYPO3 12.

It served us well by providing injected dependencies, resolving interfaces and instantiating more objects than it probably should have. Calling ObjectManager->get() is discouraged for ages and has been marked as deprecated since TYPO3 10 (Patch, Deprecation RST). Then, all usages of ObjectManager in the core itself had to be removed (start here for an overview over some of the work that was necessary) which took over a year to complete. So finally, with TYPO3 11 the whole dependency injection layer of extbase is deprecated (Patch, Deprecation RST) and will be removed in TYPO3 12.0.

What does this mean for our extension?

Well, hopefully nothing. We already used proper dependency injection and class instantiation, right? Right?

To be save, here is the very quick rundown of how to do things without the ObjectManager. For class dependencies we should use dependency injection (read my article to learn how and to find more resources on the topic), preferably constructor injection:

protected NecessaryService $service;

public function __construct(NecessaryService $service)
{
    $this->service = $service;
}

For class instantiation we should use GeneralUtility::makeInstance() to support DI on the instantiated class as well as XCLASSing. For models and other final classes without dependencies simply using new is also fine. Some simple examples:

/**
 * @return Thing[]
 */
public static function getThings(): array 
{
    $finder = GeneralUtility::makeInstance(Finder::Class);
    return $finder->findThings();
}

public function newAction(Product $product = null): ResponseInterface
{
    $this->view->assign('product', $product ?? new Product());
    return $this->htmlResponse();
}

For a future proof (extbase) extension all usages of the ObjectManager should be removed.

Top

Controller actions must return a PSR-7 Response

Since TYPO3 11

With TYPO3 11.0, extbase actions have to return an implementation of the PSR-7 Psr\Http\Message\ResponseInterface (Patch, RST) instead of nothing or null or a string or an object that could be cast to a string ...

The goal here was once again to further embrace the PSR standards, reduce framework "magic" being done behind the curtain and enforce more specific controller code. It also enables developers to have full control over the response returned from their controller actions.

If you are interested in some of the preparation work for this change, you can have a look at the patch (RST) that removed usage of extbases self baked response in favor of PSR-7 responses.

To offer a dedicated API for creating response objects, the core introduced a new dependency to the ActionController: the \Psr\Http\Message\ResponseFactoryInterface as specified in PSR-17 (Patch). With the ResponseFactory we can create response objects and return those from our actions. The response body can be created by using the StreamFactory (the StreamFactoryInterface is also specified in PSR-17, Patch). An example could look like this:

public function staticAction(): ResponseInterface
{
    return $this->responseFactory->createResponse()
        ->withAddedHeader('Content-Type', 'text/html; charset=utf-8')
        ->withBody($this->streamFactory->createStream('Static Content'));
}

Since most actions want to render the fluid view as their response body, extbase provides helper methods for doing just that: htmlResponse() for HTML views and jsonResponse() for JSON Views. Here is how those methods look:

protected function htmlResponse(string $html = null): ResponseInterface
{
    return $this->responseFactory->createResponse()
        ->withHeader('Content-Type', 'text/html; charset=utf-8')
        ->withBody($this->streamFactory->createStream($html ?? $this->view->render()));
}

protected function jsonResponse(string $json = null): ResponseInterface
{
    return $this->responseFactory->createResponse()
        ->withHeader('Content-Type', 'application/json; charset=utf-8')
        ->withBody($this->streamFactory->createStream($json ?? $this->view->render()));
}

This means that extbase actions can be made compatible quiet easily most of the time by simply using the helper methods. A short example:

public function commonAction(): ResponseInterface
{
    $this->view->assign('stuff', $this->figgureOutStuff());
    return $this->htmlRespone();
}

With this, it should be fairly easy to migrate existing code to the new return type. Plus we gain the flexibility of returning any PSR-7 compatible response object.

Woop woop.

Top

Forwarding to another action must be done with the ForwardResponse

Since TYPO3 11

Building on the new PSR-7 response handling discussed above, the method forward() of the ActionController has been marked as deprecated (Patch, Deprecation RST). Instead, we can now use the newly introduced TYPO3\CMS\Extbase\Http\ForwardResponse object (Feature RST). Let's see how that looks in (an) action:

public function forwardAction(): ResponseInterface
{
    return new ForwardResponse('list');
}

This (questionable) action will forward to the action list in the same context (controller, plugin, etc). Of course all those could be set as well:

public function advancedForwardAction(): ResponseInterface
{
    return (new ForwardResponse('list'))
        ->withControllerName('Record')
        ->withExtensionName('Extension')
        ->withArguments(['forwarded' => true]);
}

And that is that.

Happy forwarding.

Top

StopActionException must no longer be thrown

Since TYPO3 11

In order to achieve the goal that every extbase action always returns a PSR-7 response object the methods redirect() and redirectToUri() both have to be tackled. To prevent a breaking change late in the TYPO3 v11 development, the methods have only been prepared to return a response object and its current behavior which is throwing the StopActionException has been marked as deprecated (Patch, Deprecation RST). In fact the whole StopActionException is deprecated, and we should stop throwing it in our code.

Both methods will return a RedirectResponse in TYPO3 v12 but calls in our controllers could already be prepared by adding a return statement.

public function redirectAction(): ?ResponseInterface
{
   return $this->redirect('otherAction');
}

Keep in mind, that since redirect() does not actually return a ResponseInterface in TYPO3 v11, this code might made code sniffer and analysis tools unhappy. I'd say it is a matter of taste whether to prepare for the upcoming changes like this or not.

One minor thing to add regarding the deprecation of the StopActionException would be that throwStatus() now throws a PropagateResponseException (Patch).

Let's move on to more exiting things.

Top

Extbase Request implements the PSR-7 ServerRequestInterface

Since TYPO3 11

Since the very beginning, extbase provided its own class to represent the incoming request: \TYPO3\CMS\Extbase\Mvc\Request. With TYPO3 moving more and more towards PSR standards the core used PSR-7 request objects everywhere except in extbase. With TYPO3 11 this has finally changed (Patch, Feature RST). While we still have extbase specific stuff going on in the request object, it now also implements the PSR-7 ServerRequestInterface and serves as a decorator for the original PSR-7 request.

This means, to access the PSR-7 request in an extbase controller we no longer have to rely on $GLOBALS['TYPO3_REQUEST']. The extbase related information is attached to the request as an attribute:

public function someAction(): ResponseInterface
{
    /** TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters $extbaseStuff */
    $extbaseStuff = $this->request->getAttribute('extbase');
    // [...]
    return $this->htmlResponse();
}

The attribute 'extbase' of the PSR-7 request contains an instance of \TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters. This class can be used to get extbase specific information of the current request, like the plugin name, arguments, controller name and much more.

The request object also offers a type hinted method to get the ExtbaseRequestParameters via $this->request->getExtbaseAttribute().

Yay.

Top

Generic extbase models for user related tables must no longer be used

Since TYPO3 11

The TYPO3 core used to ship generic extbase models for

  • Backend users and groups
  • Frontend users and groups

Those have been deprecated with this patch (Deprecation RST) alongside the corresponding repository classes.

The reasoning behind this is that each domain and therefore the model classes solve specific problems with specific requirements. A generic model that simply maps every field of the table to properties and does so also with relations to other tables, is likely to contain overhead for specific domains. It tends to introduce and contributes to the bad practice to have domain models have "everything". In addition to the user related classes, the CategoryRepository has been deprecated, because it was making assumptions regarding the storage pid handling. There is no way such an assumption is correct for every use case.

Of course, it is still a legit use case to provide domain model representations for users and user groups, however the way to go is to compose them according to the requirements of the domain. This is what we should have always done and are now forced to do: create model classes and repositories on our own.

We also have to configure the mapping instructions for extbase when it comes to those models as the naming of the database tables does not follow the extbase convention. So let's look at this next

Top

Table to model mapping must be done in a PHP file

Since TYPO3 10

We used to have to map database tables to model classes and database fields to model properties via typoscript with

config.persistence.classes

Since TYPO3 10 this no longer works (Patch, Breaking RST). To speed up parsing and consolidate extbase configuration, the mapping moved to the PHP file EXT:mx_ext/Configuration/Extbase/Persistence/Classes.php. The structure of the mapping instructions also changed a bit, so let's just look at an example:

<?php
declare(strict_types = 1);

return [
    \Vendor\Extension\Domain\Model\FrontendUser::class => [
        'tableName' => 'fe_users',
        'properties' => [
            'name' => [
                'fieldName' => 'username'
            ],
            'mail' => [
                'fieldName' => 'email'
            ]
        ],
    ],
];

With this the mapping will work again.

Top

FQCN's must be used when registering plugins or modules

Since TYPO3 10

Another change introduced in TYPO3 10 LTS (Patch, Deprecation RST) is the requirement for fully qualified class names when registering plugins. This change allowed to get rid of extbase guessing the class name by looking at the vendor provided with the plugin registration and a conventional place for controllers. Therefore, it is no longer needed to provide the vendor name and controllers could theoretically reside wherever we want. That being said, it is still recommended to follow the convention whenever possible, it just is no longer a technical necessity.

While having a vendor name in the plugin registration and using the short controller alias name was deprecated with TYPO3 10, compatibility with the old registration structure was removed with TYPO3 11 (Breaking RST).

Let's quickly compare the old and the new way of registering plugins.

// Old way
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
    'Vendor.extension',
    'superPlugin',
    ['Exquisite' => 'shine,spark'],
    ['Exquisite' => 'spark']
);

// New way
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
    'extension',
    'superPlugin',
    [\Vendor\Extension\Controller\ExquisiteController::class => 'shine,spark'],
    [\Vendor\Extension\Controller\ExquisiteController::class => 'spark']
);

The same changes apply for registering backend modules via \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule().

Top

These are all the notable changes to extbase in TYPO3 v10 and v11. I hope this overview was of some use to you.

As always, thanks for reading and big thank you to all supportes and sponsors of this blog!