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

More about support

Attributes were among the cool new things introduced with version 8 of PHP in late 2020. Attributes offer a structured way of adding meta information to classes, methods etc. A welcome improvement to annotations within DocBlock comments, that we were used to until this point.

Granted, at time of writing PHP attributes are not exactly new anymore and neither is PHP 8. In fact PHP 8.0 already reached its end of life. But since TYPO3 v12 was the first TYPO3 LTS version to require PHP 8, we only started to see PHP 8 features relatively recently in a TYPO3 LTS release.

In this post we will quickly talk about PHP attributes in general (despite others have done so years ago) and then have a look at how they are utilized by the TYPO3 core and what new possibilities they offer when we upgrade our projects to TYPO3 v12 or v13.

PHP Attributes - Quick Overview

A PHP attribute provides meta data for a class, a property, a method, a constant. Think of it as if the attribute describes an aspect of its target. For example: "This class is a CLI command" or "This property will be validated to not be empty" or something along those lines. And since attributes are part of the language and e.g. accessible via reflection, they can be evaluated and therefore used for registration and configuration.

Attributes are also classes themselves. And they all have the attribute #[Attribute], which is - in a sense - the mother of all attributes.

Let's unpack this step by step.

First, this is how a class with a simple attribute looks like:

use Vendor\Namespace\CustomAttribute;

#[CustomAttribute]
class Something
{
    // the code
}

Attributes can be assigned more than once if the attribute has Attribute::IS_REPEATABLE set (see below). They also can have arguments. All ways known to the PHP syntax of providing arguments are supported:

use Vendor\Namespace\CustomAttribute;

#[CustomAttribute('special')]
#[CustomAttribute(type: 'something else')]
class SomethingElse
{
    // the other code
}

The attribute  CustomAttribute must be a PHP class that must be able to process the given arguments. It could look like this:

use Attribute;

#[Attribute]
class CustomAttribute
{
    public function __construct(public readonly $type = 'default')
    {
    }
}

When we define an attribute class, we can also specify the valid targets of the attribute to limit its usage. We do this by using a bitmask:

use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)]
class CustomAttribute
{
    public function __construct(public readonly $type = 'default')
    {
    }
}

The following constants are available for this:

  • Attribute::TARGET_CLASS
  • Attribute::TARGET_FUNCTION
  • Attribute::TARGET_METHOD
  • Attribute::TARGET_PROPERTY
  • Attribute::TARGET_CLASS_CONSTANT
  • Attribute::TARGET_PARAMETER
  • Attribute::TARGET_ALL
  • Attribute::IS_REPEATABLE

Equipped with this basic knowledge about attributes, we are ready to dive into the TYPO3 specifics.

Buckle up.

Top

PHP Attributes in TYPO3

In TYPO3 attributes are mostly used for the registration of services, that we had to manually add to our Services.yaml before.

The symfony service container introduced with the dependency injection rollout (read more about this in the post Dependency Injection in TYPO3) offers a convenient way of automatically tag classes that have a certain attribute and use the attribute's arguments for the configuration. The method registerAttributeForAutoconfiguration() is used in Services.php files for this. Head over to the official symfony documentation about Autoconfiguring Tags with Attributes if you want to read about the details.

We will see a few examples of this shortly.

That being said, one great advantage of using annotations is convenience. We have the implementation and the registration in one place, which is nice. It reduces the size of our Service.yaml files, while offering a structured way to configure our services through objects with type safety. The later is another important aspect of attributes: they are part of PHP itself and therefore have smooth IDE support. As a developer, I like.

There are already quite a few places where attributes are used in the TYPO3 core. As usual, we find the documentation about all the supported attributes in the TYPO3 documentation.

So, let's look at them one by one.

Top

Backend Controller - #[AsController]

Since TYPO3 12

Patch | Patch | Feature RST | Feature RST | Official Documentation

This is a simple one.

To have dependency injection available in classes, that are instantiated via GeneralUtility::makeInstance(), those classes need to be marked as public in the service container (so that the makeInstance() method can get them out of the container). This can be achieved directly in the Services.yaml by setting public: true. Since all backend controller are instantiated this way and we want dependency injection in all of them, the tag 'backend.controller' was introduced to achieve the same thing, but still in the Services.yaml.

Now we have an attribute for this:

use TYPO3\CMS\Backend\Attribute\AsController;

#[AsController]
class CustomBackendController {

}

This is all we have to do, which is nice because there is no need to write anything into the Services.yaml for backend controllers anymore.

Top

CLI Commands - #[AsCommand]

Since TYPO3 12

Patch | Important RST | usetypo3.com Article

CLI commands got an attribute as well. In this case the attribute shipped with the symfony component was used. Classes with this attribute will receive the tag console.command automatically. This means it is now possible to register a command like this:

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Attribute\AsCommand;

#[AsCommand(name: 'custom:doIt', description: 'A description', aliases: ['custom:doIt-alias'])]
class CustomCommand extends Command
{
}

However, using the symfony attribute in a TYPO3 context is a bit of an odd choice, because TYPO3 CLI commands can be configured to be schedulable through the TYPO3 scheduler module. Of course the symfony attribute knows nothing about the scheduable: true|false option.

All CLI commands registered with the #[AsCommand] attribute, will be schedulable: true and cannot be set to schedulable: false. This means if we want our CLI command to not show up in the scheduler, we still need to register it in the Services.yaml.

A quick opinion piece on that:

The argument for this deliberate decision seems to be, that all "proper" CLI commands should always be schedulable. A statement that I disagree with and so I unfortunately cannot use this attribute for many of my commands. We may see some more changes in this area, though. For now, it is what it is.

