Problem/Motivation

While working on the Layout Library module, I discovered that Layout Builder is basically incompatible with any layout storage strategy except for its two built-in ones, which are "defaults" (storing a layout in an entity view display) and "overrides" (storing a layout in a field on a content entity).

The problem comes down to the getRuntimeSections() method of \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay:


  protected function getRuntimeSections(FieldableEntityInterface $entity) {
    if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) {
      return $entity->get('layout_builder__layout')->getSections();
    }

    return $this->getSections();
  }

This code is rooting around in internal implementation details of both the defaults and overrides storage plugins. It is completely unaware of any other layout storage strategy -- which, as I understand it, was the entire point of making layout storage pluggable in the first place -- yet it pretty much controls how layout-able entities are rendered. This code kinda renders the entire idea of "section storage plugins" moot.

Proposed resolution

LayoutBuilderEntityViewDisplay::getRuntimeSections() is adjusted to use the new API from #3016473: Allow a single SectionStorage plugin to be returned for a set of available contexts, removing all hardcoded runtime knowledge of $entity->get('layout_builder__layout')

Remaining tasks

N/A

User interface changes

N/A

API changes

N/A

Data model changes

N/A

Release notes

N/A

CommentFileSizeAuthor
#208 2976148-delegate-applicable-208-interdiff.txt2.01 KBtim.plunkett
#208 2976148-delegate-applicable-208.patch35.41 KBtim.plunkett
#206 2976148-delegate-applicable-182-to-205-interdiff.txt18.57 KBtim.plunkett
#205 2976148-delegate-applicable-205-interdiff.txt2.92 KBtim.plunkett
#205 2976148-delegate-applicable-205.patch35.33 KBtim.plunkett
#202 2976148-delegate-applicable-202-interdiff.txt11.9 KBtim.plunkett
#202 2976148-delegate-applicable-202-PASS.patch33.22 KBtim.plunkett
#202 2976148-delegate-applicable-202-FAIL.patch29.3 KBtim.plunkett
#200 2976148-delegate-applicable-200-interdiff.txt1.73 KBtim.plunkett
#200 2976148-delegate-applicable-200.patch29.91 KBtim.plunkett
#196 2976148-delegate-applicable-196-interdiff.txt7.36 KBtim.plunkett
#196 2976148-delegate-applicable-196.patch29.9 KBtim.plunkett
#192 2976148-delegate-applicable-192-interdiff.txt1.49 KBtim.plunkett
#192 2976148-delegate-applicable-192.patch25.72 KBtim.plunkett
#191 2976148-delegate-applicable-191-interdiff.txt5 KBtim.plunkett
#191 2976148-delegate-applicable-191.patch25.72 KBtim.plunkett
#182 2976148-delegate-applicable-182-interdiff.txt3.37 KBtim.plunkett
#182 2976148-delegate-applicable-182.patch25.74 KBtim.plunkett
#177 2976148-delegate-applicable-177-interdiff.txt8.86 KBtim.plunkett
#177 2976148-delegate-applicable-177.patch25.63 KBtim.plunkett
#175 2976148-delegate-applicable-175-interdiff.txt10.63 KBtim.plunkett
#175 2976148-delegate-applicable-175.patch25.49 KBtim.plunkett
#174 2976148-delegate-applicable-174-interdiff.txt2.78 KBtim.plunkett
#174 2976148-delegate-applicable-174.patch25.01 KBtim.plunkett
#168 2976148-delegate-applicable-168-interdiff.txt5.59 KBtim.plunkett
#168 2976148-delegate-applicable-168.patch24.26 KBtim.plunkett
#164 2976148-delegate-applicable-164-interdiff.txt12.34 KBtim.plunkett
#164 2976148-delegate-applicable-164.patch24.32 KBtim.plunkett
#162 2976148-delegate-162-interdiff.txt8.88 KBtim.plunkett
#162 2976148-delegate-162.patch34.65 KBtim.plunkett
#160 2976148-delegate-160-interdiff.txt21.01 KBtim.plunkett
#160 2976148-delegate-160.patch32.13 KBtim.plunkett
#158 2976148-delegate-158-interdiff.txt10.55 KBtim.plunkett
#158 2976148-delegate-158.patch20.46 KBtim.plunkett
#155 2976148-delegate-155-interdiff.txt8.66 KBtim.plunkett
#155 2976148-delegate-155.patch22.16 KBtim.plunkett
#155 2976148-delegate-nocaching-155-interdiff.txt7.36 KBtim.plunkett
#155 2976148-delegate-nocaching-155.patch17.48 KBtim.plunkett
#147 2976148-delegate-147-interdiff.txt25.51 KBtim.plunkett
#147 2976148-delegate-147.patch16.94 KBtim.plunkett
#145 2976148-delegate-145-interdiff.txt5.42 KBtim.plunkett
#145 2976148-delegate-145.patch25.16 KBtim.plunkett
#141 2976148-delegate-141.patch23.88 KBtim.plunkett
#141 2976148-delegate-141-interdiff.txt2.98 KBtim.plunkett
#137 2976148-delegate-137-interdiff.txt2.34 KBtim.plunkett
#137 2976148-delegate-137-PASS.patch22.9 KBtim.plunkett
#137 2976148-delegate-137-FAIL.patch22.11 KBtim.plunkett
#131 2976148-delegate-refactor-131-do-not-test.patch21.15 KBtim.plunkett
#129 2976148-delegate-129-interdiff.txt2.51 KBtim.plunkett
#129 2976148-delegate-129.patch97.13 KBtim.plunkett
#127 2976148-delegate-127-interdiff.txt2.28 KBtim.plunkett
#127 2976148-delegate-127.patch96.96 KBtim.plunkett
#121 2976148-delegate-121-PASS.patch98.6 KBtim.plunkett
#121 2976148-delegate-121-FAIL.patch86.83 KBtim.plunkett
#121 2976148-delegate-121-interdiff.txt28.36 KBtim.plunkett
#114 2976148-delegate-114.patch81.35 KBtim.plunkett
#113 2976148-delegate-113-interdiff.txt2.55 KBtim.plunkett
#113 2976148-delegate-113.patch81.29 KBtim.plunkett
#105 2976148-delegate-105-interdiff.txt25.66 KBtim.plunkett
#105 2976148-delegate-105-PASS.patch81.11 KBtim.plunkett
#105 2976148-delegate-105-FAIL.patch80.39 KBtim.plunkett
#90 2976148-delegate-90-interdiff.txt5.93 KBtim.plunkett
#90 2976148-delegate-90.patch66.97 KBtim.plunkett
#82 2976148-delegate-82-interdiff.txt1.32 KBtim.plunkett
#82 2976148-delegate-82.patch66.86 KBtim.plunkett
#79 2976148-delegate-79-interdiff.txt6.61 KBtim.plunkett
#79 2976148-delegate-79.patch66.85 KBtim.plunkett
#76 2976148-delegate-76-interdiff.txt20.19 KBtim.plunkett
#76 2976148-delegate-76.patch65.15 KBtim.plunkett
#75 2976148-delegate-75-interdiff.txt9.52 KBtim.plunkett
#75 2976148-delegate-75.patch59.41 KBtim.plunkett
#74 2976148-delegate-74-interdiff.txt11.81 KBtim.plunkett
#74 2976148-delegate-74.patch58.29 KBtim.plunkett
#70 2976148-delegate-refactor-70-interdiff.txt859 bytestim.plunkett
#70 2976148-delegate-refactor-70.patch48.25 KBtim.plunkett
#66 2976148-delegate-66-do-not-test.patch48.19 KBtim.plunkett
#65 2976148-delegate-64-do-not-test.patch58.17 KBtim.plunkett
#65 2976148-delegate-64-combined-2986735-3008431.patch71.82 KBtim.plunkett
#61 2976148-delegate-61-interdiff.txt15.77 KBtim.plunkett
#61 2976148-delegate-61-review-do-not-test.patch58.9 KBtim.plunkett
#61 2976148-delegate-61-combined.patch72.53 KBtim.plunkett
#59 2976148-delegate-59-interdiff.txt11.72 KBtim.plunkett
#59 2976148-delegate-59.patch62.33 KBtim.plunkett
#57 2976148-delegate-57-interdiff.txt33.65 KBtim.plunkett
#57 2976148-delegate-57.patch51.95 KBtim.plunkett
#54 interdiff-2976148-51-54.txt2.68 KBphenaproxima
#54 2976148-54.patch38.49 KBphenaproxima
#51 interdiff-2976148-48-51.txt8.49 KBphenaproxima
#51 2976148-51.patch37.55 KBphenaproxima
#48 interdiff-2976148-47-48.txt3.62 KBphenaproxima
#48 2976148-48.patch31 KBphenaproxima
#47 2976148-47.patch30.71 KBphenaproxima
#44 interdiff-2976148-42-44.txt5.49 KBphenaproxima
#44 2976148-44.patch17.83 KBphenaproxima
#42 interdiff-2976148-38-42.txt2.88 KBphenaproxima
#42 2976148-42.patch17.07 KBphenaproxima
#38 2976148-38.patch15.74 KBphenaproxima
#31 2976148-31.patch15.09 KBphenaproxima
#30 2976148-30.patch16.46 KBphenaproxima
#28 2976148-28.patch15.54 KBphenaproxima
#26 interdiff-2976148-24-26.txt741 bytesphenaproxima
#26 2976148-26.patch14.76 KBphenaproxima
#24 2976148-24.patch14.35 KBphenaproxima
#21 interdiff-2976148-19-21.txt2.37 KBphenaproxima
#21 2976148-21.patch14.26 KBphenaproxima
#19 interdiff-2976148-16-19.txt3.23 KBphenaproxima
#19 2976148-19.patch14.16 KBphenaproxima
#16 2976148-16.patch10.56 KBphenaproxima
#14 interdiff-2976148-13-14.txt2.85 KBphenaproxima
#14 2976148-14.patch11.11 KBphenaproxima
#13 interdiff-2976148-8-13.txt915 bytesphenaproxima
#13 2976148-13.patch9.86 KBphenaproxima
#8 2976148-8.patch8.79 KBphenaproxima
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

phenaproxima created an issue. See original summary.

phenaproxima’s picture

Title: LayoutBuilderEntityViewDisplay::getRuntimeSections() is poorly encapsulated » LayoutBuilderEntityViewDisplay::getRuntimeSections() does not delegate to plugins

Re-titling.

tim.plunkett’s picture

I've continually worked to cut down on all the magic layout_builder__layout spread throughout. I think this is one of the final offenders, so +1 to this.

phenaproxima’s picture

One possibility that @tim.plunkett and I discussed in Slack is to introduce a "negotiator" pattern here. Loop through each plugin and ask it if it can apply layout at run time. The first one to answer in the affirmative (i.e., by returning sections) wins. We'd have to allow plugins to be explicitly weighted in order for this to work, but it could work quite nicely.

phenaproxima’s picture

Another idea I had whilst heading home today is to allow section storage plugins to be context-aware, and to filter their definitions by context data, in order to figure out which section storage plugins might apply to a given rendering.

So in other words, what I'm envisioning:

  • SectionStorageManager will implement ContextAwarePluginManagerInterface and use its accompanying trait.
  • SectionStorageBase will extend ContextAwarePluginBase, not PluginBase.
  • DefaultsSectionStorage will require an entity_view_display entity context, and OverridesSectionStorage will require a fieldable entity context that has the layout_builder__layout field.
  • Section storage plugins will get a weight property in their annotation, so that their contexts can be checked in a deterministic order.
  • getRuntimeSections() will create contexts for the entity being rendered, as well as the view display itself, and query SectionStorageManager for all storage plugins, ordered by weight, which satisfy the contexts.
  • The first available plugin will win, and be used to build the layout.

The main advantage to this approach is that it will use the existing context APIs in core and not require any real API changes or data model changes in Layout Builder itself. Nice.

phenaproxima’s picture

Referencing #2976356: Add a validation constraint to check if an entity has a field so that section storage plugins can filter entity contexts by whether or not they have the layout_builder__layout field available.

samuel.mortenson’s picture

OverridesSectionStorage will require a fieldable entity context that has the layout_builder__layout field

This would require a new kind of context right? Does that mean that every section storage plugin would need to create a new kind of context that would only be used for checking if they apply?

I think I would prefer doing something like an isApplicable method, which would encapsulate the one-time logic needed to check if a section storage applies.

phenaproxima’s picture

Status: Active » Needs review
FileSize
8.79 KB

An initial patch, without tests, using contexts and explicit weights to filter applicable plugins. Would love to hear thoughts on this approach...

phenaproxima’s picture

phenaproxima’s picture

phenaproxima’s picture

Title: LayoutBuilderEntityViewDisplay::getRuntimeSections() does not delegate to plugins » [PP-1] LayoutBuilderEntityViewDisplay::getRuntimeSections() does not delegate to plugins
Status: Needs work » Postponed
phenaproxima’s picture

This changes SectionStorageDefinition to use the context-aware plugin definition plumbing added in #2961822: Support object-based plugin definitions in ContextHandler.

phenaproxima’s picture

Changing the patch so that the Overrides section storage plugin can now use any entity. Once #2976356: Add a validation constraint to check if an entity has a field lands, we'll need to change that so it requires the entity to have the layout_builder__layout field.

