When developing extbase plugins for your TYPO3 projects you often have to implement a form to submit data to the server in order to create a domain record. The extbase documentation and many popular examples suggest to create a frontend form with the properties of your domain object and receive an instance of said domain object in the controller action that the form submits the data to so that you can persist it in the database (or do whatever with it). Validation is bound to the domain objects properties as is the property mapping of extbase.
In my experience this is often insufficient because while my frontend form might share some fields with the domain objects properties it most likely has its differences, too. This is among the reasons why I almost always use dedicated objects to represent just the form data as it is transferred to the server - so to speak: data transfer objects or DTOs.
The examples shown in this post use PHP 7.4 features with strictly typed properties. The extbase controller actions return response objects as it will be the case from TYPO3 11 onward. If you want to try the code with a lower PHP version and/or a lower TYPO3 version please adapt it accordingly.
This blog post will show what DTOs are, why I think they can improve many extbase extensions and when I think they are a good choice. Let's start with the term DTO.
DTOs
The abbreviation DTO stands for data transfer object and is a widely used approach to bring structure and type safety to the loose and stringy nature of e.g. web requests. DTOs are often used to hold the current state of submitted filters and search options (see for example the demand object of the TYPO3 system extension redirects). Other usecases are response objects of APIs, form submissions etc. The great advantage of objects is the type safeness of the data as well as accessing it via methods instead of using array keys that are very error prone to typos.
In the following talk from the PHP UK Conference 2019, speaker Michael Cullum talks about "Building first-class REST APIs with Symfony" and also mentions DTOs (starting at approx. 18:36). Watch the talk if you are interested, its pretty good.
There are also some good blog posts on the topic, here is one from the laravel world.
As we've seen DTOs can be used in many contexts, but we will look specifically at a web request submitting form data to an extbase plugin next. The idea is to have an immutable object that represents the form as it was submitted.
DTOs in the context of extbase
As we are going to focus on TYPO3 and especially extbase, we do not really have to deal with incoming arrays. Thanks to the property mapping we are pretty used to work with objects and their (typed) properties anyway. In that sense every object that is passed to an action via extbase could be described as a DTO. DTOs in the context of extbase as I am going to showcase in this article are therefore maybe better described as form data objects that encapsulate form data away from our domain models. But to not add another layer of confusion, I'll stick to call them DTOs.
What we are going to see in this example is a fairly simple contact form with name fields, an email address, a textarea field for the message and some checkboxes for newsletter registration and data protection. So nothing too complicated but enough to make some points about the advantages of DTOs. In our simple example the DTO would look like this:
class ContactFormData
{
protected string $firstName;
protected string $lastName;
protected string $emailAddress;
protected string $message;
protected bool $dataProtection;
protected bool $newsletterSubscription;
public function __construct(
string $firstName,
string $lastName,
string $emailAddress,
string $message,
bool $dataProtection,
bool $newsletterSubscription
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
$this->emailAddress = $emailAddress;
$this->message = $message;
$this->dataProtection = $dataProtection;
$this->newsletterSubscription = $newsletterSubscription;
}
public static function createEmpty(): self
{
return new self(
'',
'',
'',
'',
false,
false
);
}
public function getFirstName(): string
{
return $this->firstName;
}
public function getLastName(): string
{
return $this->lastName;
}
public function getEmailAddress(): string
{
return $this->emailAddress;
}
public function getMessage(): string
{
return $this->message;
}
public function hasDataProtection(): bool
{
return $this->dataProtection;
}
public function hasNewsletterSubscription(): bool
{
return $this->dataProtection;
}
public function getFullName(): string
{
return $this->firstName . ' ' . $this->lastName;
}
}
There are a few things to say about this class.
- To enable extbase to map the incoming request to the properties of our DTO, we either need setter methods which we definitely do not want in our immutable object, or we need a constructor stating the types of the properties. That is why all (my) DTOs in the context of extbase have every property spelled out in the constructor.
- We do not extend the extbase
AbstractEntity
because ... the object is not an entity that represents a database entry with a unique ID but a mere tool with the purpose of holding the submitted data. - #1 leads to inconveniences in regard of creating an instance of our class as none of the constructor arguments are optional. So we could either make the constructor arguments optional and assign default values, so that we can use
new ContactData()
or we could introduce factory methods for specific use cases. As the latter is more explicit and preserves the strictness of the constructor, I went for the factory method withContactData::createEmpty()
. - We add getter methods to support the fluid form displaying the current values in case of a validation error. The default behavior of extbase in case of a validation error is to add a flash message to the queue and then forward to the action that rendered the form. If we choose a different behavior and do no forward to the fluid form again, then we could omit every getter that is not needed to further process the data. Additionally, we add getter that allow access to the data as wee need it when processing it, as shown with
getFullName()
.
Speaking of validation, that brings us to the next chapter and additional advantage.
Validation
Now that we have a dedicated object only for our frontend form, we can happily make use of the extbase validation layer that comes with many validators out of the box.
Using extbases validation would have also been possible if we had used our domain object directly but this tends to become annoying if we have multiple forms dealing with the same entity but in different ways or if fields are added or removed. I find it rather convenient to have the validation rules of a frontend form represented by a dedicated object on the server side.
So, let's add some validators to our DTO:
use TYPO3\CMS\Extbase\Annotation as Extbase;
class ContactFormData
{
/** @Extbase\Validate("NotEmpty") */
protected string $firstName;
/** @Extbase\Validate("NotEmpty") */
protected string $lastName;
/** @Extbase\Validate("EmailAddress") */
protected string $emailAddress;
/** @Extbase\Validate("StringLength", options={"minimum": 3, "maximum": 800}) */
protected string $message;
/** @Extbase\Validate("Boolean", options={"is": "true"}) */
protected bool $dataProtection;
protected bool $newsletterSubscription;
With this approach our actual domain model does not need any validation related annotations at all. We can keep it clean and simple. In fact, we can keep it really clean, as we see in the next chapter.
Converting DTOs to Domain Models
Flow and extbase have taught the TYPO3 developers over the years to embrace the concepts of domain driven design. That is why at the heart of every extbase extension lies the domain, the business logic. We reflect the domain through a collection of objects, called the domain models.
Most likely the data that is encapsulated in our DTO (or at least parts of it) needs to be converted to a domain model, so we can hand it over to the repository for persistence. We can use a factory method or a factory class for this. Both approaches are totally fine, they are a matter of taste for rather simple structures but I would prefer and recommend a factory class if things get a little bit more complex.
Let's say our domain model is called Inquiry
and stores parts of our ContactFormData
in the database. It also holds the current date and a boolean flag indicating whether the record is highlighted. With a factory method fromContactFormData()
the class could look like this:
use Vendor\Extension\Domain\DTO\ContactFormData;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
class Inquiry extends AbstractEntity
{
protected string $name;
protected string $emailAddress;
protected string $message;
protected \DateTime $dateTime;
protected bool $highlighted;
public function __construct(
string $name,
string $emailAddress,
string $message,
\DateTime $dateTime,
bool $highlighted
) {
$this->name = $name;
$this->emailAddress = $emailAddress;
$this->message = $message;
$this->dateTime = $dateTime;
$this->highlighted = $highlighted;
}
public static function fromContactFormData(ContactFormData $contactFormData): self
{
return new self(
$contactFormData->getFullName(),
$contactFormData->getEmailAddress(),
$contactFormData->getMessage(),
new \DateTime('now'),
false
);
}
public function markHighlighted(): void
{
$this->highlighted = true;
}
public function markNotHighlighted(): void
{
$this->highlighted = false;
}
}
We remember that the form rendered in the frontend had a field for the first and last name each, and we now see that the domain model stores the full name in a single field. Additionally, we do not consider the checkboxes of the frontend form at all, because agreeing to the data protection clause is mandatory for every record to make it through validation, so we do not need to store this information. The newsletter registration is done in a different service so that we are not concerned with it here.
This immediately shows the advantage of having an object that encapsulates the transferred data over putting everything in a domain model class: a very clean domain model class. As we do not use our domain model for rendering in a fluid form we do not need to add getter methods for anything. In fact, we only add methods that contain the business logic of our inquiry, in this case methods to toggle the highlighted flag. No bloat at all.
The fields date
and highlighted
are not part of the frontend form but are set in the factory method to their respective values.
With our example classes the controller would look like this:
use Vendor\Extension\Domain\DTO\ContactFormData;
use Vendor\Extension\Domain\Model\Inquiry;
use Vendor\Extension\Domain\Repository\InquiryRepository;
use Vendor\Extension\Service\NewsletterService;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Annotation as Extbase;
class InquiryController extends ActionController
{
protected InquiryRepository $inquiryRepository;
protected NewsletterService $newsletterService;
public function __construct(InquiryRepository $inquiryRepository, NewsletterService $newsletterService)
{
$this->inquiryRepository = $inquiryRepository;
$this->newsletterService = $newsletterService;
}
/** @Extbase\IgnoreValidation("contactFormData") */
public function contactFormAction(ContactFormData $contactFormData = null): ResponseInterface
{
$this->view->assign('contactFormData', $contactFormData ?? ContactFormData::createEmpty());
return $this->htmlResponse($this->view->render());
}
public function createAction(ContactFormData $contactFormData): ResponseInterface
{
$this->newsletterService->processContactFormData($contactFormData);
$this->inquiryRepository->add(
Inquiry::fromContactFormData($contactFormData)
);
return new RedirectResponse(
$this->uriBuilder->setTargetPageUid($this->settings['thankYouPage'])->build()
);
}
}
With this the example of using a DTO to create a domain model is complete. The same principles apply to editing a existing domain record, e.g. in an admin view where inquiries could be deleted, answered or marked as highlighted. We would build a new DTO that represents the editing form and have a factory method in the DTO e.g. public static function fromInquiry(Inquiry $inquiry): self
for initialization. We would hand the DTO to the view and receive it back in our updateAction
.
Collection of DTOs
Since TYPO3 11With TYPO3 11 the extbase property mapper can handle collection of DTOs in an ObjectStorage
. We need to have a setter function for this to work:
class CartData
{
/** @var ObjectStorage<ItemData> */
protected ObjectStorage $items;
/** @param ObjectStorage<ItemData> */
public function setCollection(ObjectStorage $items): void
{
$this->items = $items;
}
}
The functionality was provided with this patch (there also is a Feature RST).
Conclusion
I am not suggesting to always use DTOs no matter what. Sometimes, if your domain model matches your form 1:1 and there are no other entry points to your domain model that need to be considered, you might not need a DTO as it would only introduce overhead. So as usual it is a good idea to take a step back and reflect on requirements before writing the first lines of code.
That being said, with the approach shown in this article I have been able to develop some applications with decent complexity this year. Keeping my domain models concerned with business logic only proofed to be very nice. There are more cool features of extbase I made use of, but I am going to talk about those in other posts. For example have a look at the article Usecase: Caching, DI and Events that also explains some of the cool things we can do nowadays with extbase and TYPO3.
Maybe you are considering using DTOs as well for your next application now? Thanks for reading.
Further Information
- Working with Data, Laravel related Blog Post at stitcher.io, 2019
- Is it a DTO or a Value Object?, Blog Post by Matthias Noback, 2022