14

In Drupal 8 you can load an entity with:

$node = \Drupal::entityManager()->getStorage('node')->load(123);

This looks for the entity definitions and finds that node is defined by Drupal\node\Entity\Node - so (I guess) Drupal\node\NodeStorage will instantiate a new Drupal\node\Entity\Node instance.

What I'd like to achieve is subclassing Drupal\node\Entity\Node and being able to instantiate this subclass when it's appropriate. For example if I have a node bundle article, there would be a class:

namespace Drupal\my_module\Entity\Article;
class Article extends Drupal\node\Entity\Node {
}

And I'd call:

$node = \Drupal::entityManager()->getStorage('node_article')->load(123);

And the return would be my Article subclass.

I can achieve this by creating a new entity type and wiring it back to another existing entity definitions, for example the node-article example would be this class:

namespace Drupal\my_module\Entity;
use Drupal\node\Entity\Node;
/**
 * @ContentEntityType(
 *   id = "node_article",
 *   label = @Translation("Content"),
 *   bundle_label = @Translation("Content type"),
 *   handlers = {
 *     "storage" = "Drupal\node\NodeStorage",
 *     "storage_schema" = "Drupal\node\NodeStorageSchema",
 *     "view_builder" = "Drupal\node\NodeViewBuilder",
 *     "access" = "Drupal\node\NodeAccessControlHandler",
 *     "views_data" = "Drupal\node\NodeViewsData",
 *     "form" = {
 *       "default" = "Drupal\node\NodeForm",
 *       "delete" = "Drupal\node\Form\NodeDeleteForm",
 *       "edit" = "Drupal\node\NodeForm"
 *     },
 *     "route_provider" = {
 *       "html" = "Drupal\node\Entity\NodeRouteProvider",
 *     },
 *     "list_builder" = "Drupal\node\NodeListBuilder",
 *     "translation" = "Drupal\node\NodeTranslationHandler"
 *   },
 *   base_table = "node",
 *   data_table = "node_field_data",
 *   revision_table = "node_revision",
 *   revision_data_table = "node_field_revision",
 *   translatable = TRUE,
 *   list_cache_contexts = { "user.node_grants:view" },
 *   entity_keys = {
 *     "id" = "nid",
 *     "revision" = "vid",
 *     "bundle" = "type",
 *     "label" = "title",
 *     "langcode" = "langcode",
 *     "uuid" = "uuid",
 *     "status" = "status",
 *     "uid" = "uid",
 *   },
 *   bundle_entity_type = "node_type",
 *   field_ui_base_route = "entity.node_type.edit_form",
 *   common_reference_target = TRUE,
 *   permission_granularity = "bundle",
 *   links = {
 *     "canonical" = "/node/{node}",
 *     "delete-form" = "/node/{node}/delete",
 *     "edit-form" = "/node/{node}/edit",
 *     "version-history" = "/node/{node}/revisions",
 *     "revision" = "/node/{node}/revisions/{node_revision}/view",
 *   }
 * )
 */
class Article extends Node { }

// Results my Article sub type.
$node = \Drupal::entityManager()->getStorage('node_article')->load(123);

This works fine (as much as I can see); however, it smells. It adds a new entity type, which is not true, and might cause other troubles in the future.

How do I define a subclass for an entity bundle so that loading the entity would return an object of that class?

2
  • 2
    I'm not sure you can provide a different entity class per bundle; you can use hook_entity_type_alter() to make the change more cleanly, but I don't know how you'd limit that to a specific bundle
    – Clive
    Commented Sep 10, 2015 at 7:44
  • Thank you Clive - that looks a promising hook to investigate!
    – itarato
    Commented Sep 10, 2015 at 8:03

5 Answers 5

17

Create a new class in your module that extends \Drupal\node\Entity\Node.

use Drupal\node\Entity\Node as BaseNode;

class MyNode extends BaseNode {
}

Implement hook_entity_type_build().

