In D7, if you want to add any non-field mechanics to entities, you have to implement a number of modules, including hook_form_alter(), and stuff like hook_node_update(), etc.
Within these hooks, you need to examine the given entity and check whether your mechanic is actually enabled for this entity bundle.
This all kind of works, but it is ugly and fragile.

Examples:
- Url aliases and redirects
- Menu items for nodes
- Workflow
(I'm sure there are more examples)

-------------

My proposal:

Entity behaviors:
Modules can attach behavior objects to an entity type. Those behavior objects can subscribe to a number of events related to this entity type.
Especially, they can
- do something if an entity is created, updated, etc.
- do something if a bundle for this entity type is created, updated, etc.
- declare bundle options (see below)

Bundle behaviors:
Modules can attach behavior objects to an entity bundle. Those behavior objects can subscribe to a number of events related to this entity bundle.
Especially, they can
- provide form elements on the entity edit form, which can be rearranged on the "Manage fields" screen for this bundle.
- provide display elements on entity view modes, which can be rearranged on the "Manage display" screen for this bundle.
- do something if the entity is saved, created, deleted, etc.

The cool thing is, the same behavior class can be reused for different entity types.
E.g. there could be a "BundleBehavior\\UrlAlias", which can be attached to taxonomy terms and to nodes, as long as the "EntityBehavior\\UrlAlias" is attached to both nodes and taxonomy terms.

Also, more than one instance of the same behavior could be added to a bundle.

So far this is all code, no configuration via UI.
Any UI configuration stuff either needs to be provided by the module itself, or preferably use the "bundle option".

Bundle options:
Modules, and entity behaviors, can register "bundle options" to an entity type. Bundle options
- provide form elements on the bundle configuration form
- provide a storage for those bundle options.
- can attach behaviors to those bundles, based on the stored option values.

-------------------

Yes, this proposal is still a bit vague, and a lot needs to be ironed out.
However, I think it is generally a reasonable direction to take.

One design considerations:
I intentionally want to split the behavior from its configuration UI. This allows to attach multiple behavior objects and maybe some other stuff, based on only one configuration checkbox. Also, this split will help to keep classes smaller.

-------------------

Related:
#1346214: [meta] Unified Entity Field API
#1803064: Horizontal extensibility of Fields: introduce the concept of behavior plugins
#1818680: Eliminate hook_field_extra_fields() / Redesign field UI architecture

Comments

joachim’s picture

I really like this idea.

I'm not sure I entirely understand this bit:

> I intentionally want to split the behavior from its configuration UI.

As I see it, what a behaviour system needs to do is:

- store that a particular behaviour type is present on entity X bundle Y
- store the options for that instance of the behaviour type
- let the behaviour type add something to the entity form

Which to me looks very much like how field types and field instances work. Just without the actual field database table.

I might see about doing something around this on D7, as I've been pondering how to make http://drupal.org/project/fragment more flexible.

donquixote’s picture

#1:
Example:
You want a behavior that gives "karma" to a user every time he/she views a node.
You want this to be configurable per node type.

Later you want the same behavior to apply to commerce products.

Bundle behavior classes

First, you create a behavior class:

namespace Drupal\my_module\BundleBehavior;
class EntityViewKarmaCounter {

  protected $increment;

  function __construct($increment) {
    $this->increment= $increment;
  }

  /**
   * Subscriber method
   */
  function entityWasViewed($entity, $type, $bundle, $user) {
    $user->karma += $this->increment;
    .. // somehow save the new karma value
  }
}

This thing really knows nothing about where the $increment is configured, and why or how it is subscribed to those bundle events.
It does not need to think about "am I really enabled for this bundle?". Because if it wasn't, the subscriber method would not fire.

As if this wasn't good enough, you decide to add an alternative implementation, which has a different formula for the karma:

namespace Drupal\my_module\BundleBehavior;
class EntityViewKarmaCounterUnfair {
  function entityWasViewed($entity, $type, $bundle, $user) {
    // More important users get more karma per view. We like to be unfair!
    $user->karma += count($user->roles);
    .. // somehow save the new karma value.
  }
}

This behavior does not even have constructor arguments.

Bundle option code

Now, separate from the behavior classes, you create the option code.
I don't know yet whether this should be one class, or a collection of classes, or hooks, or whatever.

The important part is that it should happen outside of the behavior code.

The option code does this:

  • Define a configuration form. This has
    - radios where you can choose one of the above karma formulas, or disable the karma counter.
    - a number field to define the increment.
    The configuration form elements will be displayed in the bundle configuration form, for entity types that have this option.
  • Define how to store those options (per bundle).
    E.g. it tells the system that it wants to store two numbers per bundle: One for the radios, another for the karma increment.
namespace Drupal\my_module\BundleOption
class EntityViewKarmaCounter {

  protected $allowUnfairIncrement

  /**
   * @param boolean $allow_unfair_increment
   *   The unfair increment is only available on some entity types, not all.
   */
  function __construct($allow_unfair_increment) {
    $this->allowUnfairIncrement = $allow_unfair_increment;
  }

  /**
   * Callback method for form building
   */
  function buildFormElements(...) {
    ...
    if ($this->allowUnfairIncrement) {
      ... // add one more radio option
    }
  }

  function formSubmit(..) {
    ... // prepare for storage
  }

  /**
   * Callback method for storage definition
   */
  function getStorageDefinition() {
    ... // define that we need two integer slots.
  }
}

Entity behavior class

This class does the following:

  • Tell the system about the option class you created.
  • Based on the configuration for the bundle, it decides which behaviors it wants to attach.
namespace Drupal\my_module\EntityTypeBehavior;
class EntityViewKarmaCounter {

  protected $allowUnfairIncrement

  function __construct($allow_unfair_increment) {
    $this->allowUnfairIncrement = $allow_unfair_increment;
  }

  /**
   * Subscriber method, which allows to register bundle options.
   */  
  function registerBundleOptions($api) {
    $api->registerBundleOption(new Drupal\my_module\BundleOption\EntityViewKarmaCounter($this->allow_unfair_increment));
  }

  /**
   * Subscriber method which fires when a bundle ("entity subtype") is initialized in a request.
   * @param $bundle
   *   Object representing the bundle ("entity subtype"). 
   * @param $bundle_config
   *   Configuration for this bundle. Options coming from multiple "bundle option" components.
   */
  function initEntityBundle($bundle, $bundle_config) {
    switch ($bundle_config['karma_counter']['counter_type']) {
      case KARMA_COUNTER_FIXED_INCREMENT:
        $behavior = new Drupal\...\BundleBehavior\EntityViewKarmaCounter($bundle_config['karma_counter']['increment']);
        break;
      case KARMA_COUNTER_UNFAIR_INCREMENT:
        $behavior = new Drupal\...\EntityViewKarmaCounterUnfair();
        break;
      default:
        // Behavior is disabled.
    }
    $bundle->attachBundleBehavior($behavior);
  }
}

Note how this behavior class is agnostic about the entity type.

Register the entity behavior

Now you need to tell Drupal about the entity type behavior, and which entities it should be available for.

/**
 * Implements hook_entity_type_behaviors()
 */
function my_module_entity_type_behaviors($api, $entity_type) {
  switch ($entity_type) {
    case 'node':
    // For nodes, we allow the unfair option.
    $api->registerEntityTypeBehavior(new Drupal\...\EntityTypeBehavior\EntityViewKarmaCounter(TRUE));
    break;
  case 'commerce_product':
    // For products, we don't allow the unfair option.
    $api->registerEntityTypeBehavior(new Drupal\...\EntityTypeBehavior\EntityViewKarmaCounter(FALSE));
    break;
  }
}

Of course we may decide not to make this a hook, but something else. Too early to decide that.

Our custom module now has
- no hardcoded entity types outside of hook_entity_type_behaviors().
- no hardcoded bundle names anywhere.
- no complex logic to determine if something applies to a given bundle type.

andypost’s picture

Probably this feature will go to D9

andypost’s picture

Version: 8.0.x-dev » 9.x-dev
Status: Active » Postponed
catch’s picture

Status: Postponed » Closed (works as designed)

We have hook_ENTITY_TYPE_$op() which allows for a single place to hard-code entity types if needed. There are also third party settings on bundle entity config. Between these I think it's possible to do everything from the OP in 8.x, so closing as 'works as designed'.

donquixote’s picture

I don't know if what we have today really covers what I was proposing in this issue.

But anyway, I think I rather open a new issue when I have a more concrete idea what I want, and a better way to explain it. I don't even want to read my own lengthy post anymore.

Version: 9.x-dev » 9.0.x-dev

The 9.0.x branch will open for development soon, and the placeholder 9.x branch should no longer be used. Only issues that require a new major version should be filed against 9.0.x (for example, removing deprecated code or updating dependency major versions). New developments and disruptive changes that are allowed in a minor version should be filed against 8.9.x, and significant new features will be moved to 9.1.x at committer discretion. For more information see the Allowed changes during the Drupal 8 and 9 release cycles and the Drupal 9.0.0 release plan.