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

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.

#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:

<?php
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:

<?php
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.

<?php
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.

<?php
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.

<?php
/**
* 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.

Probably this feature will go to D9