Quantcast
Channel: Issues for Drupal core
Viewing all articles
Browse latest Browse all 292618

Add missing category to Drupal\layout_builder\Plugin\Layout\BlankLayout and let modules and themes alter the list of layouts

$
0
0

Problem/Motivation

Deprecated function: strnatcasecmp(): Passing null to parameter #2 ($string2) of type string is deprecated in Drupal\Core\Layout\LayoutPluginManager->Drupal\Core\Layout\{closure}() (line 204 of core/lib/Drupal/Core/Layout/LayoutPluginManager.php).

appears on pages, where layouts are being listed in a select which uses $this->layoutPluginManager->getLayoutOptions() to get its options.

Example:

$form['field_layouts']['field_layout'] = [
      '#type' => 'select',
      '#title' => $this->t('Select a layout'),
      '#options' => $this->layoutPluginManager->getLayoutOptions(),
      '#default_value' => $layout_plugin->getPluginId(),
      '#ajax' => [
        'callback' => '::settingsAjax',
        'wrapper' => 'field-layout-settings-wrapper',
        'trigger_as' => ['name' => 'field_layout_change'],
      ],
    ];

(FieldLayoutEntityDisplayFormTrait.php)

layout_builder's layout_builder_blank aka BlankLayout neither has a label nor a category defined in its annotation:

/**
 * Provides a layout plugin that produces no output.
 *
 * @see \Drupal\layout_builder\Field\LayoutSectionItemList::removeSection()
 * @see \Drupal\layout_builder\SectionListTrait::addBlankSection()
 * @see \Drupal\layout_builder\SectionListTrait::hasBlankSection()
 *
 * @internal
 *   This layout plugin is intended for internal use by Layout Builder only.
 *
 * @Layout(
 *   id = "layout_builder_blank",
 * )
 */
class BlankLayout extends LayoutDefault {

it should be sorted out by

/**
 * Implements hook_plugin_filter_TYPE_alter().
 */
function layout_builder_plugin_filter_layout_alter(array &$definitions, array $extra, $consumer) {
  // Hide the blank layout plugin from listings.
  unset($definitions['layout_builder_blank']);
}

(layout_builder.module)

but that hook never gets called!

So the layout_builder_blank layout is being passed through without label or category, but these methods rely on these values to be present. They are being called indirectly by $this->layoutPluginManager->getLayoutOptions()

  /**
   * {@inheritdoc}
   *
   * @return \Drupal\Core\Layout\LayoutDefinition[]
   */
  public function getSortedDefinitions(array $definitions = NULL, $label_key = 'label') {
    // Sort the plugins first by category, then by label.
    $definitions = $definitions ?? $this->getDefinitions();
    uasort($definitions, function (LayoutDefinition $a, LayoutDefinition $b) {
      if ($a->getCategory() != $b->getCategory()) {
        return strnatcasecmp($a->getCategory(), $b->getCategory());
      }
      return strnatcasecmp($a->getLabel(), $b->getLabel());
    });
    return $definitions;
  }

  /**
   * {@inheritdoc}
   *
   * @return \Drupal\Core\Layout\LayoutDefinition[][]
   */
  public function getGroupedDefinitions(array $definitions = NULL, $label_key = 'label') {
    $definitions = $this->getSortedDefinitions($definitions ?? $this->getDefinitions(), $label_key);
    $grouped_definitions = [];
    foreach ($definitions as $id => $definition) {
      $grouped_definitions[(string) $definition->getCategory()][$id] = $definition;
    }
    return $grouped_definitions;
  }

  /**
   * {@inheritdoc}
   */
  public function getLayoutOptions() {
    $layout_options = [];
    foreach ($this->getGroupedDefinitions() as $category => $layout_definitions) {
      foreach ($layout_definitions as $name => $layout_definition) {
        $layout_options[$category][$name] = $layout_definition->getLabel();
      }
    }
    return $layout_options;
  }

This is why the error appears.

This deprecation warning didn't appear in older versions of PHP, so the bug wasn't visible.

TL;DR:

/**
 * Implements hook_plugin_filter_TYPE_alter().
 */
function layout_builder_plugin_filter_layout_alter(array &$definitions, array $extra, $consumer) {
  // Hide the blank layout plugin from listings.
  unset($definitions['layout_builder_blank']);
}

(layout_builder.module)
is not called, so the layout_builder_blank layout, which neither has a label nor a description is not being sorted out, which leads to this error!

Steps to reproduce

  1. Install Drupal with the Umami demo profile.
  2. Enable the field_layout module.
  3. Log in as an administrator.
  4. Visit /en/admin/structure/types/manage/article/form-display.

Alternatively, install Drupal with the Standard profile, then enable the field_layout and layout_builder modules.

Further analysis

It seems as like the LayoutPluginManager (DI-injected by $container->get('plugin.manager.core.layout'),) is entirely missing FilteredPluginManagerTrait, so that its alterations are never being called:

/**
   * Implements \Drupal\Core\Plugin\FilteredPluginManagerInterface::getFilteredDefinitions().
   */
  public function getFilteredDefinitions($consumer, $contexts = NULL, array $extra = []) {
    if (!is_null($contexts)) {
      $definitions = $this->getDefinitionsForContexts($contexts);
    }
    else {
      $definitions = $this->getDefinitions();
    }

    $type = $this->getType();
    $hooks = [];
    $hooks[] = "plugin_filter_{$type}";
    $hooks[] = "plugin_filter_{$type}__{$consumer}";
    $this->moduleHandler()->alter($hooks, $definitions, $extra, $consumer);
    $this->themeManager()->alter($hooks, $definitions, $extra, $consumer);
    return $definitions;
  }

meanwhile, they are being called in Layout builder (e.g. admin/structure/types/manage/page/display/default/layout) when adding a section.
So maybe layout_builder is doing something different or special?
If layout_builder is not installed, everything is fine!

Proposed resolution

  1. Add missing attributes to Drupal\layout_builder\Plugin\Layout\BlankLayout so that getCategory() and getLabel() return string|\Drupal\Core\StringTranslation\TranslatableMarkup as required, instead of NULL.
  2. Call getFilteredDefinitions() in LayoutPluginManager::getLayoutOptions().
  3. Add test coverage for (2).

Either (1) or (2) by itself is enough to avoid the error. If we implement (1) but not (2), then it leads to a regression: the blank layout is listed, when it currently is not. See Comment #50. If we implement (2) but not (1), then we are hiding the error instead of fixing it.

For (3), mock Drupal::service('theme.manager'), since that is called in FilteredPluginManagerTrait::themeManager(). Then call getLayoutOptions() and confirm that the module and theme alter() methods are called.

Remaining tasks

  1. Decide whether to implement one of the other approaches in this issue or in a follow-up.

The decision is made: #3450764: Make $consumer optional in Drupal\Core\Plugin\FilteredPluginManagerTrait::getFilteredDefinitions() is the follow-up issue.

User interface changes

None, except for not showing the PHP error.

API changes

Drupal\Core\Layout\LayoutPluginManager::getLayoutOptions()

Data model changes

respects hook_plugin_filter_alter(), so modules and themes can alter the list of layouts.

Release notes snippet

N/A


Viewing all articles
Browse latest Browse all 292618

Trending Articles