Next.

Top

Extbase Annotations - #[Extbase\Validate]

Since TYPO3 12

Patch | Feature RST

Extbase had support for structured annotations since approximately forever. We used annotations mainly for validation of object properties or controller action arguments. There are also some ORM related annotations.

To remind ourselves how the annotations look like, lets look at a simple example of a property validation up to TYPO3 v11 LTS and PHP 7.4:

use TYPO3\CMS\Extbase\Annotation as Extbase;

class MyDTO
{
	/** @Extbase\Validate("StringLength", options={"minimum": 1, "maximum": 5}) */
	public string $shortString

    public function __construct(string $shortString)
    {
		$this->shortString = $shortString;
	}
}

All of the extbase annotations are now also available as PHP attributes.

For the property validation example from above, the usage of the attribute (and PHP 8 constructor property promotion) looks like this:

use TYPO3\CMS\Extbase\Annotation as Extbase;

class MyDTO
{
    public function __construct(
        #[Extbase\Validate(['validator' => 'StringLength', 'options' => ['minimum' => 1, 'maximum' => 5]])]
        public readonly string $shortString
    )
    {}
}

The Validate attribute is also available for controller actions:

use TYPO3\CMS\Extbase\Annotation as Extbase;
[...]

class CustomController extends ActionController
{
    #[Extbase\Validate(['validator' => CustomDTOValidator::class, 'param' => "myDto"])]
    public function processAction(MyDTO $myDto): ResponseInterface
    {}
}

Note, that the former annotation classes are still in use. They "just" got the Attribute attribute. This means that the constructor of the attribute expects an array, because that was how the annotations worked. That is why we have to specify the array keys in the attribute constructor:

// This would work:
#[Extbase\Validate(['validator' => 'NotEmpty'])]

// We cannot just use the validator as the constructor argument:
// This would NOT work:
#[Extbase\Validate('NotEmpty')]

// To make this more obvious we could use a named parameter:
// This would work:
#[Extbase\Validate(values: ['validator' => 'NotEmpty'])]

The beforementioned ORM related attributes are the following:

use TYPO3\CMS\Extbase\Annotation as Extbase;

class MyOtherDTO
{
    public function __construct(
		#[Extbase\ORM\Lazy()]
		#[Extbase\ORM\Transient()]
		#[Extbase\ORM\Cascade(['value' => 'remove'])]
		public readonly RelationalDTO $relation
	)
}

When we update our extbase applications to a TYPO3 v12 or above, the migration of annotations to attributes should be pretty straight forward.

Although the attribute constructors are not as clean as they could be because of the array syntax, this is still a welcome improvement.

What's next?

Top

Webhooks - #[WebhookMessage]

Since TYPO3 12

Patch | Feature RST | usetypo3.com Article

One of the big features introduced with TYPO3 v12 were webhooks (read the article linked above for more information on how to use them). And right from the get go, a attribute was available to register a class as a WebhookMessage:

use \TYPO3\CMS\Core\Attribute\WebhookMessage;

#[WebhookMessage(
    identifier: 'dg/cache-flushed',
    description: '... when the cache was flushed from CLI',
    method: 'createFromEvent'
)]
final class CacheFlushMessage implements WebhookMessageInterface
{

}

The 3rd argument (method) that can be set with the attribute, is optional. It specifies the factory method of the WebhookMessage object. By default this is createFromEvent(), which is used when creating a message by an event listener.

Convinient.

Top

Message Handlers - #[AsMessageHandler]

Since TYPO3 13

Patch | Feature RST | usetypo3.com Article

With the message handlers it is the same story as with the other services discussed beforehand. Instead of manually tagging them with 'messenger.message_handler' in the Services.yaml, we can simply use the attribute now:

use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final class OrderReceivedHandler
{
}

From the FQCN we see, that the attribute comes with the symfony messenger component, but this time it is fully compatible with TYPO3.

This attribute is not supported in TYPO3 v12. Support was added with TYPO3 v13.

Top

Event Listeners - #[AsEventListener]

Since TYPO3 13

Patch | Patch | Feature RST | usetypo3.com Article

Since TYPO3 v13 it is possible to register listeners for PSR-14 events with the PHP attribute #[AsEventListener]. This, like most of the other attributes, lets us remove our registration from the Services.yaml file, as the class is tagged with event.listener automatically.

The new registration looks like this:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use Vendor\Extension\Event\CustomEvent;

#[AsEventListener(
    identifier: 'my-extension/my-listener'
)]
final class MyListener
{
    public function __invoke(CustomEvent $event): void
    {
        // do things
    }
}

In case that a different method than __invoke() should take care of the event, the attribute can also be added to a method:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use Vendor\Extension\Event\CustomEvent;

final class MyListener
{
    #[AsEventListener(
        identifier: 'my-extension/my-listener'
    )]
    public function processTheCustomEvent(CustomEvent $event): void
    {
        // do things
    }
}

Nice.

These are the main attribute available to the TYPO3 core and to extension authors (for now). When working with TYPO3 v12 or above, we should definitely use them, as they are future proof. Also configuration directly in the registered classes is nice and so is full IDE support.

Top

Easy Migration with typo3-rector

If you don't want to manually change your code, have a look at the awesome typo3-rector (read the article about it and learn how to use it). Of course, there are some rectors that migrate our code automatically to use the attributes:

This is obviously great, thanks to the rector team.


Also, thanks to everybody involved in bringing the attributes to TYPO3.

And last but not least thanks for reading and supporting this blog.

Cheers.

 

Top

Further Information