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

More about support

When working with extbase it is a good practice to use Data Transfer Objects (DTOs) for incoming data. The most likely use case is a submitted form. The submitted data needs to be processed and we therefore very much want an object to work with rather than a collection of arrays and strings. I already covered the basics of working with DTOs in extbase in my post, aptly named "DTOs in Extbase".

In this post, I want to expand on this by talking about two things specifically: enumerations and PHP 8 features.

When building applications within a TYPO3 system we may work with properties that have a fixed list of possible or allowed values. This is what enumerations are helping with. So, we will look at what enumerations are and how we use them in an extbase context.

And since we are talking about TYPO3 v12 and v13 we can assume PHP 8 which leaves the bonus topic of this blog post: The simplification and beautification of DTOs with PHP 8.2 and later.

Caveats

  1. We will not look at usages of the TYPO3 core placeholder implementation for enumerations: \TYPO3\CMS\Core\Type\Enumeration. This class has done its job by providing a way to implement enumerations before PHP itself had support for it. With TYPO3 v13 the class is deprecated (Patch, Deprecation RST) and will be removed in TYPO3 v14.
     
  2. When talking about extbase, we will focus on the property mapping side of things. Let's quickly explain what that means: Extbase has two major mapping capabilities. On the one hand, the PropertyMapper maps incoming request data to controller action arguments and objects. On the other hand the DataMapper maps data from a persistence layer (most likely your database) to (domain) objects. Since DTOs are very handy when it comes to holding the incoming request data, we will look closer at that aspect of extbase. The DataMapper will be mentioned as well in the end, but always assume, we are talking about request data.

Top

DTOs and PHP 8

PHP 8 brought some very nice syntax sugar to beautify data objects (e.g. DTOs) classes and reduce the amount of code we have to write to have an immutable DTO with accessible properties. Let's see how an example data object improves with each feature applied one by one.

Here is the class, that we start with, fresh out of PHP 7.4:

use TYPO3\CMS\Extbase\Annotation as Extbase;

class SupportRequestData
{
	/** @Extbase\Validate("EmailAddress") */
	protected string $email
	/** @Extbase\Validate("NumberRange", options={"minimum": 1, "maximum": 5}) */
	protected int $supportType
	protected User $user

	public function __construct(
		string $email,
		int $supportType,
		User $user
	) {
		$this->email = $email;
		$this->supportType = $supportType;
		$this->user = $user;
	}

	// Getter
}

Now, let's skim through the related improvements PHP has received and watch this class evolve.

Please follow the links to the official documentation for a deeper dive on each of these features.

Top

PHP 8.0 - Attributes

Official Documentation

Read more about PHP attributes in TYPO3, including all attributes concerning extbase over at my article "PHP Attributes in TYPO3"

For our example class we swap out the annotations for the extbase validation attributes introduced with TYPO3 v12 (Feature RST).

Our class now looks like this:

use TYPO3\CMS\Extbase\Annotation as Extbase;

class SupportRequestData
{
	#[Extbase\Validate(['validator' => 'NotEmpty'])]
    #[Extbase\Validate(['validator' => 'EmailAddress'])]
	protected string $email
	#[Extbase\Validate([
		'validator' => 'NumberRange',
		'options' => ['minimum' => 1, 'maximum' => 5],]
	)]
	protected int $supportType
	protected User $user

	public function __construct(
		string $email,
		int $supportType,
		User $user
	) {
		$this->email = $email;
		$this->supportType = $supportType;
		$this->user = $user;
	}

	// Getter
}

With the new attributes in place, let's now start to shrink down our class.

Top

PHP 8.0 - Constructor Property Promotion

Official Documentation

Constructor property promotion allows us to define the properties of a class directly in the constructor arguments, so that we can get rid of the assignments of the constructor arguments to the properties of the class. This now happens automatically.

class SupportRequestData
{
	public function __construct(
	    #[Extbase\Validate(['validator' => 'EmailAddress'])]
		protected string $email,
		#[Extbase\Validate([
            'validator' => 'NumberRange',
            'options' => ['minimum' => 1, 'maximum' => 5],]
        )]
		protected int $supportType,
		protected User $user
	) {}

	// Getter
}

Because we want our DTO to be immutable we still need getter methods to access the data. We cannot declare the properties public ... yet. Otherwise, we would lose immutability.

This changed with PHP 8.1.

Top

PHP 8.1 - Readonly Properties