phenaproxima’s picture

Title: [PP-1] LayoutBuilderEntityViewDisplay::getRuntimeSections() does not delegate to plugins » LayoutBuilderEntityViewDisplay::getRuntimeSections() does not delegate to plugins
Status: Postponed » Needs work

The blocker is in!

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
10.56 KB

Now that the blocker is in, here's an initial patch (without tests) of the approach I'm considering taking. I'd like this approach to be validated by @tim.plunkett or some other knowledgeable human before I continue fleshing this out and writing tests.

tim.plunkett’s picture

Tracked down the failures and opened #2982626: ContextAwarePluginBase is incompatible with ContextAwarePluginDefinitionInterface
This issue can include that override within SectionStorageBase for now, with an @todo linking to the upstream.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
14.16 KB
3.23 KB

Here's a very minimal test that asserts, in a lightweight way, that rendering is delegated to plugins. I'm open to any and all feedback about the approach, and the test.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
14.26 KB
2.37 KB

This should fix at least a few more of the broken tests.

It also turns out that removing the call to $this->isOverridable() causes infinite recursive rendering. This is because:

- In the process of rendering individual fields in blocks, an ephemeral entity view display is created.
- Because Layout Builder has taken over the entity view display class, that entity view display is layout-aware, but it is not overridable (layout-aware entity view displays are not overridable by default).
- getRuntimeSections() is called, which correctly delegates its work to the layout plugins (because it is not checking if the entity view display is overridable).
- Which in turn causes the field block(s) to be re-rendered recursively, once again starting this unholy cycle until the stack is smashed.

So, we have two options here.

1) We can block on #2936464: Remove setComponent() workaround in LayoutBuilderEntityViewDisplay, which might help address this problem.

2) We can keep the call to isOverridable(), which will work around the issue. I'm not entirely happy with the workaround, but it might be appropriate in this case. In any event, we could add a @todo referencing the aforementioned issue.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
14.35 KB

Re-roll on top of 8.7.x. Let's see how far this gets.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
14.76 KB
741 bytes

Sigh. I made a dumb.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
15.54 KB

Rerolled on top of 8.7.x, and hopefully fixing tests. They pass locally, anyway.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
16.46 KB

Another reroll.

phenaproxima’s picture

Removing accidental patch noise.

tim.plunkett’s picture

