Creating and Using Entity Storage Methods

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.