Use Typed Entity as a namespace for your business logic. It provides a scope to place your business logic, and help you keep your global scope clean of myriads of small functions.

This module provides a simple way to treat you existing entities like typed objects. This will allow you to have a more maintainable and easier to debug codebase.

Lullabot logo

Lullabot-sponsored project

This module is sponsored by Lullabot, and we keep a special eye on its maintenance. Read the Architecture Decision Record on why we use this module.

Make sure to check the example module to get inspiration on how to implement this on your code base.

Why this, instead of core's bundle classes

Starting in Drupal 9.3 you can use custom bundle classes in Drupal core. However this approach falls short in several scenarios.

Read this article for more details.

Usage

Whenever you find yourself writing custom code that accesses $node->field_lorem anywhere in your site (let's say a hook for the example) you can benefit from a wrapped entity. A wrapped entity is a custom class that encapsulates the code for a particular content type. The goal is that your hook no longer does $node->field_foo, but instead $article->doTheThingDependingOnFoo().

For each entity type and bundle you need to let the system know that you want a custom class for your entities of a certain type. For that you will register a plugin in your custom module under src/Plugin/TypedRepositories. Like this example:

/**
 * The repository for articles.
 *
 * @TypedRepository(
 *   entity_type_id = "user",
 *   wrappers = @ClassWithVariants(
 *     fallback = "Drupal\my_module\WrappedEntities\User",
 *   ),
 *   description = @Translation("Repository that holds business logic applicable to all users.")
 * )
 */
final class UserRepository extends TypedRepositoryBase {}

With that you can start writing and testing Drupal\typed_entity_example\WrappedEntities\User. Now everytime you need to do things with a user in a hook (or elsewhere) you can do:

// You can inject the Repository Manager service in your services as well.
$user = typed_entity_repository_manager()->wrap($user_entity);
$user->doThatThingTheProjectNeeds();

Variants

We know that often times the business logic depends on content. It does as well in our projects! Typed Entity has the concept of variants for that. Let's change the example above to add the variants key:

/**
 * The repository for articles.
 *
 * @TypedRepository(
 *   entity_type_id = "user",
 *   wrappers = @ClassWithVariants(
 *     fallback = "Drupal\my_module\WrappedEntities\User",
 *     variants = {
 *       "Drupal\my_module\WrappedEntities\ModeratorUser"
 *     }
 *   ),
 *   description = @Translation("Repository that holds business logic applicable to all users.")
 * )
 */
final class UserRepository extends TypedRepositoryBase {}

So we can now call syncWithComplicated3rdPartyModerationService and keep that logic encapsulated and testable.

$repository_manager = typed_entity_repository_manager();
$article = $repository_manager->wrap($node);
$user = $repository_manager->wrap($user_entity);
if ($user implements ModeratorUserInterface && $article implements ModeratableInterface) {
  $user->syncWithComplicated3rdPartyModerationService($article, ModeratableInterface::PUBLISHED);
}

How does Typed Entity know when to give you a User or a ModeratorUser? All variants declared in the annotation need to implement VariantInterface, which means that they will have a method called applies(). The first class to return TRUE in that method gets used. If none applies, the fallback is used.

We encourage you to use Renderers

Renderers are a very useful and flexible way of encapsulating business logic for rendering an entity. See this example to learn how to declare a renderer.

/**
 * The repository for articles.
 *
 * @TypedRepository(
 *   entity_type_id = "node",
 *   bundle = "article",
 *   wrappers = @ClassWithVariants( ... ),
 *   renderers = @ClassWithVariants(
 *     fallback = "Drupal\my_module\Render\Article\Base"
 *     variants = {
 *       "Drupal\my_module\Render\Article\Summary",
 *     }
 *   ),
 *   description = @Translation("Repository that holds business logic applicable to all articles.")
 * )
 */
final class ArticleRepository extends TypedRepositoryBase {}

You can use the fallback key in your typed repository plugin to use a particular renderer if no other renderer is applicable. You can implement the applies() method in renderers as well, but if your variants are only for the view mode then use the VIEW_MODE constant in your renderer (like this).

When you have (1) declared your renderer, and (2) specified your VIEW_MODE / applies(), you can proceed to implement the renderer methods. In most cases you will:

  • Add preprocess logic to prepare the data for the Twig template. Use preprocess().
  • Alter the render array for the associated entity. Use viewAlter().
  • And more! Check the TypedEntityRendererInterface.

Typed Entity UI

Version 4.x of the module comes with the Typed Entity UI submodule. This will simplify debugging any gotchas in typed entity.

Go to the /admin/config/development/typed-entity to explore the definitions in a visual way.

Typed Entity UI

Upgrading from 3.x to 4.x?

Make sure to take a look to the Change Records we created to make the upgrade easier.

Supporting organizations: 
Development and maintenance
Initial development sponsor

Project information

Releases