Object composition exposed to site builders: The Cfr Plugin API

Session Track
Experience Level

The session presents a new family of modules which can be seen as

  • An alternative to the Drupal 8 plugin architecture (*).
  • Object composition exposed to site builders.

The modules provide

  • A wide range of small interfaces and classes for various functionality within Drupal.
  • A system for the site builder to configure and compose instances of these classes, with recursively nested dynamic sub-forms based on constructor or factory signatures.
  • Integration with existing Drupal subsystems, where these components can be used.

The system may change the way you work with Drupal and think about site building problems. You end up with a lot of "S.O.L.I.D." and reusable code, and a nice division of responsibilities between developer and site builder.

The system has shown to work elegantly in two real-world projects, where it did complement, but not replace, existing systems like Views and Display Suite.

The modules are currently in -alpha, and only available for Drupal 7. The reason is that some details of the API need to be fine-tuned, possibly causing BC issues.

(*) As I said, no Drupal 8 branch yet. Nevertheless, it is possible to compare the architecture of this system with that of the Drupal 8 plugin API.

This session should be seen not as a "take it and use it" cookbook lesson, but rather as an introduction to new ideas, which are hopefully very exciting. And an invitation to share your thoughts and feedback.

@organizers: Duration and other details can be discussed. I don't know which durations are practical.

Background I: Object composition

Everyone heard about this, right? Composition over inheritance, single responsibility principle, all the other letters of "S.O.L.I.D."..

Instead of big classes with big inheritance hierarchies, we use small classes implementing single-purpose interfaces. Internal functionality, which previously would have gone into private methods, is instead moved out into separate reusable components.

A class has a constructor with a signature of type-hinted parameters. A developer can instantiate the class, passing parameter values that match the interface from the type hint.

Adapters and decorators allow to use a specific class in another context, or with a modified behavior, without having to resort to inheritance.

Can there be too much of it? Can classes be too small, separation too fine-grained? Yes they can. But most of the code we find in Drupal core and contrib is far away from this.

Background II: Plugins in Drupal 7 and 8

The traditional way to build a Drupal site was to do some site building / configuration, and then target these "configuration things" with theme overrides, alter hooks, and CSS rules. This code was typically not reusable, and would break if the configuration is modified.

A lot of tools were introduced in Drupal 7 and contrib, where you could at least partially avoid this coupling problem. Now a developer would create custom field types and formatters, text filters, views style plugins, Display suite layouts, etc. Then a site builder could build a site from these components. There might still be a layer of theme overrides and such, but it would be much thinner.

Drupal 8 then introduced a "plugin API", where all of the above could be implemented in a unified and object-oriented way.

But there are still problems:

  • Plugins are typically written for a specific subsystem (e.g. Views), and cannot easily be reused outside of this subsystem. Code reuse between subsystems is not so easy.
  • Introducing a new type of plugin requires a new "plugin manager" class.
  • Typically there is one plugin = one class.
  • Plugin implementations typically have configuration UI and actual functionality in the same class.
  • A plugin instance typically has access to its own raw configuration array and even the plugin id during its lifetime.
  • Plugin instances are often mutable / stateful. The configuration may change during the lifetime of the plugin object.
  • Often it is cheaper to hard-code a configuration id (e.g. a field name) than to make it configurable, resulting in code that depends on configuration artifacts.
  • Code reuse is often achieved with inheritance, resulting in quite big inheritance hierarchies.
  • Plugin interfaces can be quite big.

Introducing the Cfr Plugin API

A wide range of small, single-purpose interfaces.

A first step was to identify functionality in Drupal that can be expressed with simple interfaces that are agnostic of any Drupal subsystem. A lot of these can be found in "renderkit" and deal with render arrays. But it is easy to write new ones.

Examples:

  • EntityDisplayInterface: Produces a render array for a given entity.
  • ElementProcessorInterface: Produces a new render array for a given render array. E.g. this could add a wrapper with '#type' => 'container'.
  • EntityToEntityInterface: Gets a related entity for a given entity. Implementations could be based on an entity reference field, the entity author, etc.
  • ListFormat: Produce a (combined) render array from a list of distinct render arrays.

So far this is all code, no configuration and "site building" involved.

A zoo of (parameterized) implementations.

Most implementations either integrate existing Drupal functionality such as fields, node title etc, or they combine functionality from injected instances of other interfaces.

Examples:

  • class EntityDisplay_ViewMode.
    function __construct($viewMode) {..}
    Builds a render array that displays the complete entity in the given view mode.
  • class EntityDisplay_RelatedEntity.
    function __construct(EntityDisplayInterface $relatedDisplay, EntityToEntityInterface $entityToEntity) {..}
    Uses $entityToEntity to determine a related entity, and then $relatedDisplay to produce a render array for the related entity.

Again, so far this is all code, no configuration.

The "configurator" ("cfr") concept

A "configurator" is an object that can translate a serializable configuration value (typically an array, but can be anything) into

  • An object or value to be used in the application, via ->confGetValue($conf).
    This could be an instance of one of the classes above.
  • A summary text, via ->confGetSummary($conf).
  • A configuration form, via ->confGetForm($conf),
    allowing a site builder to change the configuration value.

E.g. there could be a configurator that lets you choose between "ul" and "ol", and then ->confGetValue($conf) will return a ListFormatInterface implementation instance that renders the items with an "ol" or "ul" list.

Some configurators create other configurators based on the given configuration, and let these other configurators create sub-components.

Registered module-provided configurators ("plugins")

Modules can register configurators with hook_cfrplugin_info(). The definitions in this info hook specify

  • a label.
  • the interface that objects returned from ->confGetValue() should implement.
  • a recipe to construct the configurator. E.g. a class name and constructor parameters. Or a static method and parameters.

Modules can use a special form of annotation discovery to assemble the array of plugin definitions. This is inspired by Drupal 8.

One toplevel configurator per interface

For a given interface, the Cfr plugin API provides a top-level configurator, which allows to choose between all registered plugins for this interface, and then configure the respective plugin in a sub-form.

Automatic configurators from parameter signatures

Given a constructor or a static method with interfaces as parameter type hints, the Cfr plugin API can automatically create a configurator that uses sub-configurators based on the interfaces in the signature.

The shortest Cfr plugin is a static method with an annotation.

Integration with existing Drupal subsystems

Cfr plugins can be used in existing subsystems such as Views, Display suite / Field UI, blocks etc. The configuration will be stored in a place appropriate for the given subsystem.

E.g. plugins for "EntityDisplayInterface" can be used as Views row plugin, as Views field handler, as a field in Field UI / Display suite, as a formatter for entity reference, and possibly other places.

A ListFormat can be used as a Views style plugin, or for a list of field items, or possibly blocks or other things.

Cfr presets

Cfr plugin configurations can be configured from scratch and stored in the place where they are used (e.g. in a view). The "cfrpreset" module provides a central place to store reusable plugin configurations.

Step III: Profit!

I shall put here a picture of the underpants gnomes, which are my favorite gnomes!

With this system it is super-easy to create a new "plugin type" (create an interface) and a "plugin implementation" (annotated class or static factory method). The typical class or interface will look very small and simple.

Compared to Drupal 8 plugins:

  • Plugin reuse between subsystems, e.g. via simple adapter classes.
  • No custom "plugin manager" classes.
  • No 1:1 mapping of plugin to implementation class:
    • A plugin can choose a different implementation class depending on its configuration.
    • A plugin can return a composition of multiple classes.
  • Configuration and actual functionality are separate:
    • The configurator deals with the configuration.
    • The object returned by ->confGetValue() provides the actual functionality.
  • The object returned by ->confGetValue() never sees the raw configuration array.
    Typically it is stateless / immutable.
  • The configurator does not keep any configuration array, it only sees what is passed as parameters.
    Typically it is stateless / immutable.
  • Parameters that depend on site configuration (e.g. a field name) can be easily made configurable, so people don't hard-code them.
  • Code reuse is achieved with composition.
  • Plugin interfaces and classes are very small.

Division of labor - site builder vs developer

A recommended practice is to make everything configurable that depends on site configuration. But to freeze composition in code, when configuration does not add any benefit.

Outlook / TODO

Obviously, this is all a bit boring if it is not available in Drupal 8. But before that, a few API details should be ironed out.

One challenge in Drupal 8 will be integration with the dependency injection container.

For all of this I want a lot of feedback!

 

Are you a speaker at our event? Make sure to read our Speaker Guidelines and FAQ.

Let us know if you would like to volunteer!