Issue tags: +sprint
tim.plunkett’s picture

  1. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -239,11 +241,27 @@ public function buildMultiple(array $entities) {
    +      $definitions = $storage_manager->getDefinitionsForContexts($contexts);
    +      if ($definitions) {
    +        $storage_type = key($definitions);
    

    This "get all the matching ones, but then use key() to get the first one" part seems awkward to me.

    What if there were another place within LB that we need to delegate to all of them? Or run this as a chain-of-responsibility, where we run through each one until one returns something (like breadcrumb builders)?

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
    @@ -249,6 +252,13 @@ public function getSectionListFromId($id) {
    +    return $this->getContextValue('entity_view_display');
    
    +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -134,6 +137,13 @@ public function getSectionListFromId($id) {
    +    return $this->getContextValue('entity')->get('layout_builder__layout');
    

    Are there other places this should be used?

  3. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php
    @@ -28,6 +32,13 @@ class SectionStorageDefinition extends PluginDefinition {
    +    if (isset($definition['context'])) {
    +      foreach ($definition['context'] as $name => $context_definition) {
    +        $this->addContextDefinition($name, $context_definition);
    +      }
    +      unset($definition['context']);
    +    }
    

    This deserves a code comment. It reminds me of ContextAwarePluginBase but not quite?

  4. +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module
    @@ -58,3 +59,11 @@ function layout_builder_test_node_view(array &$build, EntityInterface $entity, E
    +  $storages['overrides']->setClass(OverridesSectionStorage::class);
    
    +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -0,0 +1,17 @@
    +    \Drupal::state()->set('layout_builder_test_context_applied', TRUE);
    +    return parent::getSectionListFromContext();
    

    Nice approach for the test!

tim.plunkett’s picture

Status: Needs review » Needs work
tim.plunkett’s picture

See also #2995071: Refactor LayoutBuilderLocalTaskDeriver to delegate local tasks to plugins as another part of LB which needs to delegate to different plugins in turn.
(Slightly different as that's during build-time not run-time)

I need to step through this more to see how we might do it as a CoR pattern

tim.plunkett’s picture

tim.plunkett’s picture

+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
@@ -239,11 +241,27 @@ public function buildMultiple(array $entities) {
+    if (empty($section_list) || count($section_list) === 0) {
+      $section_list = $this;
+    }

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
@@ -25,6 +25,9 @@
+ *   context = {
+ *     "entity_view_display" = @ContextDefinition("entity:entity_view_display"),
+ *   }

Discussed this more with @phenaproxima.
The confusing part for me was that I couldn't see how DefaultsSectionStorage would make it through filtering. The answer is, it won't. This last hunk with $section_list = $this was a special case representing defaults.

If we switch to a foreach loop over each plugin, call the new method, and check for truthiness before moving onto the next one.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
15.74 KB

Okay, let's see if this does the trick. I added a number of comments and changed getRuntimeSections() to use a chain-of-responsibility pattern. It will trundle through all applicable section storage types (i.e., the ones which matched the given contexts) until one returns a section list. If none of them do, the display itself will be used as the section list.

No interdiff because this required a re-roll. Sorry!

tedbow’s picture

+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
@@ -239,11 +241,35 @@ public function buildMultiple(array $entities) {
+        $storage = $storage_manager->loadEmpty($storage_type);
+        $storage->setContext('entity', $contexts['entity']);
+        $section_list = $storage->getSectionListFromContext();

How would this work if a contrib module wanted to say provide the ability to use the layout builder in other view modes by providing something like a OverridesSectionViewModeStorage?

Since the entity is context would be the same it seems like whichever storage type came first would win out. So with the entity context contrib could never provide the storage type unless some it made sure its storage types for evaluated first.

phenaproxima’s picture

So with the entity context contrib could never provide the storage type unless some it made sure its storage types for evaluated first.

That's right, which is why this patch allows the storage type plugins to be weighted. It's for precisely that reason.

tedbow’s picture

Status: Needs review » Needs work

Looks 'weight' logic was removed somewhere along the line

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
17.07 KB
2.88 KB

Lucky #42! This restores weighting, and tests it.

tedbow’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -239,11 +241,35 @@ public function buildMultiple(array $entities) {
    +      // Loop through every section storage plugin until we find one that
    +      // derives a valid section list from the context values.
    

    Can we remove "valid" here. We just loop till we fine 1. there is not validation here.

  2. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -239,11 +241,35 @@ public function buildMultiple(array $entities) {
    +        $storage = $storage_manager->loadEmpty($storage_type);
    +        $storage->setContext('entity', $contexts['entity']);
    +        $section_list = $storage->getSectionListFromContext();
    

    I still don't see how a contrib module could use this have there storage take over for only a particular view mode.

    I did some step debugging and I don't see anyway for the storage plugin to know what the current view mode is. Am I missing something?

    Could we also provide mode and even displayContext as context or otherwise let the storage know about these.

  3. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php
    @@ -36,6 +40,24 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
    +    uasort($definitions, function (SectionStorageDefinition $a, SectionStorageDefinition $b) {
    +      $a = [
    +        'weight' => $a->get('weight'),
    +      ];
    +      $b = [
    +        'weight' => $b->get('weight'),
    +      ];
    +      return SortArray::sortByWeightElement($a, $b);
    

    It seems weird weight as a sortable thing in plugin manager in a one off situation.

    I think it could easily lead to a developer looking at \Drupal\layout_builder\Annotation\SectionStorage and assume weight is a general thing that plugins support.

    Because if you look at the file there are weight is specific to this plugin type but the others aren't. So how would a developer know?

    parent::findDefinitions also already provides a way to reorder plugins.
    $this->alterDefinitions($definitions);
    couldn't you use that to reorder plugins?

  4. +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
    @@ -526,4 +527,35 @@ public function testBlockPlaceholder() {
    +    $state_key = 'layout_builder_test_context_applied';
    ...
    +    $this->assertTrue($state->get($state_key));
    

    There should be some kind comment and/or see to the test module to explain why checking this state value actually tests what we say it tests here.

  5. +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
    @@ -526,4 +527,35 @@ public function testBlockPlaceholder() {
    +    // Test that plugin weights are respected.
    +    /** @var \Drupal\layout_builder\SectionStorage\SectionStorageDefinition[] $definitions */
    +    $definitions = $this->container
    +      ->get('plugin.manager.layout_builder.section_storage')
    +      ->getDefinitions();
    +
    +    $this->assertSame('overrides', key($definitions));
    +    $this->assertSame(-10, $definitions['overrides']->get('weight'));
    +    $this->assertSame(0, $definitions['defaults']->get('weight'));
    

    Does this actually test that weights are respected? It just seems to test that you can get the property.

    It doesn't actually test that they are in the correct order, meaning the weights are respected.

  6. Is there issue to make \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::preSave() not assume layout_builder__layout Because that seems like same kind of problem
phenaproxima’s picture

Status: Needs work » Needs review
FileSize
17.83 KB
5.49 KB

Thanks for the review, @tedbow!

  1. Done.
  2. I've added a view_mode context value for this purpose, and test coverage.
  3. I left this as-is. The weight is important in making sure that the chain of responsibility is deterministic. We could set it in the alter hook, but I don't see the benefit of doing it that way vs. the way it's being done now. Besides, if we move to an alter hook, we effectively lose control of when the definitions are sorted, since AFAIK hooks don't run in a deterministic order. The alter hook is meant for contrib (and tests); we shouldn't need to alter our own plugin definitions. That said, I'm not married to it; if you insist on switching to the alter hook, I can do that. I did, however, add comments in the annotation class explaining what weight and contexts are used for, so hopefully that will make things clearer to other developers too.
  4. Added a comment.
  5. Improved the test coverage so that we are actually checking that weights are respected.
  6. I don't think that's really the same problem, so I don't know if any issue exists for that. I feel like @tim.plunkett would have a better grasp of what's needed there, if anything.
tedbow’s picture

  1. +++ b/core/modules/layout_builder/src/Annotation/SectionStorage.php
    @@ -22,6 +22,30 @@ class SectionStorage extends Plugin {
    +   * The plugin weight.
    +   *
    +   * When an entity with layout is rendered, section storage plugins are
    +   * checked, in order of their weight, to determine which one should be used
    +   * to render the layout.
    +   *
    

    👍

  2. +++ b/core/modules/layout_builder/src/Annotation/SectionStorage.php
    @@ -22,6 +22,30 @@ class SectionStorage extends Plugin {
    +   * When an entity with layout is rendered, all section storage plugins which
    +   * match a particular set of contexts are checked, in order of their weight,
    

    Maybe this is just general comment on the context system or my lack of understanding of it but..

    "match a particular set of contexts are checked" sounds weird. Because DefaultsSectionStorage is checked and it does not "match" the contexts in a sense that it is not aware of any contexts and will always be checked regardless of the what the entity context is.

  3. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -134,6 +137,16 @@ public function getSectionListFromId($id) {
    +    return $this->getContextValue('entity')->get('layout_builder__layout');
    

    Since the 'view_mode' context is now available why don't we check for it here.

    Isn't the intention of core's functionality to only take over for 'default'?

phenaproxima’s picture

@tim.plunkett and I had quite an interesting discussion yesterday about the direction this patch is taking, and how it affects the rest of Layout Builder's API. Let me see if I can summarize our findings...

The long and short of it is that leveraging the context system allows us to drop a fair amount of complexity from Layout Builder's plumbing. Right now, there are a few possible ways to load a section list, and they're a bit awkward; but in any case, the proper section storage ID must be discovered, then the section list must be derived from that, then the section list must actually be handed to the plugin. Loading a section list using context would add yet another way to do this basic legwork.

Tim and I agreed that the path forward is not to add Another Way To Do Things, but rather refactor parts of the section storage API on top of context. In other words, instead of doing a whole song and dance to figure out where the section list is, and then hand it to the section storage, we could just hand a storage ID to a storage plugin, it would derive context values from that, and in turn use those to derive the section list. That's not necessarily exactly what we will implement here, but it illustrates the point: context wil become the foundation of section storage, not just another integration.

This will take a fair amount of work and likely result in several interface methods being dropped. But, hey...beta experimental. I'll post a patch soon which will probably break a huge amount of tests, but at least serve to illustrate where this is heading.

phenaproxima’s picture

Hey kids, wanna break some stuff? Because this will break a ton of tests.

phenaproxima’s picture

Added a loadFromContext() method to SectionStorageManager and its interface.

phenaproxima’s picture

Tagging for title and issue summary update, since we're definitely gonna be having some API changes here.

phenaproxima’s picture

This should fix the unit tests.

phenaproxima’s picture

Status: Needs work » Needs review
FileSize
38.49 KB
2.68 KB

This should fix the remaining tests; all now pass on my local machine.

phenaproxima’s picture

Although it removes a lot of complexity, this patch also introduces many API changes to Layout Builder. Therefore, I think we'll have a better time landing this if we split it up into multiple issues, each of which blocks the next one. My suggestion for how we could proceed in separate issues:

  1. Add weighting to storage plugins.
  2. Make storage plugins and plugin definitions context-aware.
  3. Change the current loadFromRoute() logic so that it uses context under the hood.
  4. Add the loadFromContext() method to the section storage manager and have getRuntimeSections() delegate to that.
  5. Remove unnecessary methods from interfaces and implementations (e.g., loadFromStorageId() -- it is never called anywhere except in a test, and is obviated by the use of context as the basis for storage).
tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
62.33 KB
11.72 KB

Includes #2982183 and #3008431, we need both to fix this I believe. Let's see how many fails are left.

tim.plunkett’s picture

Status: Needs review » Postponed

Great! That passes. Now to push on those two blockers.

tim.plunkett’s picture

Posting an update with some changes. Still rolled on top of the above two issues.

phenaproxima’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
tim.plunkett’s picture

Here's a slimmed down version moving some code to #2986403: Create an easy way to get layout sections for an entity..

tim.plunkett’s picture

Title: LayoutBuilderEntityViewDisplay::getRuntimeSections() does not delegate to plugins » Allow section storage plugins to control their own requirements and ordering, especially for delegation during rendering
Issue summary: View changes
Issue tags: -Needs title update, -Needs issue summary update
phenaproxima’s picture

tim.plunkett’s picture

Once the final blocker lands, I will reupload the patch from #66, unless there are substantive reviews of that code between now and then.

tim.plunkett’s picture

All blockers are in!
Updated the comment about the sort to be more clear.

phenaproxima’s picture

Issue summary: View changes

Minor IS changes.

phenaproxima’s picture

Assigned: tim.plunkett » phenaproxima
Issue tags: +Needs change record

Assigning to myself to write the change record.

phenaproxima’s picture

Assigned: phenaproxima » Unassigned
Issue tags: -Needs change record
tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett
Issue summary: View changes
FileSize
58.29 KB
11.81 KB

Thanks for the CR, but as written it points out one thing that is bothering me.

TL;DR, I think we have a naming problem


Looking at SectionStorageManagerInterface we have 3 methods:

  1. loadEmpty($type)
    This is the main API entry point, it gives you an instance of the section storage of a given type.
    No contexts are provided, so many portions of the storage API will throw exceptions.
    The things that actually work here might as well be static methods (i.e. buildRoutes() and buildLocalTasks()).

    This does not call to $plugin->access().

  2. loadFromRoute($type, $value, $definition, $name, array $defaults)
    This is how all the routing code gets the section storage of a given type.
    Either $value has a magic string (for the Choose Section/Block pages), or it is provided within the keys of $defaults (for the main LB page).
    The instance returned here has been populated with context objects.

    This does not call to $plugin->access().

  3. loadFromContext(array $contexts)
    This is the new part. The biggest difference is that you do NOT specify the type.
    It loops through all available types until it finds the first plugin type that:
    a) has all of its context requirements satisfied
    b) returns TRUE from $plugin->access()

    It is the only method here to check access.
    It is the only method here to not be passed the $type.

Borrowing from the terminology of the larger plugin system, the first two methods are Factory methods.
(loadEmpty() is 1:1 a wrapper of FactoryInterface::createInstance and nothing more.)

loadFromContext() is more of a Mapper method, like MapperInterface::getInstance.
From those docs:

An array of options that can be used to determine a suitable plugin to instantiate and how to configure it.

The part that became clear to me was that we are missing a Factory method for when you know the type AND how to configure it.
If we were not adding these three custom methods to the plugin manager, we would have to rewrite them as follows:

loadEmpty($type) would become createInstance($type)
loadFromRoute($type, $value, $definition, $name, $defaults) would become createInstance($type, ['value' => $value, 'definition' => $definition, 'name' => $name, 'defaults' => $defaults])
loadFromContext($contexts) would become getInstance(['contexts' => $contexts])

Once again thinking about the larger plugin system, to me create vs get is not implicitly clear as a distinction between factory and mapper.
But even worse is turning all of them into load methods.

Finally, it is clear from the CR that we need a method that can be passed a $type as well as an array of $contexts.
As it turns out, we have that method in the form of protected function loadWithContextsApplied($type, array $contexts).
But that name is far too similar to loadFromContext().


How best to resolve the naming of the following methods
// Factory
  public function loadEmpty($type);
  public function loadFromRoute($type, $value, $definition, $name, array $defaults);
  // To be made public
  protected function loadWithContextsApplied($type, array $contexts);

// Mapper
  public function loadFromContext(array $contexts);

Oh btw I also wrote a test-only implementation of SectionStorageInterface, it felt bad to not have a third implementation, or not to have any solidly non-entity implementations.

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned
Issue summary: View changes
FileSize
59.41 KB
9.52 KB

Proposed:

// Factory
  public function loadEmpty($type);
  public function loadFromRoute($type, $value, $definition, $name, array $defaults);
  public function loadFromContext($type, array $contexts);

// Mapper
  public function findByContext(array $contexts);
tim.plunkett’s picture

After discussion with @EclipseGc, I settled on:

// Factory
  public function load($type, array $contexts);
  public function loadEmpty($type);

// Mapper
  public function findByContext($operation, array $contexts);

This moves the special loadFromRoute() which was only called once to be inline.

Okay I'm done refactoring, I promise.
Updating the CR

tim.plunkett’s picture

Issue summary: View changes

Updated IS to reflect last round of naming

phenaproxima’s picture

Didn't read the tests (yet), but I found very little to complain about here.

  1. +++ b/core/modules/layout_builder/src/Annotation/SectionStorage.php
    @@ -22,6 +22,30 @@ class SectionStorage extends Plugin {
    +   * @see \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::getRuntimeSections()
    +   */
    +  public $context = [];
    

    The @see should probably refer to SectionStorageManagerInterface::findBContext().

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
    @@ -237,34 +234,41 @@ protected function getEntityTypes() {
    +  /**
    +   * {@inheritdoc}
    +   */
    +  protected function extractEntityFromRoute($value, array $defaults) {
    

    Probably should not be an inheritdoc.

  3. +++ b/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php
    @@ -46,7 +46,8 @@ public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore
    +      $contexts = $this->sectionStorageManager->loadEmpty($defaults['section_storage_type'])->getContextsFromRoute($value, $definition, $name, $defaults);
    +      if ($section_storage = $this->sectionStorageManager->load($defaults['section_storage_type'], $contexts)) {
    

    This could benefit from a comment.

  4. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php
    @@ -72,4 +93,14 @@ public function set($property, $value) {
    +  /**
    +   * Returns the plugin weight.
    +   *
    +   * @return int
    +   *   The plugin weight.
    +   */
    +  public function getWeight() {
    +    return $this->weight;
    +  }
    

    Not really necessary but I guess it's harmless.

  5. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php
    @@ -39,33 +52,53 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
    +    $storage_types = array_keys($this->contextHandler->filterPluginDefinitionsByContexts($contexts, $this->getDefinitions()));
    

    Ideally I'd like a comment explaining why we didn't use ContextAwarePluginManagerTrait.

  6. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php
    @@ -15,51 +15,46 @@
    +   * @return \Drupal\layout_builder\SectionStorageInterface|null
        *   The section storage.
        */
    -  public function loadEmpty($id);
    +  public function load($type, array $contexts);
    

    The description should say when the method can return NULL. Also, should we default $contexts to an empty array, just in case some plugin is loadable even without any applied contexts? (No big deal either way.)

tim.plunkett’s picture

Thanks!

#78

1) Fixed
2) Fixed (and the other one in OverridesSectionStorage)
3) Commented and also slightly refactored (guard conditions ftw)
4) I like it. One less magic string (->get('weight'))
5) Fixed, in my own way
6) Fixed both

phenaproxima’s picture

Looks great to me! Thanks, @tim.plunkett.

phenaproxima’s picture

Tests look good too. Only minor things.

  1. +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/SimpleConfigSectionStorage.php
    @@ -0,0 +1,211 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  protected function setSections(array $sections) {
    +    $this->sections = array_values($sections);
    +    return $this;
    +  }
    

    Why is this needed?

  2. +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/SimpleConfigSectionStorage.php
    @@ -0,0 +1,211 @@
    +    $sections = array_map(function (Section $section) {
    +      return $section->toArray();
    +    }, $this->sections);
    

    I know it's a test class, so this is NBD, but can we use $this->getSections() instead of directly referencing $this->sections?

tim.plunkett’s picture

#81.1 It is abstract on the trait, so all of the rest of the trait has a single means to set the sections.
#81.2 Okay :)

phenaproxima’s picture

Nice. I would RTBC if I hadn't been so involved with this one :)

xjm’s picture

Priority: Normal » Major
tedbow’s picture

Status: Needs review » Needs work

This looks great couple questions/points. I haven't reviewed the test coverage yet.

settings to needs work for "extractEntityFromRoute" point

  1. +++ b/core/modules/layout_builder/src/Annotation/SectionStorage.php
    @@ -22,6 +22,30 @@ class SectionStorage extends Plugin {
    +   * Any required context definitions, optional.
    ...
    +  public $context = [];
    

    Should this plural because there could be more than one?

  2. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -274,11 +276,21 @@ public function buildMultiple(array $entities) {
    +    $sections = NULL;
    ...
    +    // If we don't have a section list yet (i.e., no section storage plugin
    +    // was able to derive a section list from context, or this display is not
    +    // overridable), use this display as the section list.
    +    return $sections ?: $this->getSections();
    

    Should $sections here be renamed $overridden_sections it would make condition statement in the return make more sense. Because it seems the logic is "if no overridden sections available call getSections"

  3. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -108,30 +106,42 @@ public function getStorageId() {
    +  public function getContextsFromRoute($value, $definition, $name, array $defaults) {
    

    I think extractEntityFromRoute() can now be moved into SectionStorageBase because the implementations in OverridesSectionStorage and DefaultsSectionStorage are identical.

  4. +++ b/core/modules/layout_builder/src/SectionStorageInterface.php
    @@ -79,27 +50,7 @@ public function getSectionListFromId($id);
    +   * Derives the required plugin contexts from route values.
    

    Would this always be *required* contexts? What if there was SectionStorage plugin that had option contexts that would inform which override would be in effect?

xjm’s picture

@tim.plunkett filed #3013773: Document LayoutBuilderEntityViewDisplay::getRuntimeSections() and consider renaming it for clarifying the purpose of the method that contains this bug.

EclipseGc’s picture

  1. +++ b/core/modules/layout_builder/src/Annotation/SectionStorage.php
    @@ -22,6 +22,30 @@ class SectionStorage extends Plugin {
    +  public $weight = 0;
    

    I get what you're doing here and generally approve. That being said, could we go with "priority" instead and flip the sorting logic? This would bring it in line with event dispatching and reduce the number of things we have that are different sorting mechanisms in core by 1. Not a cliff to die on, but I think it'd be nice to have.

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -23,6 +21,10 @@
    + *     "view_mode" = @ContextDefinition("string", required = FALSE),
    

    I don't see this actually used in the plugin anywhere. Oversight of some sort? I only see "$this->getContextValue('view_mode')" in the test coverage. I also don't see it returned in the getContextsFromRoute() method. Maybe that's expected, but I was expecting we'd derive all of all of that.

  3. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -108,30 +106,42 @@ public function getStorageId() {
    +    $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
    

    Seems like we could figure out whether or not this entity type is fieldable much earlier. Maybe that's not necessary, but none of this effort needs to happen if it's not a content entity in the first place since the override plugin can't deal with non-content-entities.

  4. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php
    @@ -39,33 +52,53 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
    +  public function loadEmpty($type) {
    +    return $this->createInstance($type);
    

    This is interesting. I have no objections to what you're doing here, but per other conversations we've had I think we should revisit some of this in general as we begin to move toward D9.

All in all I like this patch. It opens the door to do cool new stuff (like layout_builder for views as an example). Overall the patch looks good too. Mostly I'm just nitpicking here.

Eclipse

tedbow’s picture

re #87

1. I would second moving to priority unless we are using "weight" in relation to other plugins but I don't think we are. This is how normalizers work.
2. @see #43.2

phenaproxima’s picture

I would second moving to priority unless we are using "weight" in relation to other plugins but I don't think we are. This is how normalizers work.

Discussed with @tedbow and we agreed that we should call it weight only if other plugins, which use the same concept, call it "weight" as well. Otherwise, "priority" makes more sense.

However, IMHO both words will make sense to the kind of developers who will use this API. So this is not the hill I'm willing to die on.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
66.97 KB
5.93 KB

#85
1) Let's go one step further, since this isn't context, they are context definitions

2) Okay

3) It could for these two cases, but is not used by the new third (test-only) implementation. I'd prefer to keep entity-specific code out of the base class.

4) s/required/available

#87

1) (and #88/89) I'd much rather stick with weight here. Priority to me denotes Symfony events, which these are not.

2) As 88 says, 43.2 explains the use case. And we have tests... It is similar to why we pass as much information to calls to getFilteredDefinitions()

3) We actually do check this much earlier, see OverridesSectionStorage::getEntityTypes() which checks $entity_type->entityClassImplements(FieldableEntityInterface::class)
This instanceof check is really just functioning as a fancy inline @var :)

4)
Earlier in the patch, there is

-  public function loadEmpty($id) {
-    return $this->createInstance($id);

We're just renaming $id to $type here for consistency with the new load($type, array $contexts = [])

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

I think the #85 and #87 has been addressed.

Great work!

xjm’s picture

Issue tags: +8.7.0 release notes

This patch includes a hard break with the removal of four methods and will change how any storage plugins are implemented, so we probably should mention it in the release notes.

I asked whether it would make sense to use trigger_error()( (without the @, for broken/unintended behavior).

Per @tim.plunkett there's no real way around the break as the old code paths would be broken anyway by this change. Existing storage plugins will fatal anyway because they need to implement the new methods, and only an actual alternate layout building UI would be calling the methods externally, so coming up with a contorted way to raise deprecations before throwing an exception isn't really worthwhile either.

xjm’s picture

Status: Reviewed & tested by the community » Needs review

Just a few notes/nits so I don't lose them; still reading the patch.

  1. +++ b/core/modules/layout_builder/layout_builder.module
    @@ -202,3 +202,15 @@ function layout_builder_block_content_access(EntityInterface $entity, $operation
    +  // The \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage
    +  // plugin defines the entity context via its annotation but cannot specify the
    +  // constraint inline.
    

    Both @phenaproxima and @tim.plunkett explained this to me at some point but I still don't understand it from the comment.

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
    @@ -237,34 +234,49 @@ protected function getEntityTypes() {
    +   * @param mixed $value
    +   *   The raw value from the route.
    

    Can we really not be any more specific than mixed here? From the method body it looks like it's a string|null?

    Edit: Sorry, should have highlighted that this is for:

    +  protected function extractEntityFromRoute($value, array $defaults) {
    
  3. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php
    @@ -16,40 +15,17 @@
    -  protected function getSectionList() {
    -    if (!$this->sectionList) {
    -      throw new \RuntimeException(sprintf('%s::setSectionList() must be called first', static::class));
    -    }
    -    return $this->sectionList;
    -  }
    +  abstract protected function getSectionList();
    

    This change isn't in the CR (that SectionStorageBase no longer provides a default implementation of this method).

  4. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php
    @@ -103,4 +79,22 @@ public function removeSection($delta) {
    +   * @todo Remove after https://www.drupal.org/project/drupal/issues/2982626.
    

    This issue is RTBC so we can probably reroll to remove these and make the patch smaller, yay!

phenaproxima’s picture

Status: Needs review » Postponed

This issue is RTBC so we can probably reroll to remove these and make the patch smaller, yay!

Let's just briefly postpone this on #2982626: ContextAwarePluginBase is incompatible with ContextAwarePluginDefinitionInterface.

EclipseGc’s picture

FWIW, all my issues were addressed to my satisfaction.

Eclipse

phenaproxima’s picture

Blocker is in.

phenaproxima’s picture

Status: Postponed » Needs review

Meant to change the status...

tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett

Working on this

xjm’s picture

One other thing (point 2 below):

  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -23,6 +21,10 @@
    + *     "view_mode" = @ContextDefinition("string", required = FALSE),
    

    Huh. I have never seen this before (the required property for an annotation attribute. This is from Doctrine and used once or twice already for context stuff. Just not anywhere else.) :)

  2. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php
    @@ -39,33 +52,53 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
       /**
        * {@inheritdoc}
        */
    -  public function loadEmpty($id) {
    -    return $this->createInstance($id);
    ...
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function loadEmpty($type) {
    +    return $this->createInstance($type);
       }
    

    Seems like the only change here is internally changing the parameter name from $id to $type; is this in scope other than consistency with the naming for load() (which is new in this patch anyway)?

xjm’s picture

Status: Needs review » Needs work
+++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php
@@ -15,51 +20,46 @@
-  public function loadFromRoute($type, $value, $definition, $name, array $defaults);

This method removal is also missing from the CR. (At first I thought this might be an artifact of the goofy diff but it doesn't exist anymore with the patch applied AFAICS.)

xjm’s picture

  1. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php
    @@ -7,6 +7,11 @@
    + * Note that this interface purposefully does not implement
    + * \Drupal\Component\Plugin\PluginManagerInterface, as the below methods exist
    + * to serve the use case of \Drupal\Component\Plugin\Factory\FactoryInterface
    + * and \Drupal\Component\Plugin\Mapper\MapperInterface.
    

    This is also unclear from context (pun!).

  2. +++ b/core/modules/layout_builder/src/SectionStorageInterface.php
    @@ -79,27 +50,7 @@ public function getSectionListFromId($id);
    -  public function getRedirectUrl();
    ...
    -  public function getLayoutBuilderUrl($rel = 'view');
    
    @@ -110,21 +61,30 @@ public function getLayoutBuilderUrl($rel = 'view');
    +  public function getRedirectUrl();
    ...
    +  public function getLayoutBuilderUrl($rel = 'view');
    

    These aren't actually being removed; just a diff artifact

  3. +++ b/core/modules/layout_builder/src/SectionStorageInterface.php
    @@ -110,21 +61,30 @@ public function getLayoutBuilderUrl($rel = 'view');
    -  public function getContexts();
    

    But this one appears to be real and is missing.

  4. +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
    @@ -557,4 +558,58 @@ public function testBlockPlaceholder() {
    +  /**
    +   * Tests that the test implementation of Layout Builder works as expected.
    +   */
    +  public function testSimpleConfigBasedLayout() {
    

    This is not a great docblock. :P

  5. +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
    @@ -557,4 +558,58 @@ public function testBlockPlaceholder() {
    +    // The pre-existing section is found.
    +    $this->drupalGet('layout-builder-test-simple-config/existing');
    +    $assert_session->elementsCount('css', '.layout', 1);
    +    $assert_session->elementsCount('css', '.layout--twocol', 1);
    +
    +    // The default layout is selected for a new object.
    +    $this->drupalGet('layout-builder-test-simple-config/new');
    +    $assert_session->elementsCount('css', '.layout', 1);
    +    $assert_session->elementsCount('css', '.layout--onecol', 1);
    

    I didn't see this in the fixtures; is it being inherited? If so should it be documented?

Good test coverage.

phenaproxima’s picture

But this one appears to be real and is missing.

It's inherited from ContextAwarePluginInterface, which SectionStorageInterface now extends.

tim.plunkett’s picture

#101
2) The name of the parameter doesn't affect anything other than readability/consistency, and yes it was changed to match the new load().

#102
Fixed

#103
1) This was added in response to #78.5. I'm usually against adding comments explaining why we didn't do something or why someone shouldn't refactor it in the future, but I did so anyway. If you explain which part is unclear, maybe we can improve it?

3) As #104 says, it's now on a parent interface

4) Fixed

5) What do you mean?
The existing one is set up 2 lines above the code you highlighted, and the new one does not exist yet, hence the name.
The route is defined by \Drupal\layout_builder_test\Plugin\SectionStorage\SimpleConfigSectionStorage::buildRoutes().


Additionally, @phenaproxima and I realized that the access check for overrides was incomplete, it should check for emptiness when being used to view the layout, but can continue to return when used by the update UI.
Also, the context declaration on DefaultsSectionStorage clashed with the one on OverridesSectionStorage, but was being masked by the extra checks in getRuntimeSections. Fixed that up.

And finally, I had to move testRenderByContextAwarePluginDelegate() out to a new test class as the fixtures it needed were interfering with other test methods.

tim.plunkett’s picture

#3014871: Adapt to new changes in Layout Builder via the delegation issue is the Layout Library issue to implement these changes. The good news is that it works great, and Layout Library can finally function!

larowlan’s picture

  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php
    @@ -16,40 +15,17 @@
    +  abstract protected function getSectionList();
    

    Can you elaborate on why this is now abstract, this is an API change which would mean we would be wary of doing this in a minor. Any reason we can't do a fallback of sorts and trigger a deprecation error or similar?

  2. +++ b/core/modules/layout_builder/src/SectionStorageInterface.php
    @@ -110,21 +61,30 @@ public function getLayoutBuilderUrl($rel = 'view');
    +  public function getContextsFromRoute($value, $definition, $name, array $defaults);
    

    Can we provide a base implementation in SectionStorageBase for this? Would minimize API changes in minors, would mean we could consider backporting.

  3. +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/SimpleConfigSectionStorage.php
    @@ -0,0 +1,211 @@
    +class SimpleConfigSectionStorage extends ContextAwarePluginBase implements SectionStorageInterface, SectionStorageLocalTaskProviderInterface, ContainerFactoryPluginInterface {
    

    aka layout_library_lite in core ;), kind of like contact_storage

tim.plunkett’s picture

#108

1) Before we relied on the section list being injected into the plugin, and this was a simple getter.
Now this method must rely only on the context.

Defaults has a context named display with the type entity:entity_view_display

  protected function getSectionList() {
    return $this->getContextValue('display');
  }

Overrides has a context named entity with the generic type entity but an alter does ->addConstraint('EntityHasField', 'layout_builder__layout')

  protected function getSectionList() {
    return $this->getContextValue('entity')->get('layout_builder__layout');
  }

Between this required change and the necessary changes to SectionStorageManager, a BC layer is essentially impossible.

2) Continuing from above, I don't think that really helps much. If anything, not having it masked by an empty implementation will help clarify what is needed when fixing code.

3) In a sense, yes. I was really uncomfortable making more changes without a 3rd implementation to go against, and specifically wanted one that wasn't 100% tied to entities.

larowlan’s picture

OK - the need to implement protected function getSectionList() isn't listed in the change record.

I will defer to a release manager on the changes

tim.plunkett’s picture

Issue summary: View changes

Updated the IS a bit to explain why we cannot provide more of a BC layer here. Updating the CR now to address #110

tedbow’s picture

Status: Needs review » Needs work
  1. This comment was also confusing to me and chatted with @tim.plunkett about this. I think making this comment clear would be too complicated and explains the details how this patch changed which is unnecessary.
    I think it would be clearer to just delete this comment and add a comment to SectionStorageManager that explains that plugins should loaded via the methods on SectionStorageManagerInterface instead of via the methods on PluginManagerInterface or related.
  2. re #103.5 @xjm's question

    I didn't see this in the fixtures; is it being inherited? If so should it be documented?

    This test using the SimpleConfigSectionStorage storage manager which makes these routes. Since it just relies on simple config
    This code

    // Prepare an object with a pre-existing section.
        $this->container->get('config.factory')->getEditable('layout_builder_test.test_simple_config.existing')
          ->set('sections', [(new Section('layout_twocol'))->toArray()])
          ->save();
    

    Is all that is needed.

    I think this test should at least but a @see to SimpleConfigSectionStorage since this confusing where the route is coming from and that we don't need anything else setup.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
81.29 KB
2.55 KB

Great, thanks!

tim.plunkett’s picture

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned

If we didn't have the clash between the old \Drupal\layout_builder\SectionStorageInterface::getContexts() and the newly implemented (but way pre-existing) \Drupal\Component\Plugin\ContextAwarePluginInterface::getContexts(), it might have been possible to attempt a more graceful API break for existing plugins. (The full break for SectionStorageManagerInterface is not avoidable.)

But since the entire point of the issue is to make these plugins context aware, even if we were able to maintain BC for existing plugins, NONE of them would be loaded by the new SectionStorageManagerInterface::findByContext(), thereby breaking their use cases anyway.

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

reread all comments and patches since #102 when it was set to Needs work from RTBC. I think everything has been addressed.

Also reread the CR. Made 2 small changes.

xjm’s picture

Tagging per #110. This change is a hard break so we need to make sure we're confident about the BC break.

Can we add a parargaph summarizing the change and impacts for the release notes? See the "Important Update Information" in release notes of recent minors for examples of what to write, e.g.:
https://www.drupal.org/project/drupal/releases/8.5.0

tim.plunkett’s picture

Would it be reasonable to split the CR into 2? One for those providing implementations of SectionStorage, one for those interacting with the API externally?

xjm’s picture

Issue tags: +Needs release note

I think two CRs is a great idea and would help disentangle the info for developers.

Potential CR intro and first part of the release note:

The Layout Builder module is designed to support extensible storage for layouts, so that layouts can be created and saved for various usecases. (For example, the core module has two storages, one for default content layouts and another for individual entity layouts.) However, a serious bug with the the beta API prevents other storages from [XYZ].

And then for the release notes we could say:

If your module extends the layout builder with a custom SectionStorage implementation, blah with a link to that CR. If your module does blah blah blah with the API, blah blah other CR.

xjm’s picture

Status: Reviewed & tested by the community » Needs work

NW for the work on the CRs and the release notes; I think we're on a good track for that now.

tim.plunkett’s picture

Repurposed https://www.drupal.org/node/3012353 to be about the section storage manager changes, and wrote https://www.drupal.org/node/3016262 specifically for those providing existing plugins.

Added a release notes section to the IS

While writing the new CR, I encountered an oversight from before: while getContexts() should have been dropped from the plugins, it was not and was only not broken because nothing currently calls the new getContexts() method from the parent ContextAwarePluginBase class. Fixed that by introducing a new name for the old method, getContextsDuringPreview().
Added tests for this change.

Finally, removed workarounds for #2982626: ContextAwarePluginBase is incompatible with ContextAwarePluginDefinitionInterface now that it landed.

tim.plunkett’s picture

Status: Needs work » Needs review
Issue tags: -Needs release note
xjm’s picture

Good catch on #121. Just a few notes before I go to sleep:

  1. +++ b/core/modules/layout_builder/layout_builder.module
    @@ -203,3 +203,15 @@ function layout_builder_block_content_access(EntityInterface $entity, $operation
    +  // The \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage
    +  // plugin defines the entity context via its annotation but cannot specify the
    +  // constraint inline.
    

    It's still unclear to me what "but cannot specify the constraint inline" means or why that means we need this alter.

  2. Was my review from #98 addressed? (The above was raised back there.)
  3. In https://www.drupal.org/node/3016262:

    Corresponding to this change is the new protected method getSectionList(). This method is responsible for returning a section list, and should derive it from the context values available.

    It's not new; it's just a new requirement for each storage plugin to implement it individually. Previously there was a different implementation, but as described in #109 we can no longer rely on that default implementation because the section list is no longer injected. So this is one of the other changes an affected module must make.

tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett

I missed #95, will address that and the rest of #123 tomorrow

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
96.96 KB
2.28 KB

In #121 I added an optional param to getAvailableContexts(), and changed one call to pass TRUE. But in reality, it doesn't need the param, all of the calls need the new code path.

The fail patch in #121 is correct though.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
97.13 KB
2.51 KB

#95.1
I opened #3016420: Allow context definition annotations to specify constraints and adjusted this comment.

#95.2
This matches the docblock of \Drupal\Core\ParamConverter\ParamConverterInterface::convert(), and while it is always string|null in these implementations, it could also be int... Or anything, really.
I added an @see to convert() on deriveContextsFromRoute()

#95.3
That was added to the CR

#95.4
Just done recently

#123.1/2 point to #95

#123.3
Fair point https://www.drupal.org/node/3016262/revisions/view/11206150/11207096


Also fixed the spare `use` statement.

Status: Needs review » Needs work

The last submitted patch, 129: 2976148-delegate-129.patch, failed testing. View results

tim.plunkett’s picture

Split out the refactor from here, which will leave this issue to actually use the new API and fix the bug.

Here's a patch for after #3016473: Allow a single SectionStorage plugin to be returned for a set of available contexts goes in.

tim.plunkett’s picture

tim.plunkett’s picture

Title: Allow section storage plugins to control their own requirements and ordering, especially for delegation during rendering » Layout-based entity rendering should delegate to the correct section storage instead of hardcoding to either defaults or overrides
xjm’s picture

+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
@@ -266,20 +266,13 @@ public function buildMultiple(array $entities) {
-  protected function getRuntimeSections(FieldableEntityInterface $entity) {
-    if ($this->isOverridable() && !$entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()) {
-      return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
-    }
-
-    return $this->getSections();
+  private function sectionStorageManager() {

Rather than removing this method, we should remove the usage but change it to wrap:

@@ -240,8 +241,14 @@ public function buildMultiple(array $entities) {
+      $contexts = [
+        'view_mode' => new Context(ContextDefinition::create('string'), $this->getMode()),
+        'entity' => EntityContext::fromEntity($entity),
+        'display' => EntityContext::fromEntity($this),
+      ];
+      $contexts += $this->contextRepository()->getAvailableContexts();
+      $storage = $this->sectionStorageManager()->findByContext('view', $contexts);
+      if ($storage && $sections = $storage->getSections()) {

And then deprecate it and add a CR.

It's internal as a protected method, but:
https://www.drupal.org/core/deprecation#internal

While calling the method would now be expensive, expensive is better than broken for the 0-1 users out there who might be doing this. We shouldn't pick and choose where to apply the deprecation policy and we should only have breaks where it's a hard requirement to do so.

I discussed this with @tim.plunkett and he disagreed, but I feel pretty strongly it's important to follow the deprecation process consistently wherever and whenever possible.

xjm’s picture

Category: Task » Bug report

I keep meaning to set this to be a bug. The API was designed to support multiple, extensible storages, but because of this bug it's impossible to implement one effectively.

xjm’s picture

Status: Postponed » Needs work

#3016473: Allow a single SectionStorage plugin to be returned for a set of available contexts is in now, yay! So setting this to NW to be rerolled on top of that & with all the misc. things we scoped to this issue.

tim.plunkett’s picture

Re-re-re-reroll!
Also expanded the actual delegation test because the existing version was inconclusive IMO.

The FAIL patch has the weights removed from the Overrides and Defaults annotations, to prove that it actually matters now.
Drupal\Tests\layout_builder\Functional\LayoutBuilderTest::testOverrides() should fail as that explicitly tests that functionality. Many more tests will also likely fail due to implicitly relying on the behavior.

The last submitted patch, 137: 2976148-delegate-137-FAIL.patch, failed testing. View results

tim.plunkett’s picture

+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
@@ -275,11 +291,9 @@ public function buildMultiple(array $entities) {
   protected function getRuntimeSections(FieldableEntityInterface $entity) {
-    if ($this->isOverridable() && !$entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()) {
-      return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
-    }
-
-    return $this->getSections();
+    $contexts = $this->getContextsForEntity($entity);
+    $storage = $this->sectionStorageManager()->findByContext('view', $contexts);
+    return $storage ? $storage->getSections() : [];
   }

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -332,6 +333,12 @@ public function save() {
   public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
     $default_section_storage = $this->getDefaultSectionStorage();
     $result = AccessResult::allowedIf($default_section_storage->isLayoutBuilderEnabled())->addCacheableDependency($default_section_storage);
+
+    // Ensure there are sections available during view.
+    if ($operation === 'view') {
+      $overrides_available = ($default_section_storage->isOverridable() && count($this));
+      $result = $result->andIf(AccessResult::allowedIf($overrides_available))->addCacheableDependency($this->getEntity());
+    }
     return $return_as_object ? $result : $result->isAllowed();
   }

+++ b/core/modules/layout_builder/tests/src/Kernel/OverridesSectionStorageTest.php
+++ b/core/modules/layout_builder/tests/src/Kernel/OverridesSectionStorageTest.php
@@ -102,9 +102,13 @@ public function providerTestAccess() {

@@ -102,9 +102,13 @@ public function providerTestAccess() {
-    $data['view, enabled, no data'] = [TRUE, 'view', TRUE, []];
+    $data['view, enabled, no data'] = [FALSE, 'view', TRUE, []];

This is the key change of this issue. Before, OverridesSectionStorage::access() would return TRUE even if it was empty, and it relied on the hardcoded logic in getRuntimeSections to handle this check. Now that logic is correctly made completely internal and it will properly check its own access.

This necessitates the split into view and update operations, because overrides must be able to provide a UI even when there is no data (i.e. when trying to *add* data!)

xjm’s picture

  1. It's great to see the fail-coverage in #137, this (and the kernel I eventually found at the end of the patch from #3016473: Allow a single SectionStorage plugin to be returned for a set of available contexts) address my questions from #3016473-63: Allow a single SectionStorage plugin to be returned for a set of available contexts.2 and .5.
  2. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -249,13 +253,6 @@ public function buildMultiple(array $entities) {
    -        $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label);
    

    Ahh, there's the line that went into our workaround for getContextsDuringPreview().

  3. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -265,6 +262,25 @@ public function buildMultiple(array $entities) {
    +  /**
    +   * Gets the available contexts for a given entity.
    +   *
    +   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
    +   *   The entity.
    +   *
    +   * @return \Drupal\Core\Plugin\Context\ContextInterface[]
    +   *   An array of context objects for a given entity.
    +   */
    +  private function getContextsForEntity(FieldableEntityInterface $entity) {
    +    return [
    +      'view_mode' => new Context(ContextDefinition::create('string'), $this->getMode()),
    +      'entity' => EntityContext::fromEntity($entity),
    +      'display' => EntityContext::fromEntity($this),
    +      // @todo Remove in https://www.drupal.org/project/drupal/issues/3018782.
    +      'layout_builder.entity' => EntityContext::fromEntity($entity),
    +    ] + $this->contextRepository()->getAvailableContexts();
    +  }
    

    Private again. Are we really sure we don't want to allow a child class to use this helper in its own context/section handling?

  4. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -399,4 +413,14 @@ protected function getDefaultSection() {
    +  private function sectionStorageManager() {
    

    This as private totally makes sense.

  5. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
    @@ -23,6 +23,7 @@
      * @SectionStorage(
      *   id = "defaults",
    + *   weight = 20,
    
    +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -21,6 +21,7 @@
      * @SectionStorage(
      *   id = "overrides",
    + *   weight = -20,
    

    Can we add a line to the docblock above these why we picked each weight? (Because it needs to override/be overridden).

  6. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -332,6 +333,12 @@ public function save() {
       public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
         $default_section_storage = $this->getDefaultSectionStorage();
         $result = AccessResult::allowedIf($default_section_storage->isLayoutBuilderEnabled())->addCacheableDependency($default_section_storage);
    +
    +    // Ensure there are sections available during view.
    +    if ($operation === 'view') {
    +      $overrides_available = ($default_section_storage->isOverridable() && count($this));
    +      $result = $result->andIf(AccessResult::allowedIf($overrides_available))->addCacheableDependency($this->getEntity());
    +    }
         return $return_as_object ? $result : $result->isAllowed();
       }
    

    I'm still a little confused by this. So by default, access is granted if Layout Builder is enabled for the entity type. And then if the operation is view, then it's allowed if the entity is overridable. I guess this is special because layout building happens on the frontend when you're literally viewing the parent entity? I think the comment should say that. And it still sounds like "access is granted to everyone if the module is enabled!!" so we might want to document where else access control happens.
     

  7. I was expecting to see a change to the problematic code hardcoding the two storages in the IS in this issue. It doesn't seem to be touching that class anymore. This also wasn't one of the things we deprecated AFAIK. What happened to that part?
tim.plunkett’s picture

3) This is an entity class that we're _already_ swapping out. If someone needs to in turn swap out all of LB, they can. We can always make something protected later, but we can't go the other direction.

5) Writing the docs for Overrides makes sense. But when I went to write it for defaults, I realized it doesn't make sense for it to override the default value at all. After all, it is the default.
Though on rereading the patch, it feels _really silly_ to include that comment, as it is describing the entire purpose of the property which is documented on that property in the annotation...

6) Expanded comments and linked to #2914486: Add granular permissions to the Layout Builder as well.

7) That's in there, 5th hunk in LayoutBuilderEntityViewDisplay. I highlighted it in #139.

xjm’s picture

Though on rereading the patch, it feels _really silly_ to include that comment, as it is describing the entire purpose of the property which is documented on that property in the annotation...

Sorry, I meant why the specific integers were chosen. Weights are magic numbers; this is a problem basically as old as Drupal. So explaining why we specific magic number is chose and how it relates to the other magic numbers is important, even though the general purpose of the weight's existence is already documented elsewhere (when we've done our jobs).

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -19,6 +19,8 @@
+ * The weight is set to ensure it runs earlier than other implementations.

"Runs earlier" isn't really accurate. Only one storage "runs" (is picked off the top of the list).

So for overrides I was thinking something like:

The OverridesSectionStorage uses a negative weight to ensure it is picked over other implementations, since customizing an individual entity takes precedence. It must have a lighter weight than the DefaultSectionStorage.

@see \Drupal\blah\DefaultSectionStorage

For the default storage's weight, maybe it's useful to think about other hypothetical implementations. Zero should be whatever we want the default behavior to be for a custom implementation. If someone creates their own storage, what are the chances that they need their storage to override the defaults? What are the chances that they want defaults to override their storage? What are the chances that they don't care and we want it to just be whatever's first in the list according to that internal logic about when things are the same weight?

To me it seems like a positive integer for the default makes sense, because most custom implementations' default behavior should usually be to override the defaults when they're relevant for the context. If they want to also trump individual entity overrides, then they can go ahead and set a more negative weight, but that seems less common than the "almost always" that their implementation might want to override the bundle defaults.

xjm’s picture

  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
    @@ -441,7 +440,13 @@ public function getThirdPartyProviders() {
    +    // For all operations ensure that the Layout Builder is enabled for this
    +    // display.
    
    +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -331,14 +333,21 @@ public function save() {
    +    // For all operations ensure that the Layout Builder is enabled for the
    +    // corresponding default to this override.
    

    I'd reword this as:

    For all operations, access should only be granted if the Layout Builder is enabled for this display.

    And similarly:

    For all operations, access should only be granted if the Layout Builder is enabled for this display that this override corresponds to.

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
    @@ -441,7 +440,13 @@ public function getThirdPartyProviders() {
    +    //   https://www.drupal.org/node/2914486. For now all access is controlled
    
    +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -331,14 +333,21 @@ public function save() {
    +    //   https://www.drupal.org/node/2914486. For now all access is controlled
    

    Nit: "For now, all access..."

  3. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
    @@ -441,7 +440,13 @@ public function getThirdPartyProviders() {
    +    //   at the routing level by the 'configure any layout' permission.
    
    +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -331,14 +333,21 @@ public function save() {
    +    //   at the routing level by the 'configure any layout' permission.
    

    Should we @see the relevant code?

  4. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -331,14 +333,21 @@ public function save() {
    -    // Ensure there are sections available during view.
    +    // When viewing ensure that overrides are enabled and that there are
    +    // sections available.
    

    And maybe:

    Individual entity overrides are edited in the user interface when viewing an entity. For this reason, we grant access during the entity "view" operation, if overrides are enabled for the corresponding entity display and there are sections available.

xjm’s picture

For #140.7, I was searching for layout_builder__layout in the patch, but @tim.plunkett pointed me to #2936360: Remove duplicate references to the "layout_builder__layout" field from Layout Builder which already cleaned that up.

I checked over #3016473: Allow a single SectionStorage plugin to be returned for a set of available contexts and all my other questions are covered. The only remaining thing was access testing, and we add a number of appropriate data provider scenarios in this issue. The one thing I don't see is a functional test providing access is denied to the UI under the incorrect operation/storage combos. Do we already have a functional access test in HEAD that we can expand?

tim.plunkett’s picture

#142

Fair enough on the positive weight bit.

#143

1) Fixed
2) Fixed
3) Fixed
4) Discussed with @xjm and improved this, also filed #3019783: SectionStorageInterface implements AccessibleInterface but gives no insight into how it is used or should be implemented

Also opened #3019785: LayoutBuilderEntityViewDisplay should explain its purpose and better document the code flow

#144
\Drupal\Tests\layout_builder\FunctionalJavascript\LayoutBuilderOptInTest was written to test all of this logic, that issue should also have included kernel tests but did not, so this issue is expanding them.
This issue does NOT change the functional outcome of any of the access logic. It only moves the logic from 2 places to 1 (out of LayoutBuilderEntityViewDisplay::getRuntimeSections()).
(#2936358: Layout Builder should be opt-in per display (entity type/bundle/view mode) was the issue that added the logic and the function test).

xjm’s picture

Thanks! Review of the interdiff:

  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
    @@ -21,8 +21,14 @@
    + * DefaultsSectionStorage uses a positive weight as it must be picked after
    + * \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage and
    + * because the default weight is 0, allowing other implementations to easily
    + * take precedence.
    

    This gets a bit run-on so is hard to parse. How about:

     DefaultsSectionStorage uses a positive weight because:
    - It must be picked after
    \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage.
    - The default weight is 0, so other custom implementations will also take precedence unless otherwise specified.
    
  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
    @@ -440,13 +446,14 @@ public function getThirdPartyProviders() {
    +    // For all operations, access should only be granted if the Layout Builder
    +    // is enabled for this display.
    
    +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -333,21 +336,23 @@ public function save() {
    +    // For all operations, access should only be granted if the Layout Builder
    +    // is enabled for this display that this override corresponds to.
    

    I still think we need to explain, inline, what this method is actually doing. Per discussion with @tim.plunkett, "access" being "granted" isn't really exactly what's happening. These methods don't affect user access at all. They affect the conditions under which the storages' data might be used to render layouts; i.e. (I think) whether the section storage manager needs to ask these plugins "Hey, what have you got for me?". So the docs should explain what the method is actually doing, because it's confusing AF. The followup will help but we really can and should say inline here what the methods are doing. It's in scope here because it's specifically related to the addition of the update operation, and the impact of the code inside these implementations can be totally misunderstood without that explanation.

  3. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -333,21 +336,23 @@ public function save() {
    +    // During entity 'view' operation, ensure that overrides are enabled and
    +    // that there are sections available. See
    +    // \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
    

    The word "ensure" is still a problem here which I tried to explain on the phone. :) The sunsequent lines aren't ensuring that overrides are available or that there are sections available. That would mean doing things so that overrides are available etc. They're ensuring that overrides only act when those two conditions are true.

  4. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -333,21 +336,23 @@ public function save() {
    +    //   at the routing level by the 'configure any layout' permission, see
    

    permission. See

  5. :P

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
16.94 KB
25.51 KB

Interdiff is actually bigger than the new patch! Might be more worthwhile to read that instead.


Okay I really struggled with #146.2 and @xjm and I had to talk through it on Zoom for an hour.
The takeaway is this:
SectionStorageInterface::access() is used for routing access via \Drupal\layout_builder\Access\LayoutBuilderAccessCheck::access().
This logic is coincidentally similar to the checking that needs to happen within \Drupal\layout_builder\SectionStorage\SectionStorageManager::findByContext()

However, whether the plugin should be returned by findByContext() and whether it should deny access at the routing level are different concerns.

@xjm recalled that other plugins (widgets and formatters) have a method called isApplicable() that is used similarly.

So, this patch:

  • Switches findByContext() to use a new isApplicable() method
  • Adds a default implementation of isApplicable to SectionStorageBase
  • Moves the $operation = 'view' part of this patch from OverridesSectionStorage::access() to ::isApplicable()
  • Reverts the view/update changes from the earlier patch
  • Removes the $operation parameter from findByContext()
  • Reverts the change to OverridesSectionStorage's weight. I don't think it's actually special enough to have a higher weight, and the struggle to explain why it did convinced me. DefaultsSectionStorage's higher weight still stands and the expanded docs reference overrides.
  • Moves the workaround for #3018782: Remove extraneous context mapping of layout_builder.entity to the only place it absolutely needs to be (section rendering)

The interdiff looks like it removes other things but those are changes that are now unrelated that were being added by this patch.
Mostly along the lines of trying to differentiate between the two times access was being checked.

Finally, I went ahead and switched getContextsForEntity from private to protected, as requested.

tim.plunkett’s picture

+++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php
@@ -30,15 +30,13 @@ public function load($type, array $contexts = []);
-  public function findByContext($operation, array $contexts);
+  public function findByContext(array $contexts);

+++ b/core/modules/layout_builder/src/SectionStorageInterface.php
@@ -164,4 +164,12 @@ public function label();
+  public function isApplicable();

Just for the record, I think we should keep $operation in findByContext and pass it to isApplicable, "just in case". But I couldn't come up with a second use case for it, so I removed it per @xjm's request.

tedbow’s picture

I like the change to isApplicable() this makes much more sense to me!

xjm’s picture

Issue tags: +Needs followup

Yeah this makes so much more sense to me. One of those things where if you need to write a lot of docs about something then maybe it's the code you need to change and not the doc. :)

I'd be fine with an $operation parameter as an API addition in a followup if it turns out we need it.

I think we also want a followup to rename the strangely-named view operation currently used when editing layouts, but it's no longer coupled to this issue.

tim.plunkett’s picture

xjm’s picture

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -335,4 +335,12 @@ public function access($operation, AccountInterface $account = NULL, $return_as_
+  public function isApplicable() {
+    // Check that overrides are enabled and have at least one section.
+    return $this->getDefaultSectionStorage()->isOverridable() && count($this);
+  }

Is there any usecase for someone to purposefully create an empty layout with an override? Like the default has sections and for whatever reason they want to get rid of them and have no content inside the entity's display. The count() here would prevent that if someone ever wanted to deliberately create an empty layout. I guess they could still create one empty section and just not put stuff in it, but I wanted to raise the question since zero and null aren't necessarily the same thing (conceptually speaking I mean) and it's the sort of assumption about emptiness that can lead to perplexing bugs.

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned
+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
@@ -275,11 +294,8 @@ public function buildMultiple(array $entities) {
-    if ($this->isOverridable() && !$entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()) {

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -335,4 +335,12 @@ public function access($operation, AccountInterface $account = NULL, $return_as_
+    return $this->getDefaultSectionStorage()->isOverridable() && count($this);

We're swapping in the count() in place of the previous !...->isEmpty() check

LB has never supported empty layouts for overrides, not changing here.

tedbow’s picture

Status: Needs review » Needs work
+++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderSectionStorageTest.php
@@ -0,0 +1,79 @@
+    // During layout rendering, the storage plugin used for testing will set the
+    // state key to an array containing the plugin class, weight, and view mode,
+    // which proves that the plugin matched the appropriate contexts and was
+    // actually used to render the layout.
+    list ($class, $weight, $view_mode) = $state->get($state_key);
+    $this->assertSame(TestOverridesSectionStorage::class, $class);
+    $this->assertSame(-100, $weight);
+    $this->assertSame('default', $view_mode);

I feel like if there a @see to layout_builder_overrides_test_layout_builder_section_storage_alter() this test would a little more clear that we actually have 2 additional Section storage plugins using the same class different weights.

UPDATE:
Playing around with this I think there another confusing thing with the test.

The comment here says "During layout rendering" implying that only if the the section storage is used for rendering will the state value be set via \Drupal\layout_builder\SectionListInterface::getSections.

But getSections() is also called in \Drupal\layout_builder\Plugin\SectionStorage\SectionStorageBase::count() which is in turn called from \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::isApplicable().

At first I thought TestOverridesSectionStorage overrode isApplicable() because the testing node doesn't have any sections but we still want it to be used. Which is probably case. But even it also has the affect of avoiding a call to getSectionList() when the plugin is actually not used for rendering.

Could we change the test to check the the call to getSectionList() is for sure actually a call that is used for rendering and not some other reason(like count()).?

Maybe \Drupal\layout_builder_overrides_test\Plugin\SectionStorage\TestOverridesSectionStorage::getSectionList() could call the parent implementation and then add extra hardcoded section?

I guess my point is getSectionList() is called !== plugin was selected for rendering but the test seems to assume that

tim.plunkett’s picture

#154 is absolutely right that the current test is confusing and brittle.
Instead of altering and making copies of existing plugins, I provided a new one.
It always returns the same custom section when used, but its applicability is controlled by state.

The test now goes and adds a block to the defaults and then switches the state value to use the custom override.

The patch and interdiff with the -nocaching suffix are up to this point.

But that test fails.

Because this patch introduces a conditional within the rendering pipeline that didn't exist before.
Every plugin that is loaded during findByContext could affect the rendering based on the isApplicable() method.
Before, we accounted for the dependencies of overrides and defaults via their access() methods.
But this new test one is never cacheable, thanks to the state-based applicability.

So there are two options I see:
1) Make the plugin itself responsible for returning the cacheability info (as they now implement CacheableDependencyInterface)
2) Switch isApplicable() to using AccessResult return values

The second patch attempts to do approach #1. This means expanding the interface of findByContext and getRuntimeSections...

I haven't tried the other approach yet. But I'd bet @xjm won't like it because then there are two separate methods doing AccessResult logic...

The last submitted patch, 155: 2976148-delegate-nocaching-155.patch, failed testing. View results

xjm’s picture

Good catch, Ted!

Skimmed #155 only, but I think that approach is cleanest and I think it's good to make the cacheability dependencies explicit so that plugin authors think about it.

    +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -265,21 +274,38 @@ public function buildMultiple(array $entities) {
    +   * @param \Drupal\Core\Cache\CacheableMetadata|null $cacheability
    +   *   (optional) Cacheability metadata object, which will be populated based on
    +   *   the cacheability of each section storage candidate.
    
    +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php
    @@ -30,15 +31,16 @@ public function load($type, array $contexts = []);
    +   *   (optional) Cacheability metadata object, which will be populated based on
    +   *   the cacheability of each section storage candidate.
    

    This is a little unclear and makes it sound like this happens magically from some external force. What do I do with it in my implementation? The docs for the manager interface and the plugin interface should probably be different.

  1. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -240,7 +243,13 @@ public function buildMultiple(array $entities) {
    -      $sections = $this->getRuntimeSections($entity);
    +      $cacheability = new CacheableMetadata();
    +      $sections = $this->getRuntimeSections($entity, $cacheability);
    +
    +      // Apply cacheability metadata to the build array.
    +      $build_list[$id]['_layout_builder'] = [];
    +      $cacheability->applyTo($build_list[$id]['_layout_builder']);
    

    Looking at buildMultiple() in the EntityViewDisplay, we construct a new CacheableMetadata() object inline there and manually apply it to the build array. Will other authors know they need to do that from the current interface addition?

tim.plunkett’s picture

Here's a third approach: go back to using ::access() for both.

tedbow’s picture

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -332,6 +332,12 @@ public function save() {
   public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {

On thing that is weird of our use of \Drupal\Core\Access\AccessibleInterface::access() here is that the doc for the $account @param for that method says:

(optional) The user for which to check access, or NULL to check access
for the current user. Defaults to NULL.

In both of our implementations $account is ignored. Do we think other implementations will dependent on the value of $account?

The reason I mention this is because the @param doesn't say "to not check access based on an account pass NULL" it says if NULL is passed we should check access based on the currently logged in user.

Are there implementations of AccessibleInterface where the access really isn't $account dependent? Are we just using this because is the interface that returns AccessResultInterface or because we actually except $account to be used in some cases.

tim.plunkett’s picture

Agreed with #159. Here's another idea, interdiff is against #155

This does not address #157 yet

Status: Needs review » Needs work

The last submitted patch, 160: 2976148-delegate-160.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
34.65 KB
8.88 KB

Just posting a working version of that last approach, but after that going to take a step back and update the IS with the conundrum I've been trying to solve.

tim.plunkett’s picture

tim.plunkett’s picture

Why not have the same mechanism as findByContext and pass the CacheabilityMetadata to isApplicable, and keep the return value as TRUE/FALSE?
Interdiff against #155.

Also removes the optionality of cacheability in findByContext referred to in #157.1

And to answer #157.2, yes that is how a developer is supposed to interact with CacheabilityMetadata.

tim.plunkett’s picture

+++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php
@@ -92,12 +93,14 @@ public function load($type, array $contexts = []) {
+      // Each plugin used in this loop must be added as a cacheable dependency.
+      $cacheability->addCacheableDependency($plugin);
+      if ($plugin && $plugin->isApplicable($cacheability)) {

The remaining question is whether or not we *should* add the plugin here, or only rely on the manipulation within isApplicable().

We have test coverage for both.

xjm’s picture

Just skimmed the interdiff. The new approach seems much simpler.

One thing that's unclear to me though is how calling code will know they need to use this. Do they do `new CacheableMetadata()`? Is it their responsibility to make sure the same object is used each time they call it? Isn't the cacheability an intrinsic part of the storage rather than something that could arbitrarily be injected with any public method call?

Edit: I see that this was sort of addressed above; I'm just saying the docs provide no information about it. Edit 2: Also if there are docs somewhere else about it, we should link them. And calling code having to maintain two separate objects seems odd.

tedbow’s picture

Status: Needs review » Needs work
+++ b/core/modules/layout_builder/tests/modules/layout_builder_overrides_test/src/Plugin/SectionStorage/TestOverridesSectionStorage.php
@@ -0,0 +1,99 @@
+ *   weight = -100,

Could we change the weight here to -10? It would clearer I think that weight here being less than OverridesSectionStorage is not why it is being used in the test.

If we change the weight here then it would be more clear that \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::isApplicable() will be called but will return FALSE.

Otherwise think it looks very good.

tim.plunkett’s picture

#166

Cacheability is not intrinsic to SectionStorageInterface. Only to those section storages who opt into CacheableDependencyInterface, which SectionStorageBase does. Also see the test implementation of TestOverridesSectionStorage, which uses Drupal::state() in its isApplicable() and must set the cache max age to 0 to indicate that it is uncacheable.

+++ b/core/modules/layout_builder/tests/modules/layout_builder_overrides_test/src/Plugin/SectionStorage/TestOverridesSectionStorage.php
@@ -0,0 +1,99 @@
+  public function isApplicable(CacheableMetadata $cacheability) {
+    $cacheability->setCacheMaxAge(0);
+    return \Drupal::state()->get('layout_builder_overrides_test', FALSE);
+  }

calling code having to maintain two separate objects seems odd

Not sure exactly what you mean here. The calling code in this patch is as follows:

+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
@@ -240,7 +243,13 @@ public function buildMultiple(array $entities) {
+      $cacheability = new CacheableMetadata();
+      $sections = $this->getRuntimeSections($entity, $cacheability);
+
+      // Apply cacheability metadata to the build array.
+      $build_list[$id]['_layout_builder'] = [];
+      $cacheability->applyTo($build_list[$id]['_layout_builder']);

So yes, you must set up your own cacheability object via `new CacheableMetadata();` and yes you must call applyTo() for the render array at hand.
Which is true in every case where you encounter CacheableMetadata (the applying part, there are other ways to create the object).

The line I highlight in #165 is the equivalent of having every isApplicable() method doing $cacheability->addCacheableDependency($this); and I don't know if it is what we want here.

#167
Agreed. We can remove the weight completely here. I renamed the plugin to clarify that it really doesn't have anything to do with overrides.

Also after discussion with @tedbow, agreed to remove the blind caching based on the plugin, and ensure that isApplicable is responsible for itself.

tedbow’s picture

  1. Also after discussion with @tedbow, agreed to remove the blind caching based on the plugin, and ensure that isApplicable is responsible for itself.

    I think this is good because not everything in the plugin context will necessarily affect caching. so if all are added it the cache would be invalidated in cases when it doesn't need to be. Also there might be information outside of the plugin object that affect caching so any implementation of isApplicable() will have to make sure they added that information to $cacheability. there is not getting around that.

  2. So yes, you must set up your own cacheability object via `new CacheableMetadata();` and yes you must call applyTo() for the render array at hand.

    agreed this is pretty standard.

tim.plunkett’s picture

Adding credit for those that helped on now-duplicate issues.

xjm’s picture

Status: Needs review » Needs work

calling code having to maintain two separate objects seems odd

So yes, you must set up your own cacheability object via `new CacheableMetadata();` and yes you must call applyTo() for the render array at hand.
Which is true in every case where you encounter CacheableMetadata (the applying part, there are other ways to create the object).

I was referring to calling code, not storage implementations. The entity view display is a storage implementation, a subclass. But they are public methods, not protected, so available to external calling code as well. Which means that external calling code needs to keep passing this cacheability object to stuff, and if they accidentally pass the wrong object it seems like an easy way to introduce a bug. Every single public method now takes this cacheability metadata as a parameter, so you have to keep passing it to stuff. Normally I would expect the base class to contain the metadata as a protected property, and the methods to use it as needed.

Still think we need at least something in the parameter docs and an @see. It can be small. If I don't know what to do from reading the code/docs, that's a problem with the code/docs, not me.

NW for that. Really I'm just asking for this actually to be explained (or an explanation linked). Thanks!

tim.plunkett’s picture

Opened #3021846: Expand documentation of CacheableMetadata and RefinableCacheableDependencyInterface to give usage examples for starters. Ideally devs unfamiliar with CM would be able to click through to that class and learn everything they'd need to know.
In the meantime, we can be more helpful here.
Also making isApplicable() @internal.

@xjm and I worked through this interdiff together.

tim.plunkett’s picture

Discussed this with @Wim Leers and he pointed me to #2561773: CacheableMetadata is misnamed. That made me realize that we should *not* be typehinting with the class name but with the full interface name.

Wim Leers’s picture

Looks great! Some questions with a set of fresh eyes — I did not read the entire issue, so I'm sorry if I'm repeating some questions. 😅

  1. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -240,7 +244,13 @@ public function buildMultiple(array $entities) {
    +      $cacheability = new CacheableMetadata();
    
    +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
    @@ -21,8 +21,15 @@
    + * DefaultsSectionStorage uses a positive weight because:
    + * - It must be picked after
    + *   \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage.
    + * - The default weight is 0, so other custom implementations will also take
    + *   precedence unless otherwise specified.
    ...
    + *   weight = 20,
    
    +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -19,8 +20,15 @@
    + *   weight = -20,
    
    +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php
    @@ -92,12 +93,12 @@ public function load($type, array $contexts = []) {
    +      if ($plugin && $plugin->isApplicable($cacheability)) {
             return $plugin;
    

    The evaluation order of the different @SectionStorage plugins affects the result and the cacheability of the result due to the early return (the first applicable plugin wins).

    This means that changing the weight could affect the resulting decision and its cacheability.

    Which means that technically you always want the layout_builder_section_storage_plugins cache tag to be associated, since that is the cache tag associated with the cached discovery.

  2. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -240,7 +244,13 @@ public function buildMultiple(array $entities) {
    +      // Apply cacheability metadata to the build array.
    

    The render array is built based on decisions made by

  3. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -265,21 +275,41 @@ public function buildMultiple(array $entities) {
    +      'view_mode' => new Context(ContextDefinition::create('string'), $this->getMode()),
    

    Nit: view_mode_id (since it's just the ID, not the object)?

  4. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -265,21 +275,41 @@ public function buildMultiple(array $entities) {
    +   *   (optional) Cacheability metadata object, which will be populated based on
    ...
    +  protected function getRuntimeSections(FieldableEntityInterface $entity, RefinableCacheableDependencyInterface &$cacheability = NULL) {
    

    Why is it optional? This is an internal API since it's protected and there's only one caller.

  5. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php
    @@ -108,4 +109,11 @@ public function getContextsDuringPreview() {
    +  public function isApplicable(RefinableCacheableDependencyInterface $cacheability) {
    +    return TRUE;
    +  }
    

    I expected this to be abstract — can you explain why this is a sensible default implementation? Wouldn't you want every @SectionStorage plugin developer to think this through?

  6. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php
    @@ -30,15 +31,24 @@ public function load($type, array $contexts = []);
    +   * @see \Drupal\Core\Cache\CacheableMetadata
    
    +++ b/core/modules/layout_builder/src/SectionStorageInterface.php
    @@ -164,4 +165,23 @@ public function label();
    +   * @see \Drupal\Core\Cache\CacheableMetadata
    

    @see \Drupal\Core\Cache\RefinableCacheableDependencyInterface

  7. +++ b/core/modules/layout_builder/src/SectionStorageInterface.php
    @@ -164,4 +165,23 @@ public function label();
    +   * Returns if the section storage should be selected during plugin mapping.
    ...
    +   *   information that affects whether the storage is applicable.
    ...
    +   *   TRUE if the section storage can be used, FALSE otherwise.
    

    s/the/this/ ?

  8. +++ b/core/modules/layout_builder/src/SectionStorageInterface.php
    @@ -164,4 +165,23 @@ public function label();
    +   *   Cacheability metadata object, typically provided by the section storage
    

    Perhaps s/Cacheability metadata object/Refinable cacheability object/

    ?

tim.plunkett’s picture

#176

1) @tedbow pointed this out before and I pushed back, but I guess we might as well. Because plugin managers themselves implement CDI, we can do this instead of reusing the string.

2) That seems like a doc suggestion but it trails off and I'm unclear what to do. For now switched it to // Apply cacheability information to the build array.

3) Yes, though this string is not added here and matches the existing annotation on OverridesSectionStorage

4) Somehow in the past couple iterations I reintroduced the usage of this method, which should be deprecated in #2986403: Create an easy way to get layout sections for an entity..
Even though it is a protected method in an @internal entity class override in an experimental module, @xjm insisted it remain intact. So the method signature is not changed here.

5) This was also done for BC purposes, but you're entirely correct. Developers are currently relying on the return value of ::access() and need to account for the switch to this method.
This necessitates a CR.

6) Fixed

7) Fixed

8) Fixed

xjm’s picture

Even though it is a protected method in an @internal entity class override in an experimental module, @xjm insisted it remain intact.

Again, it is documented policy:

Changing internal experimental module APIs without BC has been literally the #1 source of people being outraged about Drupal minor upgrades breaking their sites. If we can provide BC, we should, unless there is literally no way to fix a major/critical bug without the BC break.

tim.plunkett’s picture

"Insisted" was bad word choice on my part. "@xjm made it clear why it was important, here are the links as to why" would have been better.

Wim Leers’s picture

#177: thanks!

  1. Yes, sorry, this was an unfinished doc suggestion; I meant to write The render array is built based on decisions made by @SectionStorage plugins and therefore it needs to depend on the accumulated cacheability of those decisions. It's just a suggestion though. I'm trying to make it more explicit, more clear. The current comment IMHO doesn't add much information.
  1. Got it. Agreed with need for CR. I also think that this then needs a @todo for making it abstract in Drupal 9.
tedbow’s picture

Status: Needs review » Needs work
  1. re #180 I think this some other comment that mentions the decision process and cacheble info would be good. this issue has been confusing so others also might not get what is going on here.
  2. +++ b/core/modules/layout_builder/src/SectionStorageInterface.php
    @@ -164,4 +165,23 @@ public function label();
    +   * Returns if this section storage should be selected during plugin mapping.
    

    Nit: I don't think we should say "should be selected" here because there are other factors, namely plugin weight that determine what should be selected.

    maybe "Determines if this section storage is applicable given the current contexts." or something

  3. +++ b/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php
    @@ -249,11 +252,59 @@ public function testFindByContext($plugin_access) {
    +    $first_plugin->getCacheContexts()->willReturn([]);
    ...
    +    $first_plugin->getCacheMaxAge()->willReturn(-1);
    

    These 2 lines for getCacheContexts() and getCacheMaxAge() can be commented out and the test still passes.

    We should remove them or test for these in $cacheability like we do for getCacheTags()

tim.plunkett’s picture

#180
1) Fixed
2) It doesn't need to be abstract as it's on the interface and not on the base class.

#181
1) I think this is addressed.
2) Fixed
3) Ah this changed when we removed the addCacheableDependency($this). Instead I'm switching these to shouldNotBeCalled().

Working on the CR now.

tim.plunkett’s picture

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

Looks Good! 🎉

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderSectionStorageTest.php
    @@ -0,0 +1,88 @@
    +    // Create two nodes.
    +    $this->createContentType(['type' => 'bundle_with_section_field']);
    +    $this->createNode([
    +      'type' => 'bundle_with_section_field',
    +      'title' => 'The first node title',
    +      'body' => [
    +        [
    +          'value' => 'The first node body',
    +        ],
    +      ],
    +    ]);
    

    How many nodes? :) I'd remove the comment - the code is clear enough.

  2. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -265,21 +278,43 @@ public function buildMultiple(array $entities) {
    +  protected function getRuntimeSections(FieldableEntityInterface $entity, RefinableCacheableDependencyInterface &$cacheability = NULL) {
    

    One concern I have is now this is never called which feels dangerous. I think it would be better to call this in ::buildMultiple() and then do the deprecation and moving of functionality in https://www.drupal.org/node/2986403

    Tricky. For me this is why classes like this should be final. Like all things marked internal or considered internal-by-default because then we could safely change or remove this because we would know that there cannot be any code that calls it. See #3019332: Use final to define classes that are NOT extension points

    Anyhow unused code that is not tested is another place we've been caught out in the past so perhaps the simplest thing is to call this. Ah... I see you need all the parts of this for later in ::buildEntity() i.e. $contexts

    I guess we'd have to do something like protected function getRuntimeSections(FieldableEntityInterface $entity, RefinableCacheableDependencyInterface &$cacheability = NULL, &$contexts = NULL) {

    If people feel this isn't the way to go then we need to add a test that leverages the untested / unused code somehow.

xjm’s picture

Status: Needs work » Needs review

Sorry, my review was apparently never posted 5 airports ago. Thank you Chrome...

So the new typehint is to something that is more clearly named and that has at least an extra sentence or two of docs.

However, now the typehint and the thing we're actually constructing/using aren't totally the same. applyTo() also does not exist on the interface.

  1. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -240,7 +244,17 @@ public function buildMultiple(array $entities) {
    +      $cacheability = new CacheableMetadata();
    +      $contexts = $this->getContextsForEntity($entity);
    +      $storage = $this->sectionStorageManager()->findByContext($contexts, $cacheability);
    +      // The render array is built based on decisions made by @SectionStorage
    +      // plugins and therefore it needs to depend on the accumulated
    +      // cacheability of those decisions.
    +      $cacheability->applyTo($build_list[$id]['_layout_builder']);
    

    The part in LEVD::buildMultiple() is maybe okay because the applyTo() call is just a few lines after we explicitly construct a CacheableMetadata object. And buildMultiple() itself isn't getting it from anything else; it's just using the object internally.

  2. @@ -265,21 +278,43 @@ public function buildMultiple(array $entities) {
    +   * @todo Deprecate this method in https://www.drupal.org/node/2986403.
    ...
    +  protected function getRuntimeSections(FieldableEntityInterface $entity, RefinableCacheableDependencyInterface &$cacheability = NULL) {
    +    if (!$cacheability) {
    +      $cacheability = new CacheableMetadata();
    
    +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php
    @@ -92,12 +93,16 @@ public function load($type, array $contexts = []) {
    -  public function findByContext($operation, array $contexts) {
    +  public function findByContext(array $contexts, RefinableCacheableDependencyInterface $cacheability) {
    

    The fact that getRuntimeSections() accepts anything that implements RCDI, but (for BC reasons) responds to it being null by constructing specifically a CacheableMetadata object is a little weirder. Maybe this is okay if the method is eventually getting deprecated (which means that, if nothing else, people won't be encouraged to use it for code examples).

  3. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php
    @@ -30,15 +31,24 @@ public function load($type, array $contexts = []);
    +   * @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability
    +   *   Refinable cacheability object, which will be populated based on the
    +   *   cacheability of each section storage candidate. This is typically created
    +   *   directly before this method call and must be applied to a render array
    +   *   after this method call.
    

    Also with this typehint our doc deficiency workaround of "must be applied to a render array after this method call" is also slightly off, because the method to "apply" it doesn't exist on RCDI. This might be okay because even if you're somehow using a RCDI implementation that doesn't include applyTo() specifically will need to, er, apply the information somehow... although it may also still not apply to a render array then? (I do still think the docs are worthwhile to explain the caller's responsibilities and what findBContext() does and doesn't do.)

All these things together make me wonder if we're still missing something. Maybe this is all just pointing to some shortcomings in the dependency caching API itself.

Thoughts?

xjm’s picture

Status: Needs review » Needs work

Didn't mean to change status.

IMO #185.2 proposing adding (back?) tests for getRuntimeSectionsis needless work as it's to be deprecated shortly (although that said I was already wondering why we aren't just deprecating it in this issue; I thought in the blocker that the method was only being kept around for this issue).

#3019332: Use final to define classes that are NOT extension points is also irrelevant because (a) this method was introduced in a tagged release a year ago (prior to 8.5.0 beta) and (b) it's also not any of the internal APIs listed in that issue's summary anyway. Let's not drag it in here as it's off-topic.

alexpott’s picture

Deprecating something doesn't mean removing tests for it. There needs to be at least one test remaining that triggers the deprecation error otherwise it is just dead code which is better to remove than to have it lying around ready to be broken. If we marked all @internal code final then changing or removing this protected method would have no repercussions.

xjm’s picture

I meant to mention in #186 that the @todo to #3021846: Expand documentation of CacheableMetadata and RefinableCacheableDependencyInterface to give usage examples didn't end up in the comment either; @tim.plunkett, did you express a preference in Slack or a meeting to not include an @todo for it? I forget.

Also adding the related issues which the xpost removed.

We can't mark this final now because it would break BC because it's been in the codebase since 8.5.0-beta1. Planning to mark it final later is also not worthwhile because we're already planning to remove it elsewhere. Not in scope for this issue.

Wim Leers’s picture

+++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php
@@ -30,15 +31,24 @@ public function load($type, array $contexts = []);
+   *   cacheability of each section storage candidate. This is typically created
+   *   directly before this method call and must be applied to a render array
+   *   after this method call.

IMHO This is typically created directly before this method call and must be applied to a render array after this method call. can be omitted altogether. In fact, I think it should be omitted.

Just Refinable cacheability object, which will be populated based on the cacheability of each section storage candidate. already describes why this parameter exists. We don't need to spell out details of what every piece of information will be eventually used for.

+++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php
@@ -30,15 +31,24 @@ public function load($type, array $contexts = []);
+   * After calling this method the $cacheability parameter will reflect the
+   * cacheability information used to determine the correct section storage.
+   * This must be applied to any output that uses the result of this method.

This is already talking about the same thing, and talks about it in a more abstract manner i.e. not coupled to the specific default implementation of this interface). This must be applied to any output that uses the result of this method. is accurate.

(Slightly more accurate would be s/must be applied to/must be associated with/ — the "applied to" is implicitly referring to \Drupal\Core\Cache\CacheableMetadata::applyTo(), which just happens to be the name of a convenience method used to associate cacheability with a render array. The output may also be something other than a render array, and then "apply" may be weird, "associate" is always accurate.)

If desired, that piece of documentation before the @param docs could be moved inside the @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability doc.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
25.72 KB
5 KB

#185
1) Fixed :)

2) Fixed.

#186
Addressed via the feedback in #190!

#187/188
No feedback to address here, but yes. Adding final when introducing this would have been the right move. Too late now.
Not deprecating it in this issue because we still have to figure out #2986403: Create an easy way to get layout sections for an entity. (where I think we'll end up doing the deprecation fully)

#189
I don't think LB code or docs will change based on the outcome of that issue, so I did not include an inline @todo to that issue.

#190
Thanks this makes it a lot clearer!

tim.plunkett’s picture

@Wim Leers pointed out that the default value for cacheability can be an empty array.

xjm’s picture

Per @tim.plunkett getRuntimeSections() is not deprecated because a copy of its code also exists in a trait somewhere related to the method, and #2986403: Create an easy way to get layout sections for an entity. will address both cases. (The first part wasn't clear to me from #191 so documenting it here.) I'm fine with just retaining the usage here. The fact that it doesn't have existing unit tests is weird but that's existing technical debt, not a problem introduced by this issue.

Interdiffs look fine to me. I'm all for initializing arrays to be empty arrays rather than null and the docs.

I'm not sure #186 is actually addressed though?

tim.plunkett’s picture

Further clarifying on the final discussion:

Core coupled the storage of entity view display entity information with the runtime rendering of entities.
If those two things were separate, we'd definitely want to mark the class with rendering as final.

But it's not, so we don't want to do that. Even if we had had the foresight to mark it final originally, we would probably need to unfinalize it when contrib really got going.

#186.3 is addressed.
#186.1 and .2 I think are fine, as there is no more pure implementation of RCDI than CacheableMetadate at the moment. And the BC layer in getRuntimeSections should mirror the code path in buildMultiple, hence the same class.

xjm’s picture

#186.1 and .2 I think are fine, as there is no more pure implementation of RCDI than CacheableMetadate at the moment. And the BC layer in getRuntimeSections should mirror the code path in buildMultiple, hence the same class.

Can we document this in the code somehow? I think this will come up again when we revisit things in #2986403: Create an easy way to get layout sections for an entity..

tim.plunkett’s picture

Turned out it was easier to deprecate getRuntimeSections() here than punt.

Opened https://www.drupal.org/node/3022574 for the deprecation.

I split the super important and delicate part of buildMultiple into a private method.
This also made it easier to reference from the documentation within getRuntimeSections().

Also added a test for the deprecated code.

Wim Leers’s picture

Looks sensible to me, but I'm no expert in Layout Builder so I'm not sure I have insight into all consequences. So leaving the RTBC'ing to somebody else.

xjm’s picture

    The refactor and deprecation here seem totally sensible. I'd like to think a little about the implications of making the method private;
  1. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -308,9 +326,15 @@ protected function getContextsForEntity(FieldableEntityInterface $entity) {
    +    // For backwards compatibility mirror the functionality of ::buildSections()
    

    backwards compatibility, mirror

    (Unless there is a "Backwards Commpatibility Mirror" time travel device of some sort. I would really like to get my hands on it if so.)

xjm’s picture

Status: Needs review » Needs work

The CR could use a little work I think. It says:

Instead, \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::findByContext() should be used in conjunction with \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::getContextsForEntity().

I think this could use:

tim.plunkett’s picture

#200!

This switches from private to final protected, which fits the intention more.
Also addressed #198 :D

I updated the CR with an example and some more notes.

xjm’s picture

+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
@@ -238,48 +243,106 @@ public function buildMultiple(array $entities) {
+      // If there are any sections, remove all display configurable fields from
+      // the existing build.

This comment appears to be new. I read it and asked "what is a display configurable field? Too many nouns." Had to read the code to figure out what it meant.

How about:

Remove all fields with configurable display from the existing build.

...Followed by why we're doing that.

tim.plunkett’s picture

The addition of $cacheability and $contexts to getRuntimeSections() is no longer required since we're fully deprecating it here.

While fixing #201, I was examining buildMultiple() more closely, and realized there was still a bug.
All section storages were coupled directly to defaults. If defaults were off, nothing would run.
This runs counter to the entire point of this issue: that contrib/custom SectionStorage plugins should have unfettered access to the build pipeline.

Fixing this also cleaned up some other questionable bits of code:

  • Why was view_mode an optional context? What happened if you did or didn't provide one?
  • Why did DefaultsSectionStorage always return TRUE from isApplicable(), yet have a guard condition in buildMultiple()?
  • How was it exactly that we prevented an infinite loop when rendering field blocks for custom blocks (which also have the ability to use LB)?
  • Why did isOverridable() return TRUE even if LB was disabled?
  • Why did one of the workarounds for #2907413: Consider supporting Layout Builder Overrides for other view modes use full even though the other used default?

All of that came from removing the defaults-specific guard condition in buildMultiple().
Now none of the section storages have any more power than the others, and the system is truly fair.

Local commits:

966b14d37a Remove unnecessary flexibility additions from getRuntimeSections
bd55ed72c9 Fix comment per #201
3a1759a825 Test coverage for fixing default-required-ness
b869a09169 Fix default-required-ness

FAIL patch is without that last commit (so the test is in but not the fix).

tim.plunkett’s picture

Status: Needs review » Needs work
Issue tags: +Needs update path
  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -31,7 +31,7 @@
    - *     "view_mode" = @ContextDefinition("string", required = FALSE),
    + *     "view_mode" = @ContextDefinition("string"),
    

    Ahhhh this needs an "update path" of clearing caches. Will add one next patch. No update path test possible for that.

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -285,9 +285,7 @@ protected function getEntityTypes() {
    -    // @todo Expand to work for all view modes in
    -    //   https://www.drupal.org/node/2907413.
    

    Note that this issue is still in another @todo in the LB UI code

xjm’s picture

Fail-patch failed but the non-fail patch failed even more. :D

tim.plunkett’s picture

Very true :)

Turns out we had test coverage for that full view mode bit, but at least we're moving the hardcoding from a one-off spot to the context level.

tim.plunkett’s picture

Interdiff all the way back to #182 when this was last RTBC

xjm’s picture

  1. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -63,6 +67,10 @@ public function setOverridable($overridable = TRUE) {
    +    // Layout Builder cannot be enabled for the custom view mode.
    

    This comment begs the question "why"?

  2. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -234,37 +242,76 @@ protected function contextRepository() {
    +  final protected function buildSections(FieldableEntityInterface $entity) {
    

    This worked its way back in.

  3. How was it exactly that we prevented an infinite loop when rendering field blocks for custom blocks (which also have the ability to use LB)?

    Should that be tested explicitly?

tim.plunkett’s picture

#207
1) Good call, that was very not clear.

2) Removed that. Without a solid core policy, it's not worth opening that can of worms.

3) That is specifically fixed by the code mentioned in point 1. It was fine in HEAD, but only coincidentally by the now-removed $this->isLayoutBuilderEnabled() check in buildMultiple(). Undoing that return FALSE causes all of the functional and functional JS tests to fail. Idk that it needs to be explicitly tested more

Also removed an extra leftover debugging line from the test.

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

I went through all the comments since #182 and this seems to be good to go!

  • xjm committed 9065203 on 8.7.x
    Issue #2976148 by tim.plunkett, phenaproxima, xjm, tedbow, Wim Leers,...
xjm’s picture

Status: Reviewed & tested by the community » Fixed

Alright, I think this is well and truly done. Committed and pushed to 8.7.x. Thanks!

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned

Woo! Thanks all.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.