Change record status: 
Project: 
Introduced in branch: 
8.7.x
Description: 

Previously, SectionStorage plugins had a public method setSectionList(SectionListInterface $section_list).
This was used by the plugin manager in conjunction with the plugin's public method getSectionListFromId($id):

$plugin->setSectionList($plugin->getSectionListFromId($id));

The correct plugin ID was either explicitly provided, or derived by the plugin from routing information:

if ($id = $plugin->extractIdFromRoute($value, $definition, $name, $defaults)) {
  return $plugin->setSectionList($plugin->getSectionListFromId($id));
}

However, this meant that the section list of a plugin was

  1. not available until the setter is called, and therefore unreliable
  2. mutable during runtime, causing unpredictable side-effects
  3. directly controlled by two public methods only intended for usage by the manager but available to all

Furthermore, it was impossible to know exactly what cases a given SectionStorage would be applicable for. Only the ::extractIdFromRoute()

Here is an example SectionStorage plugin written for the old API:
(unchanged methods access(), buildRoutes(), getStorageId(), getRedirectUrl(), getLayoutBuilderUrl(), label(), and save() are excluded)


namespace Drupal\layout_builder\Plugin\SectionStorage;

use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\node\Entity\Node;

/**
 * @SectionStorage(
 *   id = "example_node",
 * )
 */
class NodeStorageSection extends SectionStorageBase {

  /**
   * {@inheritdoc}
   */
  public function getSectionListFromId($id) {
    if (strpos($id, '.') !== FALSE) {
      list($entity_type_id, $entity_id) = explode('.', $id, 2);
      if ($entity_type_id === 'node' && $node = Node::load($entity_id)) {
        return $node->get('section_list');
      }
    }
    throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType()));
  }

  /**
   * {@inheritdoc}
   */
  public function extractIdFromRoute($value, $definition, $name, array $defaults) {
    if (strpos($value, '.') !== FALSE) {
      return $value;
    }

    if (!empty($defaults['node'])) {
      return 'node.' . $defaults['node'];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getContexts() {
    $contexts = [];
    $entity = \Drupal::service('layout_builder.sample_entity_generator')->get('node', 'article');
    $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity);
    return $contexts;
  }

}

Here is the same functionality, updated for the new API:


namespace Drupal\layout_builder\Plugin\SectionStorage;

use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\node\Entity\Node;

/**
 * @SectionStorage(
 *   id = "example_node",
 *   context_definitions = {
 *     "entity" = @ContextDefinition("entity:node"),
 *   }
 * )
 */
class NodeStorageSection extends SectionStorageBase {

  /**
   * {@inheritdoc}
   */
  protected function getSectionList() {
    return $this->getContextValue('entity')->get('section_list');
  }

  /**
   * {@inheritdoc}
   */
  public function deriveContextsFromRoute($value, $definition, $name, array $defaults) {
    $contexts = [];

    if ($entity = $this->extractEntityFromRoute($value, $defaults)) {
      $contexts['entity'] = EntityContext::fromEntity($entity);
    }
    return $contexts;
  }

  /**
   * Extracts an entity from the route values.
   *
   * @param mixed $value
   *   The raw value from the route.
   * @param array $defaults
   *   The route defaults array.
   *
   * @return \Drupal\Core\Entity\EntityInterface|null
   *   The entity for the route, or NULL if none exist.
   */
  protected function extractEntityFromRoute($value, array $defaults) {
    if (strpos($value, '.') !== FALSE) {
      list($entity_type_id, $entity_id) = explode('.', $value, 2);
      if ($entity_type_id !== 'node') {
        return NULL;
      }
    }
    elseif (!empty($defaults['node'])) {
      $entity_id = $defaults['node'];
    }
    else {
      return NULL;
    }

    return Node::load($entity_id);
  }

  /**
   * {@inheritdoc}
   */
  public function getContextsDuringPreview() {
    $contexts = parent::getContextsDuringPreview();
    $entity = \Drupal::service('layout_builder.sample_entity_generator')->get('node', 'article');
    $contexts['entity'] = EntityContext::fromEntity($entity);
    return $contexts;
  }

}

With this change, SectionStorage plugins must instead declare their required contexts on the plugin annotation with the context_definition key.
The plugin will only be used in scenarios when all of its required contexts are satisfied.
In the example, the entity context is required, with a type of entity:node. If no nodes are available, this plugin will not be loaded.

Corresponding to this change is the protected method getSectionList(). Previously this was a simple getter and was implemented by the base class. Now it is abstract on the base class and must be implemented. This method is responsible for returning a section list, and should derive it from the context values available.
In the example, this is a field on the node.

Previously, this code lived within getSectionListFromId(), which was responsible for transforming a string ID to the full section list.
Also responsible for string transformation was extractIdFromRoute().
That string transformation code should now be contained within deriveContextsFromRoute() (or in this example, a helper method).

Finally, existing implementations of getContext() should be renamed to getContextsDuringPreview() so as to not clash with \Drupal\Component\Plugin\ContextAwarePluginInterface::getContexts() which has been added to SectionStorageInterface.

API changes

  1. The previous\Drupal\layout_builder\SectionStorageInterface::getContexts() has been renamed to getContextsDuringPreview() due to collision with ContextAwarePluginInterface::getContexts(). Implementations should update the method name of the previous method.
  2. \Drupal\layout_builder\SectionStorageInterface::setSectionList() has been removed, and the base class implementation will now throw an exception to ensure it is not called. This method is incompatible with deriving the section list from context.
  3. The following methods have been deprecated:

    \Drupal\layout_builder\SectionStorageInterface

    • public function getSectionListFromId($id)
    • public function extractIdFromRoute($value, $definition, $name, array $defaults)
  4. The following method has been added:

    \Drupal\layout_builder\SectionStorageInterface

    • public function deriveContextsFromRoute($value, $definition, $name, array $defaults)
  5. Additionally, \Drupal\layout_builder\Plugin\SectionStorage\SectionStorageBase previously contained a protected helper method called getSectionList(). This is now an abstract method and all subclasses must provide an implementation.
  6. Finally, \Drupal\layout_builder\SectionStorageInterface now extends \Drupal\Core\Plugin\ContextAwarePluginInterface, and \Drupal\layout_builder\Plugin\SectionStorage\SectionStorageBase extends \Drupal\Core\Plugin\ContextAwarePluginBase to provide a base implementation of those new inherited methods.
Impacts: 
Module developers