0

Is there any object oriented Drupal way to add a base field definition to an existing entity?

Of course I am familiar with hook_entity_base_field_info(), but I am trying to keep my .module file size to a minimum and I was wondering if there is a way to achieve that with a different approach.

I thought of decorating or extending the entity, but after some research, I stumbled upon these issues that suggest those two options aren't yet possible or elegant, as far as I understand.

Am I wrong in that?

Is there anyone that has had the same case and found a different approach?

Edit: TL;DR answer is that trying to replace this specific hook, will cause more confusion, rather than improve code clarity.

2
  • 1
    Why did you pick this hook out of the hundreds of hooks in Drupal? This would be quite chaotic if you want to replace all hooks by OOP. The correct approach would be to change EntityFieldManager not to invoke Drupal hooks and instead dispatch Symfony events. But don't do this on your own. This only makes sense in a core effort, so that all modules use the same API. – 4k4 Apr 9 at 20:20
  • Yeah, I see there is a trend in trying to replace hooks with events in core.. like the entity insert, update, presave, etc hooks. I guess then, trying to replace this specific hook with something else, would actually cause more harm and confusion, than clarity. – M.Anagnostopoulos Apr 10 at 18:42
2

Without using any hook, it's not possible, since to change the class used for an entity, it's necessary to implement hook_entity_type_alter(), which is problematic to implement when more modules need to alter the entity definition, for example changing the same entity handler, or the entity class, for the same entity type.

Imagine two different modules that try to add two different fields to the User entity by replacing the entity class used for it. For example, the first module could use the following code.

/**
 * Implements hook_entity_type_alter().
 */
function login_id_entity_type_alter(array &$entity_types) {
  $entity_types['user']->setClass(LoginIdUser::class);
}
namespace Drupal\login_id\Entity;

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\user\Entity\User;

class LoginIdUser extends User {

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
    $fields = parent::baseFieldDefinitions();

    $fields['login_id'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Login ID'))
      ->setDescription(t('The ID used for the login credentials.'))
      ->setRequired(TRUE)
      ->setConstraints([
        'LoginId' => [],
        'LoginIdUnique' => [],
    ]);

    $fields['name']->getItemDefinition()
      ->setClass('\\Drupal\\login_id\\LoginIdItem');

    return $fields;
  }

}

The second module could use the following code.

/**
 * Implements hook_entity_type_alter().
 */
function admin_email_entity_type_alter(array &$entity_types) {
  $entity_types['user']->setClass(AdminEmailUser::class);
}
namespace Drupal\admin_email\Entity;

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\user\Entity\User;

class AdminEmailUser extends User {

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
    $fields = parent::baseFieldDefinitions();

    $fields['admin_mail'] = BaseFieldDefinition::create('email')
      ->setLabel(t('Admin email'))
      ->setDescription(t('The email used from administrator users to contact the user.'))
      ->setDefaultValue('')
      ->addConstraint('UserMailUnique')
      ->addConstraint('UserMailRequired')
      ->addConstraint('ProtectedUserField');

    return $fields;
  }

}

With two modules implementing that code, only a field would be added to the entity. To get both the entity fields, LoginIdUser should extend AdminEmailUser, which would mean that a module should have the other one as dependency.
This assumes that admin_email_entity_type_alter() is executed before login_id_entity_type_alter(). This is what normally happens when the hook execution order isn't altered by a hook_module_implements_alter() implementation, or when the weight associated to module isn't altered by a call to module_set_weight(). In the case login_id_entity_type_alter() is invoked before admin_email_entity_type_alter(), for example because a module implements hook_module_implements_alter() and changes the order of those hook implementations, AdminEmailUser should extend LoginIdUser.

Implementing hook_entity_base_field_info() allows to modules to add different fields without interfering with each other.

As side note, hooks can be placed in a file different from a .module file, which is automatically loaded from Drupal. Which files are loaded from Drupal when looking for hooks is influenced by hook_hook_info(). With the implementations of that hook done from Drupal core modules, the files Drupal core looks for are the following.

  • <module_name>.tokens.inc
  • <module_name>.views.inc

If you are interested in reducing the size of the .module file, implementing hook_hook_info() to tell Drupal where your module hooks are could be a way to achieve it.

1

You could override the class being used for the entity type, and add your base field definitions there.

For example, if you wanted to add a base field definition to the Node entity, you could do this:

namespace Drupal\[EXAMPLE]\entity;

use Drupal\node\Entity\Node as NodeBase;

class Node extends NodeBase {

  /**
   * {@inheritdoc}
   */
  public function baseFieldDefinitions() {
    $definitions = parent::baseFieldDefinitions();
    // Add your field definition here

    return $definitions;
  }

}

Then you would override the class used as the backend for nodes, overriding the default node class with your class that extends it:

/**
 * Implements hook_entity_type_alter().
 */
function HOOK_entity_type_alter(array &$entity_types) {
  $node = &$entity_types['node'];
  $node->setClass('Drupal\[EXAMPLE]\Entity\Node');
}

I would suggest that if you are creating a contributed module to be used across projects, you're better off using a hook to add your field definition. This method should only be used for projects in which you're able to control the inheritance/hierarchy of the objects backing an entity type.

4
  • Yes I thought of that as well and it would be ok to do this in your custom project's module etc, but would that not be bad to exist in a contrib module since it would not be compatible with other modules doing the same thing? Or do i get that wrong? – M.Anagnostopoulos Apr 9 at 19:22
  • It could. If you found on your system that a different module had extended the Node object, you would then need to extend that instead if you wanted to maintain the changes made by that module. Of course if multiple modules did this, you'd run into issues. Hence hooks, which don't require overriding the Node class. It's really a matter of which best suits your needs and preferred architectural type. – Jaypan Apr 9 at 20:28
  • 2
    Replacing the node class would make sense for a module which advertises to implement a new enhanced node with a whole set of new features so that it's clear if you install two of that kind there will be a conflict. But not for a module which just wants to add a base field. – 4k4 Apr 10 at 9:53
  • That's a good point. – Jaypan Apr 11 at 23:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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