Daniel Siepmann - Coding is Art

Blog Post

Prepare legacy code for upcoming TYPO3 versions

Published: , Updated:

Tested with TYPO3: 9 LTS, 8 LTS

Topics: typo3

Introduction

Some of you, like one of our customers, are still on TYPO3 v8 or 9. We all dream of a bright future where we get shiny new features like new event handling by PSR-14, and dependency injection by PSR-11. Still we can write code that actually follows the new features and provides benefits today. Updates will become very smooth thanks to rector which could auto migrate everything necessary.

Read the following sections to get concrete examples on how we write code in 8.7 that will be auto migrated to 10.4 (11.x) via rector and how we profit today.

Using Dependency Injection

The BIG change with 10.4 is dependency injection through the whole code base and usage of Symfony container, and therefore configuration of dependency injection.

Actually older versions already have dependency injection which you can use. And that can be migrated via TYPO3 rector, once you update. Let me explain a bit more, before showing a concrete example, because there are two things to understand, which you also need to understand for 10.4 anyway.

1. TYPO3 has some "entry points" into your Code, such as hooks, user functions. Those entry points always use TYPO3 API like GeneralUtility::makeInstance to create instances of your classes. You don't have dependency injection within any TYPO3 version in such cases. In order to get dependency injection within 10.4, you need to explicitly mark those classes to be public. Find out more at docs.typo3.org.

2. TYPO3 has deprecated usage of Extbase ObjectManager, which provides Dependency Injection. One might think adding usages of that class will complicate updates, but TYPO3 rector will do the job for you. Don't fear that change in case you follow this post.
Once you understood those two points about TYPO3 10.4, you should have all you need to prepare older code base for an update.

Let's have a look at two concrete examples, all stripped down to what matters.

First concrete example for DI

First, let's take a look at a concrete user function, which is an entry point from TYPO3 to our code base.

<?php

namespace E2\E2Core\Export\UserFunction;

use E2\E2Core\Export\JsonSerializer\JsonEncode;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;

class Convert
{
    /**
     * @var JsonEncode
     */
    private $jsonEncoder;

    public function __construct(
        JsonEncode $jsonEncoder = null
    ) {
        $this->jsonEncoder = $jsonEncoder ?? GeneralUtility::makeInstance(ObjectManager::class)->get(JsonEncode::class);
    }

    public function typo3LinkReference(string $link): string
    {
        return json_encode($this->jsonEncoder->normalizeTypo3Link($link));
    }

    public function typo3FilePath(string $filePath): string
    {
        return $this->jsonEncoder->normalizeFilePath($filePath);
    }
}

This class offers two user functions and already is using dependency injection. It expects an JsonEncode instance on creation. The little change compared to TYPO3 10.4? This dependency is marked optional by using = null. That's necessary as older TYPO3 versions don't resolve dependency for us. Therefore, we use GeneralUtility::makeInstance(ObjectManager::class)->get(JsonEncode::class); to fetch the dependency.

The usage of ObjectManager is part of our second concrete example.

Following this concrete example, our code is already prepared for 10.4 native dependency injection. We also can write our tests and inject dependencies. No need to change those tests for 10.4.

Once we update to 10.4, we will use TYPO3 rector to remove the = null and ?? GeneralUtility … call. No manual work is involved in updating this code.

Second concrete example for DI

The second example is code from within our code base. We expect that this class is already injected, or at least fetched via Extbase ObjectManager.

<?php

namespace E2\E2Core\Export\JsonSerializer;

use E2\E2Core\Service\ImageServerService;
use E2\E2Core\Service\TypolinkService;

class JsonEncode
{
    /**
     * @var ImageServerService
     */
    private $imageService;

    /**
     * @var TypolinkService
     */
    private $typolinkService;

    public function __construct(
        ImageServerService $imageService,
        TypolinkService $typolinkService
    ) {
        $this->imageService = $imageService;
        $this->typolinkService = $typolinkService;
    }

    public function normalizeTypo3Link($value)
    {
        if (is_string($value) === false) {
            return $value;
        }

        $result = $this->typolinkService->process($value);
        return $result ?? $value;
    }

    public function normalizeFilePath(string $value): string
    {
        return $this->imageService->generateFileSrc($value);
    }
}

That's the class which is used by the first example. As instances are always injected, or fetched by ObjectManager, there is no need for the more complex code from first example. This code will not change in 10.4 and just work.

Using PSR-14 events

TYPO3 10.4 introduced the PSR-14 events. Until then, there were hooks and signal / slots from Extbase. The events will replace those solutions step by step.

Some concepts can already be used within older TYPO3 code base, easing actual code in older versions and reducing amount of work during updates.

One can follow the new TYPO3 documentation regarding events, and already create events. Those events encapsulate information and provide APIs. (Let's not talk about using technical implementations for foreign concepts here). Events are simple classes that are passed through the dispatcher. The same is already possible in older versions. Only the dispatcher needs to be replaced during an update to 10.4, keeping the rest as is.

Concrete example for events

Let's have a look at a concrete example on how to implement an event in 8.7. I'll not go into detail here, because you can find information at docs.typo3.org for 10.4 already. This example should only show how to use it in older versions.

private function jsonEncode($object): string
    {
        $normalizer = new ObjectNormalizer();

        $event = new InitializeJsonEncode($object);
        $this->dispatcher->dispatch(__CLASS__, 'jsonEncodeNormalizer', [$event]);
        $normalizer->setCallbacks($event->getCallbacks());
        $normalizer->setIgnoredAttributes($event->getAttributesToIgnore());

        $serializer = new Serializer(
            [$normalizer],
            [new JsonEncoder()]
        );

        return $serializer->serialize($object, 'json');
    }

The above method creates a new instance of an event. The event is dispatched by Extbase dispatcher. Once you update, replace the dispatcher and the line dispatching the event. Everything else can stay.

Let's see the counterpart, registering and using the event.

public static function register()
    {
        $dispatcher = GeneralUtility::makeInstance(Dispatcher::class);

        $dispatcher->connect(
            JsonSerializer::class,
            'jsonEncodeNormalizer',
            JsonEncode::class,
            'initializeNormalizer'
        );
    }

    public function initializeNormalizer(InitializeJsonEncode $event)
    {
        $event->addCallback('meta', [$this, 'normalizeFeUserGroups']);

        $object = $event->getObject();

        if ($object instanceof HasExtbaseFileReferences) {
            $this->initializeCallbackForAttributes(
                $event,
                'fileReferenceProperties',
                'normalizeExtbaseFileReference'
            );
        }
        if ($object instanceof HasTypo3LinkReferences) {
            $this->initializeCallbackForAttributes(
                $event,
                'linkProperties',
                'normalizeTypo3Link'
            );
        }
    }

The first method is called from ext_localconf.php to connect the listener to the dispatcher. This will be removed with 10.4. Instead, the registration will be done via configuration inside Services.yaml. The public method which receives the event will stay when updating to 10.4, no change will be necessary.

Conclusion

If you follow recent development, e.g. by reading the changelog, you can understand new concepts and ideas. By following these new concepts within older projects, updates will become smoother. You can already learn new concepts, which will make working with recent versions easier. Beside all of that, your code already follows those concepts, which should make it "better".

In some cases updates become even easier, thanks to preparation and usage of TYPO3 rector.

Acknowledgements

Thanks to Naderio for pushing me to write this blog post.

Thanks to Sebastian Schreiber for creating, maintaining and developing TYPO3 rector.

Further reading

You might also be interested in the following sources, which touch the same topics: