Entity Bundle Plugin

After weeks of work Commerce License is finally up, as well as Commerce File 7.x-2.x to go along with it.

Commerce License provides a framework for selling access to local or remote resources.

In practice, this means that there’s a license entity, usually created during order checkout, that holds information about accessing the purchased resource, and it has a status and an optional expiration date.
This allows selling access to anything from files to node types, or perhaps ZenDesk tickets and accounts on remote sites, all using a common API, while always having a record of the purchased access.

At the heart of that API is the entity bundle plugin, which allows different license types to have different logic.
What is entity bundle plugin? The project page says only this:

This API module allow developers to build an entity type which is attached to strong behaviors.

That doesn’t help much, so let’s dive in. Let’s start by looking at how entities are built on D7.

The core way.

$license = new stdClass();

That’s it. Core entities are anonymous objects with no defined properties and no methods. They have no logic whatsoever.

You can’t even determine their type, which creates the need to pass the entity type separately everywhere the entity is used.

The Entity API way.

Entity API allows entities to be classed objects. It provides a base Entity class
which is extended by the class for your own entity type.

function license_entity_info() {
  $return = array(
    'license' => array(
      'label' => t('License'),
      'label callback' => 'entity_class_label',
      'entity class' => 'License',
      'controller class' => 'EntityAPIController',
      // other keys here, such as "entity keys"
     ),
  );
  return $return;
}

And then our entity class looks like this:

class License extends Entity {
  public $id;
  public $title;
  public $created;
  public $changed;
  public $uid;
}

Now when I do entity_load_single(‘license’, 1), I get an object that is an instance of the License class. The entity base class provides some useful methods such as $license->entityType(), $license->bundle(), $license->label(), $license->save() and $license->delete(). All of our properties are explicitly declared, allowing for autocomplete.

This is already a big improvement, and the Entity API in Drupal 8 core has developed in a similar way.

The entity bundle plugin use case

Now let’s go ahead and extend our license class. We want to add a method that will return the access details for a resource (a link to a file, credentials for a remote service, etc). We also want a method that returns a message to be shown on the checkout complete screen.

class License extends Entity {
  public $id;
  public $title;
  public $created;
  public $changed;
  public $uid;

  public function accessDetails() {
    // @todo Return the field(s) with the
    // access details here.
    return '';
  }

  public function checkoutCompletionMessage() {
    $message = 'Thank you for purchasing ' . $this->title;
    $message .= $this->accessDetails();
    return $message;
  }
}

We can now do $license->accessDetails() and $license->checkoutCompletionMessage(), a great centralization of logic that directly depends on (gets or sets) the entity data.

Here’s where our problem becomes obvious. Our license types have different needs for these methods. For access details, a file license type needs to return the formatted file field, but a Yottaa license type would need to return the API key and the site id (the credentials returned by the service on account creation). Same thing for the checkout completion message, different license types might have a need to show different text to the customer.
We also have a need for some license types to have methods that others don’t have.
The yottaa license type contacts a remote service, so it’s logical to be able to do $license->synchronize(), but such a method makes no sense on a file license.

Ideally, we would have some way for each license type (entity bundle) to have its own class, instead of having one class for all types. That is what entity_bundle_plugin provides.

Entity bundle plugin

So, Entity bundle plugin allows you to provide a class for each entity bundle.
It uses ctools plugins (see this tutorial for an intro) as a discovery mechanism (finding all entity bundle implementations provided by all modules).
It also allows each entity bundle class to define the fields that are created on that bundle.

For a practical example, let’s take a look at commerce_license_example, a submodule of Commerce License that provides two example license types.

commerce_license_example.module

/**
* Implements hook_ctools_plugin_directory().
*/
function commerce_license_example_ctools_plugin_directory($owner, $plugin_type) {
  // This tells ctools that the license types
  // are located in the plugins/ subfolder.
  if ($owner == 'commerce_license') {
    return "plugins/$plugin_type";
  }
}

The module structure:

commerce_license_example.info
commerce_license_example.module
plugins/
  type/
  example.inc
  CommerceLicenseExample.class.php

So inside the plugins/type directory we have two files.
example.inc:

$plugin = array(
  'title' => t('Example'),
  'class' => 'CommerceLicenseExample',
);

This file is the equivalent of a part of an info hook.
It just defines the label of the license type, and points to the class
located in CommerceLicenseExample.class.php
This class is automatically added to the registry by ctools, so there’s
no need to reference it from commerce_license_example.info.

Now let’s take a look at CommerceLicenseExample.class.php:

class CommerceLicenseExample extends Entity implements EntityBundlePluginProvideFieldsInterface, EntityBundlePluginValidableInterface  {
  // EntityBundlePluginProvideFieldsInterface
  static function fields() {
    $fields['cle_name']['field'] = array(
      'type' => 'text',
      'cardinality' => 1,
    );
    $fields['cle_name']['instance'] = array(
      'label' => t('Name'),
      'required' => 1,
      'settings' => array(
        'text_processing' => '0',
      ),
      'widget' => array(
        'module' => 'text',
        'type' => 'text_textfield',
        'settings' => array(
          'size' => 20,
        ),
      ),
    );

    return $fields;
  }