Official Documentation

With the readonly keyword, the value of a property can no longer be changed after initialization of the class. This alows for properties to be public and immutable.

With that our class now looks like this:

class SupportRequestData
{
	public function __construct(
	    #[Extbase\Validate(['validator' => 'EmailAddress'])]
		public readonly string $email,
		#[Extbase\Validate([
            'validator' => 'NumberRange',
            'options' => ['minimum' => 1, 'maximum' => 5],]
        )]
		public readonly int $supportType,
		public readonly User $user
	) {}
}

Now, that the properties are all readonly, and we switched to declaring them public, we can access the data directly via e.g. $supportRequestData->email.

As a consequence, we got rid of all out getter methods because we do not need them anymore. Fluid can also access public properties just fine.

Yeah.

Top

PHP 8.2 - readonly Classes

Official Documentation

To further simplify this, PHP 8.2 introduced the option to declare the whole class readonly. This means, internally, readonly is applied to every property of the class.

In our example instead of writing it three times, we now only need to write it once:

readonly class SupportRequestData
{
	public function __construct(
	    #[Extbase\Validate(['validator' => 'EmailAddress'])]
		public string $email,
		#[Extbase\Validate([
            'validator' => 'NumberRange',
            'options' => ['minimum' => 1, 'maximum' => 5],]
        )]
		public int $supportType,
		public User $user
	) {}
}

That's it. That's the entire class.

We only have a constructor with public readonly properties. Fluid works with this. And we can access all our data through the object. Of course some internal logic could be implemented in methods but for mere data transportation this is all we need.

Now let's get to the meat of this article.

Top

Enumerations

With version 8.1 PHP finally got native support for enumerations. To quote the official documentation.

Enumerations, or "Enums", allow a developer to define a custom type that is limited to one of a discrete number of possible values. That can be especially helpful when defining a domain model, as it enables "making invalid states unrepresentable."

We stick to our example where one of the constructor arguments is the $supportType. We see from the attached validator that it can have a value from 1 to 5, so apparently there are 5 different valid support types.

This could be represented with an enumeration.

The main idea behind this would be that $supportType is no longer of the type int, but instead its type becomes an instance of an enumeration, e.g. \Vendor\Extension\Enum\SupportType that has one of the defined cases as its value.

Before we turn back to the example, let's briefly talk about the two flavors of Enums in PHP: Basic and backed Enums, because they differ in terms of how an enumeration relates to its value.

Top

Basic Enumerations

Official Documentation

In a basic enumeration, each value is represented by a unique singleton object that corresponds to the name of the case, allowing for easy identification and comparison within a given context.

To make this clearer, let's look at the following example of how a basic enumeration for our SupportType could be structured:

enum SupportType
{
    case Technical;
    case Billing;
    case Account;
	case Information;
	case Feedback;
}

Instantiation and comparison looks like this:

$a = SupportType::Billing;
$b = SupportType::Billing;

$a === $b; 					// true
$a instanceof SupportType; 	// true
$a->name 					// Billing

Generally speaking, this works fine for DTOs or for scenarios where we only need a clear, human-readable representation of each case. But there are limitations.

When dealing with databases, it is often necessary to store enumeration values in a more compact form, such as integers or strings, rather than relying on the case names themselves. This is where basic enumerations can fall short, as they don’t provide an inherent way to link enumeration cases to specific, storable values.

This is where backed enumerations come in handy. Let's look at those next.

Top

Backed Enumerations

Official Documentation

Backed enumerations are backed (hence the name) by a scalar type (string or integer). This means that every case has a representation of that type, that can be accessed by $enumeration->value. This can be used for persistence in a database and has many other applications.

Let's look at the backed enumeration variant of our SupportType:

enum SupportType: int
{
    case Technical = 1;
    case Billing = 2;
    case Account = 3;
	case Information = 4;
	case Feedback = 5;
}

As soon as an enumeration has a scalar type hint it will automatically implement the interface \BackedEnum that contains two methods:

  • from($value): self will return the Enum Case corresponding to $value or throw a ValueError in case $value does not match any case.
  • tryFrom($value): ?self does the same thing but instead of throwing an error, it simply returns null if $value does not match any case.

Instantiation and comparisons looks like this:

$a = SupportType::from(2);
$b = SupportType::tryFrom(2);
$c = SupportType::from(0);  		// ValueError
$d = SupportType::tryFrom('foo');  	// null
$e = SupportType::Billing;

