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
- not available until the setter is called, and therefore unreliable
- mutable during runtime, causing unpredictable side-effects
- 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
- The previous
\Drupal\layout_builder\SectionStorageInterface::getContexts()has been renamed togetContextsDuringPreview()due to collision withContextAwarePluginInterface::getContexts(). Implementations should update the method name of the previous method. \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.-
The following methods have been deprecated:
\Drupal\layout_builder\SectionStorageInterfacepublic function getSectionListFromId($id)public function extractIdFromRoute($value, $definition, $name, array $defaults)
-
The following method has been added:
\Drupal\layout_builder\SectionStorageInterfacepublic function deriveContextsFromRoute($value, $definition, $name, array $defaults)
-
Additionally,
\Drupal\layout_builder\Plugin\SectionStorage\SectionStorageBasepreviously contained a protected helper method calledgetSectionList(). This is now an abstract method and all subclasses must provide an implementation. -
Finally,
\Drupal\layout_builder\SectionStorageInterfacenow extends\Drupal\Core\Plugin\ContextAwarePluginInterface, and\Drupal\layout_builder\Plugin\SectionStorage\SectionStorageBaseextends\Drupal\Core\Plugin\ContextAwarePluginBaseto provide a base implementation of those new inherited methods.