Problem/Motivation
Content types are essentially business objects, but we have no standard ability to modify or subclass them to add business logic, which therefore gets scattered among helper services.
This issue introduces the concept of "bundle classes" which are subclassing the current entity classes such as \Drupal\node\Entity\Node
. A bundle class is defined using the class
property of the entity type bundle info, so modules can define bundle classes for their own entities using hook_entity_bundle_info()
and can alter bundle classes of entities defined in other modules with hook_entity_bundle_info_alter()
.
For example a custom module could provide a bundle class for a node type like this:
use Drupal\mymodule\Entity\BasicPage;
/**
* Implements hook_entity_bundle_info_alter().
*/
function mymodule_entity_bundle_info_alter(&$bundles) {
$bundles['node']['page']['class'] = BasicPage::class;
}
The bundle classes themselves extend the entity class:
namespace Drupal\mymodule\Entity;
use Drupal\node\Entity\Node;
class BasicPage extends Node implements BasicPageInterface {
// Implement whatever business logic specific to basic pages.
public function getBody() {
return $this->get('body')->value;
}
}
The entity type manager will always return bundle classes if possible, so we can rely on the data types rather than having to call $entity->bundle()
:
$entity = $this->entityTypeManager->getStorage('node')->load($id);
if ($entity instanceof BasicPageInterface) {
// Some logic which applies only to basic pages.
)
Remaining tasks
Reviews
User interface changes
None
API changes
None
Data model changes
None
Original summary by larowlan
Content types are essentially business objects, but we have no standard ability to modify or subclass them to add business logic, which therefore gets scattered among helper services.
We can't safely modify the defaults under the current system, because we have lots of places where entity storage determines the entity class using $this->entityClass
. If something like Entity Bundle Plugin were to try and sub-class sql content entity storage it would need to override each of these calls.
Example use case
Details generalized from a real past project: I'm building a website for a popular trading card game. Each trading card has its own page, displaying its type (character, trainer, or special), properties such as hit points and attack strength, and (in the case of characters) evolution chain. I want to be able to encode specialized business logic in entity class methods I can call on the theme layer and elsewhere to do things like getting the node ID of the next card in a series or generating an evolution chain. I want this logic centralized in the business object--not scattered across integrating subsystems--as a matter of code quality and DX. To illustrate the difference, if I can extend Node
with CharacterTradingCardNode
and add an evolutionChain()
method right on it, I can embed the business logic right in the class and directly access it anywhere I have that type of node, such as a Twig file. If not, I have to create a separate service for the business logic and inject its output into the context I want to use it in.
Pseudocode with Subclassing (The Proposal)
<!-- node-character_trading_card.html.twig -->
<ul>
{% for evolution in node.evolutionChain %}
<li>{{ evolution.id }}</li>
{% endfor %}
</ul>
Pseudocode without Subclassing (The Present)
# mytheme.theme
function mytheme_preprocess_node(&$variables) {
if ($variables['node'] && $variables['node']->bundle() === 'character_trading_card') {
$evolution_chain_generator = \Drupal::service('mymodule.evolution_chain_generator');
$variables['evolution_chain'] = $evolution_chain_generator->generateChain($variables['node']);
}
}
<!-- node-character_trading_card.html.twig -->
<ul>
{% for evolution in evolution_chain %}
<li>{{ evolution.id }}</li>
{% endfor %}
</ul>
Proposed resolution
Put access to the entity class behind a protected method, passing the bundle when available.
This is generally considered better for refactoring too.