  // EntityBundlePluginValidableInterface
  public static function isValid() {
    return TRUE;
  }

  public function accessDetails() {
    // Just display the name field.
    $output = field_view_field('commerce_license', $this, 'cle_name');
    return drupal_render($output);
  }

  public function checkoutCompletionMessage() {
    $name = $this->cle_name[LANGUAGE_NONE][0]['value'];
    $text = "Congratulations, $name.
";
    $text .= "You are now licensed.";
    return $text;
  }
}

The real CommerceLicenseExample class extends the CommerceLicenseBase class which declares the properties and implements the mentioned interfaces (and a few more), I’ve simplified this version to make it more clear.

The fields() method defines the fields and instances to be created and attached to this license type. On the next cache clear, entity_bundle_plugin ensures that the required fields are created.
The isValid() method allows us to tell the discovery mechanism to skip this bundle.
For instance, if one of our license types depends on the advancedqueue module, isValid() can do a module_exists(‘advancedqueue’), and if it returns FALSE, this license type will be skipped.

The other two methods are license type specific, and implement the logic specific for that entity type.
Now when we load a license of the type “example”, the returned entity will be an instance of CommerceLicenseExample. and $license->accessDetails() will return the access details (in this case, the $license->cle_name field). Yay!

Let’s now take a look at how the commerce_license part of the entity_bundle_plugin integration works:

function commerce_license_entity_info() {
  $return = array(
    'commerce_license' => array(
      'label' => t('License'),
      'label callback' => 'entity_class_label',
      'controller class' => 'EntityBundlePluginEntityController',
      'bundle plugin' => array(
        'plugin type' => 'type',
        // The name of the class to use when loading an
        // invalid bundle.
        'broken class' => 'CommerceLicenseBroken',
      ),
      // other keys here, such as "entity keys"
     ),
  );
  foreach (commerce_license_get_type_plugins() as $plugin_name => $plugin) {
    $return['commerce_license']['bundles'][$plugin_name] = array(
      'label' => $plugin['title'],
    );
  }
  return $return;
}

// This function should move to entity_bundle_plugin
// at one point.
function commerce_license_get_type_plugins() {
  ctools_include('plugins');
  $plugins = ctools_get_plugins('commerce_license', 'type');
  foreach ($plugins as $key => $plugin) {
    if (!class_exists($plugin['class'])) {
      // Invalid class specified.
      unset($plugins[$key]);
      continue;
    }
    $r = new ReflectionClass($plugin['class']);
    if (!$r->hasMethod('isValid') || !call_user_func(array($plugin['class'], 'isValid'))) {
      // Invalid plugin specified.
      unset($plugins[$key]);
      continue;
    }
  }
  uasort($plugins, 'ctools_plugin_sort');
  return $plugins;
}

As you can see, we are using the entity_bundle_plugin controller (which instantiates the right bundle class and fills it with data), and instead of “entity class” we’re defining a “bundle plugin” key. There we’re defining the “broken class”, which is a class implementing the same interfaces but doing nothing, used a fallback in case something goes wrong and a bundle class can’t be found (the same concept is found in Views with the “broken handler”).

The commerce_license_get_type_plugins() function finds all available bundles by calling ctools, and then discarding any plugin that doesn’t have a loadable class or has returned FALSE from isValid().

Conclusion

As you can see, it’s simple to grasp and has obvious benefits. At one point amateescu tried to get this into Drupal 8 core, but it was too early to introduce yet another concept, so entity_bundle_plugin will need to stay in contrib at least for another release cycle.
We are also thinking about using it for Commerce 8.x-2.x.

I believe it is a perfect match for the described use case, where bundles are defined by other modules and carry their own logic.

About these ads

4 thoughts on “Entity Bundle Plugin

  1. I wouldn’t say it was too early. The only problem at the time was that the concept was shut down by EclipseGc as an “improper use of plugins”, that’s all.

  2. Perhaps it would’ve been better to position the plugin as a simple pointer to an alternate bundle class (with the default being a base class for the entity type), which resided elsewhere in the module? In that regard, the swappable functionality afforded by using the plugin system would be the code used to instantiate the class for the entity, not the class to use itself.

  3. This reminds me of a paradigm called DCI (Data, Context , Interaction) en.wikipedia.org/wiki/Data,_Context,_and_Interaction
    The basic idea is that the context (state of our system) at any given time determines the role that our data is playing, and the role of each object determines how they can interact with one another. The reason why I am reminded of DCI is that if somehow we were allowed to switch bundles on the fly, we would effectively be changing logic on the fly (very similar to switching roles in DCI). Anyways, very interesting.

  4. I like the idea, but reading this blog post I could help to wonder if object composition with the Strategy pattern (https://en.wikipedia.org/wiki/Strategy_pattern, an entity is the ‘context’) would provide a better/cleaner answer. This will allow clean encapsulation of the bundle-specific behavior management of the entity type in its single entity class, instead of needing a dedicated controller class. A strategy pattern based solutuion would be compatible with any implementation of EntityAPIControllerInterface (for instance the Remote Entity’s one).

Comments are closed.