use Drupal\Core\Entity\EntityTypeInterface;

/**
 * @param EntityTypeInterface[] $entity_types
 */
function my_module_entity_type_build(&$entity_types) {
  if (isset($entity_types['node'])) {
    $entity_types['node']->setClass('Drupal\my_module\Entity\MyNode');
  }
}

Remember to rebuild the cache.

It works fine when loading nodes via the entity type manager service and the node storage. It even works when you just use Drupal\node\Entity\Node::load($nid) thanks to the fact that this load() function it is just a static wrapper for the entity type manager service call provided by the Entity class which is extended from the Node class.

// Part of Entity class for reference
abstract class Entity implements EntityInterface {
  /**
   * Loads an entity.
   *
   * @param mixed $id
   *   The id of the entity to load.
   *
   * @return static
   *   The entity object or NULL if there is no entity with the given ID.
   */
  public static function load($id) {
    $entity_manager = \Drupal::entityManager();
    return $entity_manager->getStorage($entity_manager->getEntityTypeFromClass(get_called_class()))->load($id);
  }
}

This also works fine with the soon-to-be-removed entity_load_multiple() function, so I guess this covers all standard use cases for loading nodes.

Of course, if your module does this and another module does the same, you'll have a problem, but I guess it's not common scenario, and it makes sense only for very specific use cases.

6
  • 2
    Sorry but no :) The question was to have a different class per bundle. You're changing the class for all bundles of the entity type node. That's not the same.
    – Berdir
    Commented May 18, 2016 at 18:04
  • @Berdir, your right :( ... Having classes per bundle means that a entity storage for node would have to be extended too so that it's load methods could be overridden to produce those per bundle classes. Which basically is one huge headache. Commented May 18, 2016 at 19:52
  • 1
    Yeah. drupal.org/node/2570593 is one of the issues that I referenced but forgot to actually link to in my answer.
    – Berdir
    Commented May 18, 2016 at 21:37
  • I have implemented hook_entity_type_alter to set custom class. It works too.
    – Yenya
    Commented Jul 19, 2016 at 11:04
  • When I attempt this method I get a 'Drupal\Component\Plugin\Exception\PluginNotFoundException: The "node" entity type does not exist.' message. I'm using the ablecore_entity_type_build function in my .module file. And I have my AbleNode.php in /src/Entity/AbleNode/ folder
    – Amy
    Commented Oct 11, 2017 at 17:15
6

From Drupal 9.3, there is support for entity bundle classes included in core.

So now you can subclass your entity bundles as much as you like without needing any contrib modules.

The core change record is extensive, but basically this:

use Drupal\mymodule\Entity\BasicPage;

function mymodule_entity_bundle_info_alter(array &$bundles): void {
  if (isset($bundles['node']['page'])) {
    $bundles['node']['page']['class'] = BasicPage::class;
  }
}
1
  • 1
    This is the right answer since 9.3.
    – dxvargas
    Commented Dec 9, 2022 at 0:34
3

I experienced the same issue and decided to create a module that alter the entity type class of Drupal entities via the plugin system. Currently it supports altering the Node, User and File entity classes. When altering the Node entitiy you are able to alter the type class per node bundle.

Check out the module description for an example:

https://www.drupal.org/project/entity_type_class

The module uses hook_entity_type_alter() to set a handler class on entities that you provide in your plugin annotation.

3

This module also gives you real bundle classes - https://github.com/amcgowanca/discoverable_entity_bundle_classes

1

This is an old question, but the real answer should be:

If you need different behaviour across bundles, then you should be using different entity types, not different bundles.

Custom Content Entities are 1st class citizens in D8. In fact we estimate that it takes about 30 minutes to get a new custom content entity to the level that node is (which really just comes down to adding the Form UI to get the nice side panel, and the alias/revision fields.) This doesn't include adding the translation pages, but that isn't that much more.

If you haven't seen it, take a look at the Drupal Console's generate:custom:entity features.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.