I’ve been working for a while with wrapped entities, following the principles of this Lullabot article: Maintainable Code in Drupal: Wrapped Entities, but using a custom solution and not the Typed Entity module.
I have to say that, now that the project has evolved and I can have an overview of it, I think we could not have implemented all the complexity of the business logic without this design pattern. In my opinion, it is a fantastic way to organize the code, although it has some problems that must be taken into account.
Why wrapped entities and not bundle classes?
Mateu Aguiló explains it perfectly in the Typed Entity module page: Bundle classes may seem attractive at first, but they have disadvantages: they carry on all the complexity of EntityInterface
, making it hard to do tests with them.
However, wrapped entities use the facade pattern, which hides all the complexity of EntityInterface, leaving a clean object that is much easier to test and mock.
Repositories? Active Record pattern?
It is very convenient to use the Active Record pattern this way:
$owner = $book->owner();
But, if not implemented well, this pattern has its drawbacks in terms of testing the code. Fortunately, there are some solutions, and one of them is to pass as a dependency the repositories that the entity will use to get the related entities. This has a drawback: if the Book entity is related with many other entity types, then we will have a lot of dependencies (one for each of the repositories that allow obtaining those entities)
$bookRepository = \Drupal::service('model.book.repository');
$authorRepository = \Drupal::service('model.author.repository');
$libraryRepository =\Drupal::service('model.library.repository');
$book = new Book(
$bookRepository,
$authorRepository,
$libraryRepository
);
$author = $book->author();
$library = $book->library();
Typed Entity module solves this problem using a “Repository Manager”, which becomes a single dependency and is in charge of wrapping an entity with its wrapped entity facade object.
class WrappedEntityBase {
...
public function wrapReference(string $field_name): ?WrappedEntityInterface {
$target_entity = $this->getEntity()->{$field_name}->entity;
return $this->repositoryManager()->wrap($target_entity);
}
public function owner(): ?WrappedEntityInterface {
$owner_key = $this->getEntity()->getEntityType()->getKey('owner');
if (!$owner_key) {
return NULL;
}
return $this->wrapReference($owner_key);
}
...
}
We can see an example of this in the WrappedEntityBase
base class, that has an owner()
method that makes use of the repository manager to obtain the entity. This, in turn, uses a wrapReference()
utility method that wraps an entity related through any EntityReferenceField
.
Note: At the time of writing, in the Typed Entity module, the repository manager is not an injected dependency and is being obtained through a .module function. I think it could be because most of the module tests are Kernel tests, and this way things are simpler.
Although the owner()
function example is a bit confusing (because it uses the EntityType
definition element to get the field name), in our custom entities we can do something as simple as this:
class Book extends WrappedEntityInterface {
public const FIELD_LIBRARY = 'field_library'
public function library(): ?WrappedEntityInterface {
return $this->wrapReference($self::FIELD_LIBRARY);
}
}
Advanced use of repositories and Query Objects
If we want to get our wrapped entities from our repositories using a query analogous to how we would do it with the Entity Query API, we start to see an old known repositories problem: the repository interface starts to be cluttered with many findBySomething() methods.
To avoid this, in my “custom adventure” (without using Typed Entity) I was able to implement the Query Object pattern, so that a “query object” is prepared and then passed as a parameter to a repository. This is a much smoother way of working, as it can be seen in the following example:
// Given this library wrapped entity (it does not matter now how it's created).
$library = ...
// And a featured book.
$featuredBook = ...
// We can get all the other books from the library
$otherBooks = $this->bookRepository->get(
BookQuery::new()
->relatedWith($library)
->notEquals($featuredBook)
->sort('DESC', Book::FIELD_NAME_DATE)
->range(0, 10)
);
The QueryBase
class (from which BookQuery
extends) has several utility methods that can be used to communicate very expressively with repositories, giving them precise selection, sorting and paging instructions.
Here are some examples:
Identity:
idIn(array $entityIds)
idNotIn(array $entityIds)
Relations:
relatedWith(WrappedEntity $entity)
Perhaps this method is the most curious, since it uses the entity’s field definitions to find automatitically the entity reference fields that relate to the entities of the concrete WrappedEntity child passed as parameter.
Dates:
current()
past()
future()
fromDate(DrupalDateTime $date)
toDate(DrupalDateTime $date)
betweenDates(DrupalDateTime $from, DrupalDateTime $to)
(current(), past() and future()
are possible by using a unique date range field for all entities):
Result control:
sort(string $direction = 'ASC', ?string $fieldName = NULL)
range(?int $start = NULL, ?int $length = NULL)
We aren’t limited to these base query object utility methods. We can extend the BaseQueryObject with a BookQueryObject that could have custom entity specific conditions.
$bestSellers = $this->bookRepository->get(
BookQuery::new()
->purchasedAtLeastNtimes(100)
);
Conclusion
As you can see, the use of query objects is a great advance in clarity and expressiveness of the code, although it does not solve all cases. For those special cases, we can always continue using a getBySomething() in the repository.