Block plugins need to be able to update their settings in multiple different storage implementations

Created on 25 April 2025, 1 day ago

Problem/Motivation

Block plugins are used in core in three different modules:

1. Block module (config entities)
2. Layout builder (config entities and also view mode overrides in content).
3. Navigation (a single config object - navigation.block_layout, but using the layout builder data structure)

Also, in Experience Builder - see 🌱 [META] Production-ready ComponentSource plugins Active and 📌 [SPIKE] Prove that it's possible to apply block settings update paths to stored XB component trees Active which inspired this issue.

When a block plugin updates its settings structure, all of these places should be updated, but, this is not the case.

As far as I can tell, there are two reasons this hasn't come up before:
1. We rarely update block plugin settings so have got lucky that we haven't fatally broken any sites yet.
2. We don't validate block plugin settings config in these various places yet.

/**
 * Updates Search Blocks' without an explicit `page_id` from '' to NULL.
 */
function search_post_update_block_with_empty_page_id(&$sandbox = []) {
  $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class);
  $config_entity_updater->update($sandbox, 'block', function (BlockInterface $block): bool {
    // Only update blocks using the search block plugin.
    // @see search_block_presave()
    if ($block->getPluginId() === 'search_form_block' && $block->get('settings')['page_id'] === '') {
      $settings = $block->get('settings');
      $settings['page_id'] = NULL;
      $block->set('settings', $settings);
      return TRUE;
    }
    return FALSE;
  });
}

This is updating block config entities, but not layouts or navigation.

Search module theoretically could add additional updates for layouts and navigation, and also update their config with the same changes, but it doesn't. And even if it did do that, search module can't update experience builder, because that's in contrib, or any other theoretical place that consumes blog plugins and stores their settings.

Some of this is going to be quite painful to implement, but I think conceptually it ought to work.

We will also need to change the search update to use this new, correct method, and document this very clearly somewhere too. It's like an extra level of update hell on top of normal config entity updates which everyone already gets wrong all the time.

Steps to reproduce

Proposed resolution

I think we need to add an API for updating block plugin settings.

It would look something like this - very rough:

BlockPluginSettingsUpdaterInterface {
  isUpdateNeeded(array $stuff): bool;
  updateStuff(array $stuff): array;
}

Search module's update would then look something like this:

class SearchEmptyPageUpdater implements BlockPluginSettingsUpdaterInterface {
 // logic here
}

<?php
hook_post_update_NAME(&$sandbox) {
\Drupal::service('block_plugin_updater')->updateBlockPluginSettings('search', 'SearchEmptyPageUpdater::class', $sandbox);
}

The new block_plugin_updater service then triggers an event/hook, and... this bit is fuzzy, layout builder, block, navigation, experience builder, return a callback or something, that does what they would normally do in their hook_update_N().

Either they need create a batch that does the update (which would end up as a nested batch inside the post update hook), or return enough information for the block_plugin_updater class to do that.

Conversely, updates are not the only place that this logic needs to run, it also needs to run on presave of all these different config entity types to support shipped config too.

Soo SearchEmptyPageUpdater and its equivalents, as well as implementing the interface, could also be a plugin. Then the various hook_entity_presave() can call another core service, which takes a config entity, discovers all the blocksettingsupdater plugins, then runs that same logic to update the config entity before it's saved. Or maybe not via a service and do it directly since they're going to be responsible for determining where in their config structure the settings live and passing that to the updater plugins.

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

🐛 Bug report
Status

Active

Version

11.0 🔥

Component

base system

Created by

🇬🇧United Kingdom catch

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @catch
  • 🇬🇧United Kingdom catch
  • 🇬🇧United Kingdom catch
  • 🇬🇧United Kingdom catch

    I haven't looked at Dashboard module internals, but pretty sure that also needs the same treatment.

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺
  • 🇬🇧United Kingdom catch

    @longwave mentioned in slack the possibility of generalising this to all plugins that have settings. We could maybe use it for views config updates if we did that.

    Also config entities that store plugin settings could maybe generically do something to, to apply any plugin update plugins they need to. Although they also need to indicate where those plugin settings are stored in their own data structures. Only views knows where area or filter plugins are in its own config entities.

  • 🇬🇧United Kingdom catch

    For scope something like:

    1. Add the update plugin interface, and the updater service called from post updates, and the presave bit, and implement this in block module + search for the update that already exists. That will give us one working implementation of one update and one block plugin storage. We'll need to decide if we make this generically useful for all plugin types or limit it to blocks.

    2. Open separate issues for layout builder, navigation, and experience builder to implement the storage update parts.

    3. Check for any other block updates in core that follow a similar pattern to the search one and convert those too.

    4. If we do make this generically useful for other plugin types and config entities, open follow-ups for those.

  • 🇪🇸Spain penyaskito Seville 💃, Spain 🇪🇸, UTC+2 🇪🇺

    #4 🐛 Block plugins need to be able to update their settings in multiple different storage implementations Active I haven't looked at Dashboard module internals, but pretty sure that also needs the same treatment.

    Confirm, we would need to do the same, and we are not doing anything for this scenario (in this case I'd expect layout builder implementation to handle this for everyone using a SectionStorage plugin).

    #0 🐛 Block plugins need to be able to update their settings in multiple different storage implementations Active Conversely, updates are not the only place that this logic needs to run, it also needs to run on presave of all these different config entity types to support shipped config too.

    IMHO this will be complex enough that it could be left for a follow-up, and AFAIK we have no precedent of this. Looks like a related-but-not-the-same separate beast.

    E.g. if node module implements a config schema change on node_type, the shipped node types config of contrib module X are not updated on installation of module X.

    #8 🐛 Block plugins need to be able to update their settings in multiple different storage implementations Active @longwave mentioned in slack the possibility of generalising this to all plugins that have settings.

    If we include shipped config on the scope, and per my comment above, wouldn't this be a general problem for any config that has an update to its structure/config schema, and not only plugins?

    And I'd expect we would need some kind of tracking of the last hook_update_N/post_update where this shipped config was generated with, which might mean a huge change with no BC layer.

  • 🇬🇧United Kingdom catch

    IMHO this will be complex enough that it could be left for a follow-up, and AFAIK we have no precedent of this.

    We already implement an equivalent presave hook for every* config entity update in core. Views has the most examples of this, which are handled centrally via ViewsConfigUpdater. The search example above also comes with a presave hook. See https://api.drupal.org/api/drupal/core%21modules%21search%21src%21Hook%2...

    *except when we forget, although I think Drupal 10/11 are better than 8/9 were.

Production build 0.71.5 2024