This is the third article in a sort of mini series about developing applications with extbase. The other two articles are
In this article we are going to look at ways to structure and contextualize an extbase application. With bounded context domain objects, a seldom used concept in extbase that can be very useful in larger applications, we can break down a huge domain model into small and focused parts that each have a bounded context.
This article is heavily inspired by a fantastic talk of Oliver Hader (say Hi at Twitter) and Benjamin Kott (also say Hi at Twitter) at the TYPO3 Developer Days 2019 in Karlsruhe. Go ahead and watch in on YouTube!
We will focus on Olis part of the talk in this article, especially on what he had to say about bounded context in extbase. But first I want to start with some general thoughts about how to approach a new extbase project.
Approaching Extbase
In 2020, I worked on multiple extbase applications and I think the key to success with TYPO3's integrated application framework is to know its capabilities and its limitations.
Extbase can make your life so much easier but it can also lead to cluttered and bloated classes if one was only to follow the available documentation and examples that can be found online. That does not mean those information are bad or wrong, they just have the usual issues that (online) documentation sometimes has:
- Being rather broad
- Trying to cover everything
- Using simplified examples
- Offering outdated information
Don't get me wrong. That's all fine for documentation, tutorials and examples. As a documentation, you don't have a specific context, so you are broad. You don't answer a specific question, so you cover everything. You don't want too much complexity, so you use simple examples. And you are maintained by the hands of voluntary contributors, so you might not be updated always immediately.
The point I am trying to make, is that the documentation must be read as a template, a scheme that merely talks about concepts and that it is not meant to be copied and pasted to production code. You may not need getter methods in your domain models, you most likely don't have to implement all CRUD actions in your controllers, your controller actions do not necessarily need to receive your domain models as arguments (read more about DTOs in extbase), your data does not necessarily need to be editable through the backends form engine, you don't have to stick to the default behavior in case of a validation error, etc etc.
Instead you need to think about the requirements of what you are trying to achieve and how the concepts that the documentation talks about can help you getting there. If you are already doing this, great, you can feel good now :)
This blog btw suffers from (at least some of) the same inadequacies as the documentation does. It may have more context in a single article and allows for a more specific and in-depth discussion of a topic, but it still uses simplified examples and may contain outdated information. It is also necessary to evaluate whether a concept like bounded context makes sense in an application. The technique is an option that one should be aware of when developing with extbase, it is by no means always beneficial.
It is all about knowing the tools, so that you don't use a hammer where a screwdriver would do the better job.
With this mediocre analogy out of the way, let's jump into the topic.
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.
Bounded Context
We are going to use roughly the same example application (simplified) as Oli did in the talk of the Developer Days: a car rental service. Let's assume our aggregate root is the car. A simplified domain model representing a car could look like this:
namespace Vendor\Ext\Domain\Model;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
class Car extends AbstractEntity
{
protected string $name;
protected string $promoTeaser;
protected string $color;
protected Brand $brand;
/** @var ObjectStorage<Tire> */
protected ObjectStorage $tires;
/** @var ObjectStorage<Rental> */
protected ObjectStorage $rentals;
/** @var ObjectStorage<Maintenance> */
protected ObjectStorage $maintenances;
/** @var FileReference[] */
protected array $images;
// constructor, getter and business logic
}
Now, the idea of bounded context objects is to think about the domain models (especially the aggregate roots) in the different contexts they are processed in. For example we could separate the context of renting a car from the context of maintaining a car. Instead of having one big Domain\Model\Car
with all properties (as shown above), we split it into Domain\Model\Rental\Car
and Domain\Model\Maintenance\Car
.
namespace Vendor\Ext\Domain\Model\Rental;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
class Car extends AbstractEntity
{
protected string $name;
protected string $color;
protected Brand $brand;
/** @var ObjectStorage<Rental> */
protected ObjectStorage $rentals;
/** @var FileReference[] */
protected array $images;
}
This Rental\Car
object only contains properties and relations relevant in the context of renting a car. We do not need information about past maintenances, tires or other aspects of the technical side of a car, so we just don't have them here.
The Maintenance\Car
counterpart could look like this:
namespace Vendor\Ext\Domain\Model\Maintenance;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
class Car extends AbstractEntity
{
protected string $name;
protected string $color;
protected Brand $brand;
/** @var ObjectStorage<Tire> */
protected ObjectStorage $tires;
/** @var ObjectStorage<Maintenance> */
protected ObjectStorage $maintenances;
// constructor, getter and business logic
}
Having bounded context objects means that we have to think about our application to identify only those properties and relations that are needed for a given context. We will see how we implement such a structure in extbase soon, but let's talk a little bit more about the concept first.
There are two main reasons for introducing context aware domain models. The first is performance and the second is separation of concerns.
Performance
The ORM part of extbase kindly maps data (coming from web requests or the database most of the time) to our domain models. Without spending too much time on the details of that process it is one of the key convenience aspects of the framework. If we assume a complex aggregate root with many properties, extbase will fetch and map all of them if we use the class as an action argument or if we ask a repository for it. All relations of our model are also fetched with all their properties and their relations and so on. This can add up to a lot of database queries and property mapping time, especially if a collection of objects is needed e.g. to render a list of items.
To prevent the framework from fetching all relations, one could make use of lazy loading by using the @TYPO3\CMS\Extbase\Annotation\ORM\Lazy
annotation (official documentation). This, however will still be evaluated and if accessed, the relation is fetched later with additional database queries. That being said, lazy loading could be a valid solution.
A bounded context object on the other hand would only contain the properties needed for the current purpose. All superfluous relations and properties are just not part of the class. This is much cleaner and most efficient in terms of performance, because fewer properties need to be processed and mapped. We only fetch what we need. For example the Maintenance\Car
model does not need the rentals and the Rental\Car
model does not need the maintenances (among other things).
Let's quickly look at separations of concerns.
Separations of Concerns
Separating our classes by context also enables us to put logic where it belongs. Business logic concerning the rental process is no longer present in the maintenance part of the domain and vice versa. Having business logic separated is desirable for maintainability and testability of an application.
With the distinction between the contexts we naturally move towards another advantage: having dedicated plugins for each process. When we look at the maintenance and rental context separately, it is kind of obvious to not mix up both processes within the same extbase plugin. Extbase allows for restriction of callable controller actions on a per plugin base, that is why it makes so much sense to not only separate the bounded context objects but the plugins as well. The rental plugin has no business whatsoever calling anything from the maintenance context.
If we have plugins that are bounded to a context as well, we can also make use of TYPO3s built in user group based access management.
This also mitigates some security issues we might run into if we'd allow one plugin to contain all the funtionality.
Implementation
To retrieve our Vendor\Ext\Domain\Model\Rental\Car
we create a repository with the same little extra namespace part: Vendor\Ext\Domain\Repository\Rental\CarRepository
. Unfortunately (but not surprisingly) this will not work out of the box.
When mapping database tables to domain models, extbase uses a convention that expects the database table name to correlate with the last parts of the FQCN of a domain object, resp. a repository class. By introducing a new section in the namespace we broke this convention and extbase can no longer find the database table automatically. Therefore we need to tell extbase what database table to use when building our classes.
This happens in the file Configuration/Extbase/Persistence/Classes.php
in our extension:
return [
\Vendor\Ext\Domain\Model\Maintenance\Car::class => [
'tableName' => 'tx_ext_domain_model_car',
],
\Vendor\Ext\Domain\Model\Rental\Car::class => [
'tableName' => 'tx_ext_domain_model_car',
]
];
And that's it. That is all we have to do to enable extbase to handle our bounded context objects.
In TYPO3 9LTS and below the class to table mapping had to be done in TypoScript. If you want to know more, please refer to the official documentation or the Breaking RST from the patch.
Let's look at one more example.
Use Case Example
To not only write down what Oli talked about at the Developer Days, I'll add another example. Let's assume we also need an API. People love APIs. We need an endpoint that provides promotional information for all our cars, as JSON of course. Maybe an app displays this with a link to a rental form or something like this.
So how do we do this with what we learned so far?
First we create a new bounded context object: Vendor\Ext\Domain\Model\Promotion\Car
:
namespace Vendor\Ext\Domain\Model\Promotion;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
class Car extends AbstractEntity implements \JsonSerializable
{
protected string $name;
protected string $promoTeaser;
protected string $color;
protected Brand $brand;
/** @var FileReference[] */
protected array $images;
// constructor
public function jsonSerialize()
{
return [
'name' => $this->name,
'promoTeaser' => $this->promoTeaser,
'color' => $this->color,
'brand' => $this->brand->getName(),
'images' => $this->getImageUrls()
];
}
public function getImageUrls(): array
{
// fetch and return array of image URLs
}
}
Also Vendor\Ext\Domain\Repository\Promotion\CarRepository
needs to exist and extend TYPO3\CMS\Extbase\Persistence\Repository
. We need to add our new bounded context domain model to the class mapping configuration for extbase:
return [
\Vendor\Ext\Domain\Model\Maintenance\Car::class => [
'tableName' => 'tx_ext_domain_model_car',
],
\Vendor\Ext\Domain\Model\Rental\Car::class => [
'tableName' => 'tx_ext_domain_model_car',
],
\Vendor\Ext\Domain\Model\Promotion\Car::class => [
'tableName' => 'tx_ext_domain_model_car',
]
];
Now, that the mapping works, we can move on to think about how we access the data.
To keep things separated we create a dedicated plugin that only deals with this API.
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
'ext',
'car_promotion',
[Vendor\Ext\Controller\CarPromotionController::class => 'list']
);
No TCA registration for tt_content needed, as we only embed this plugin via a page type, that we can map to /cars.json
.
Our controller looks like this:
namespace Vendor\Ext\Controller;
use Psr\Http\Message\ResponseInterface;
use Vendor\Ext\Domain\Repository\Promotion\CarRepository;
class CarPromotionController extends ActionController
{
protected CarRepository $carRepository;
public function __construct(CarRepository $carRepository)
{
$this->carRepository = $carRepository;
}
public function listAction(): ResponseInterface
{
return new JsonResponse(
$this->carRepository->findAll()->toArray()
);
}
}
We use constructor injection here. Read my article about Dependency Injection in TYPO3 if you want to learn more about it.
Now we define the page type. Although this can be considered out of scope of this article as it does not really have anything to do with the bounded context approach. It is just the way we make the data available, there are other ways and it does not matter how we access the data for bounded context to make sense here. Anyway, for the sake of completeness, here is the TypoScript we need to make everything work:
carPromotion = PAGE
carPromotion {
config {
disableAllHeaderCode = 1
additionalHeaders {
10 {
header = Content-Type: application/json
replace = 1
}
}
}
typeNum = 121212
10 < tt_content.list.20.ext_car_promotion
}
Mapping the page type to /cars.json
can be done in the routeEnhancers
part of our site configuration with the Page Type Decorator (see official documentation).
By looking at our new model Vendor\Ext\Domain\Model\Promotion\Car
again, we can immediately see the advantages. We do not fetch any relations other than the Brand. We can do our specific logic, in this case implementing JsonSerializable
and fetching image URLs as needed for the purpose of the promotion. We do not need to spend time thinking about accidentally leaking rental or maintenance data because that data is simply not in our scope of our domain model. We do not need to worry about our plugin being able to grant unintended access to actions of other parts of our applications because it is strictly limited to do one thing and one thing only.
I hope after reading this article you consider using the concept of bounded context in the next extbase project where it seems fit. I want to thank Oli again for the inspiring talk (go to YouTube now and finally watch the video, this is the end of the article anyway).
Thanks for reading.