$a === $b; 							// true
$a === $e							// true
$a instanceof SupportType;  		// true
$a instanceof \BackedEnum;  		// true
$a->name  							// Billing
$a->value  							// 2

Both types of enumeration provide the static method ::cases(), which returns an array of all cases, each represented by an instance of the enumeration. An example usage of this method can be found below.

With this basic knowledge about enumerations in PHP, let's turn our head back towards extbase.

Top

Enumerations in Extbase (Property Mappper)

Since TYPO3 12

Fortunately, extbase's PropertyMapper received support for enumerations in TYPO3 v12 via this patch (Feature RST) in form of the EnumConverter. Since this is a TypeConverter, it enables enumerations as controller action arguments or (our use case) as DTO properties.

Turning to our example again, we can replace the $supportType integer with the backed enumeration from above:

readonly class SupportRequestData
{
	public function __construct(
	    #[Extbase\Validate(['validator' => 'EmailAddress'])]
		public string $email,
		public SupportType $supportType,
		public User $user
	) {}
}

The EnumConverter will convert any of the valid values of the SupportType enumeration into an instance of SupportType.

However, if any invalid value is submitted, the TypeConverter will convert to null. Since in our example, $supportType is a non-optional, non-nullable constructor argument, this would lead to a TypeError.

Because of this, we should allow the $supportType property to be nullable and add a validator to assure it is an instance of SupportType if null is not a valid value.

Our class now looks like this:

readonly class SupportRequestData
{
	public function __construct(
	    #[Extbase\Validate(['validator' => 'EmailAddress'])]
		public string $email,
		public User $user,
		#[Extbase\Validate(['validator' => MyValidator::class])]
		public ?SupportType $supportType = null,
	) {}
}

Other approaches to this dilemma would be to catch the TypeError or have a custom type converter, that could throw a meaningful exception. If you have a better way of dealing with this flaw, please let me know.

For now, let's turn our attention to fluid and how to use enumerations in our templates.

Top

Enumerations in Fluid

Since enumerations behave like objects with public methods and properties, we can access the name of an enumeration just like we would access the property of any other object. Backed enumerations also allow direct access to the value:

{supportType.name}
{supportType.value}

In the context of a form, it is a common use case for a enumeration property to be rendered as a drop down list or a list of radio buttons with all (or some) of the options for the user to select one.

Let's say we want to render a select box to contain all the valid values of our SupportType enumeration, we could do something like the following:

enum SupportType: int
{
    case Technical = 1;
    case Billing = 2;
    case Account = 3;
	case Information = 4;
	case Feedback = 5;

	public function getAsSelectOptions(): \Generator
    {
        foreach (self::cases() as $case) {
            yield $case->value => $case->getLabel();
        }
    }

    public function getLabel(): string
    {
        return LocalizationUtility::translate(
            'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:option.' . $this->value
        ) ?? $this->name;
    }
}

By utilizing these helper methods, we can now render our select field more efficiently and with minimal effort. The options of the select field can be populated directly from the enumeration, and we use labels from locallang.xlf files, so that we can use this in sites with multiple languages as well.

<f:form.select property="supportType" options="{supportRequestData.supportType.asSelectOptions}" />

We see, using enumerations is as convenient in fluid templates as it is in PHP. No need to construct arrays of all possible options, we can keep that logic within the enumeration object itself and easily access it from fluid.

There is one more thing to look at, though.

Top

Enumerations in Extbase (DataMapper)

Since TYPO3 13

When we store the scalar values of our backed enumeration in the database and ask any extbase repository to construct our entity objects (aka domain models) from the database, the DataMapper is capable of creating the enumeration instance with the correct case since this patch (Feature RST) in TYPO3 v13.

Note, that this only works with backed enumerations as only those are backed by a scalar value and because the ::from() and ::tryFrom() methods are used. The null-case is also covered nicely here, as nullable properties use ::tryFrom(), where mandatory properties will be instantiated with ::from(), so only on those we will get an error on invalid values.

If you need DataMapper support for enumerations in TYPO3 v12, this is also a fairly easy patch to composer-patch into a TYPO3 v12.

And with all of this, we conclude the article.

I definitely think, we all should start using enumerations in DTOs with TYPO3 v12 and get used to it for the sake of better typed, cleaner and more approachable code.

Thanks for reading and special thanks to all supporters of the blog.

Cheers.

Top