When loading or interacting with entities in Drupal 8, we often use the EntityTypeManagerInterface
interface, which is the brains behind the entity_type.manager
service that is provided in many of the Drupal core base classes.
This often appears in one of the following ways:
\Drupal::service('entity_type.manager')->getStorage('node');
$this->entityTypeManager->getStorage('node');
Either approach returns an instance of EntityStorageInterface
. Each entity type can define a class that extends EntityStorageBase
and adds additional custom methods that are applicable to a given entity type.
The node
entity type uses this pattern in \Drupal\node\NodeStorage
to provide many of its commonly used methods such as revisionIds()
and userRevisionIds()
.
The benefits of adding custom storage methods becomes more apparent when you begin to work with custom entities. For example, if you have a recipe
entity type, you could have a loadAllChocolateRecipes()
method that abstracts the query and conditions needed to load a subset of Recipe entities.
The resulting call would look like this:
/* @var $recipes \Drupal\recipe_module\Entity\Recipe[] */
$recipes = $this->entityTypeManager
->getStorage('recipe')
->loadAllChocolateRecipes();
A custom storage handler class is integrated with an entity via the annotated comments in the entity class.
\Drupal\recipe_module\Entity\RecipeEntity
/**
* Define the Recipe entity.
*
* @ContentEntityType(
* id = "recipe",
* label = @Translation("Recipe"),
* handlers = {
* "storage" = "Drupal\recipe_module\RecipeStorage",
…
Then in the storage handler class, custom methods can be added and existing methods can be overridden as needed.
/**
* Defines the storage handler class for Recipe entities.
*/
class RecipeStorage extends SqlContentEntityStorage {
/**
* Load all recipes that include chocolate.
*
* @return \Drupal\example\Entity\Recipe[]
* . An array of recipe entities.
*/
public function loadAllChocolateRecipes() {
return $this->loadByProperties([
'field_main_ingredient' => 'chocolate',
]);
}
Manual SQL queries can also be performed using the already provided database connection in $this->database
. Explore the Drupal\Core\Entity\Sql\SqlContentEntityStorage
class to see the many properties and methods that you can override or leverage in your own methods.
Again, the NodeStorage
and TermStorage
offer many great examples and will demystify how many of the “magic” methods on these entities work behind the scenes.
For example, if you ever wondered how the Term::nodeCount()
method works, this is where the magic happens.
\Drupal\taxonomy\TermStorage
/**
* {@inheritdoc}
*/
public function nodeCount($vid) {
$query = $this->database->select('taxonomy_index', 'ti');
$query->addExpression('COUNT(DISTINCT ti.nid)');
$query->leftJoin($this->getBaseTable(), 'td', 'ti.tid = td.tid');
$query->condition('td.vid', $vid);
$query->addTag('vocabulary_node_count');
return $query->execute()->fetchField();
}
The next time you need to write a method that returns data specific to an entity type, explore the use of a storage handler. It beats stuffing query logic into a custom Symfony service where you are likely violating single responsibility principles with an overly broad class.
This potentially removes your dependency on a custom service, removing the need for extra dependency injection and circular service dependencies. It also adheres to a Drupal core design pattern, so it is a win, win, win, or something like that.