#208 | 2976148-delegate-applicable-208-interdiff.txt | 2.01 KB | tim.plunkett |
#208 | 2976148-delegate-applicable-208.patch | 35.41 KB | tim.plunkett |
|
#206 | 2976148-delegate-applicable-182-to-205-interdiff.txt | 18.57 KB | tim.plunkett |
#205 | 2976148-delegate-applicable-205-interdiff.txt | 2.92 KB | tim.plunkett |
#205 | 2976148-delegate-applicable-205.patch | 35.33 KB | tim.plunkett |
|
#202 | 2976148-delegate-applicable-202-interdiff.txt | 11.9 KB | tim.plunkett |
#202 | 2976148-delegate-applicable-202-PASS.patch | 33.22 KB | tim.plunkett |
|
#202 | 2976148-delegate-applicable-202-FAIL.patch | 29.3 KB | tim.plunkett |
|
#200 | 2976148-delegate-applicable-200-interdiff.txt | 1.73 KB | tim.plunkett |
#200 | 2976148-delegate-applicable-200.patch | 29.91 KB | tim.plunkett |
|
#196 | 2976148-delegate-applicable-196-interdiff.txt | 7.36 KB | tim.plunkett |
#196 | 2976148-delegate-applicable-196.patch | 29.9 KB | tim.plunkett |
|
#192 | 2976148-delegate-applicable-192-interdiff.txt | 1.49 KB | tim.plunkett |
#192 | 2976148-delegate-applicable-192.patch | 25.72 KB | tim.plunkett |
|
#191 | 2976148-delegate-applicable-191-interdiff.txt | 5 KB | tim.plunkett |
#191 | 2976148-delegate-applicable-191.patch | 25.72 KB | tim.plunkett |
|
#182 | 2976148-delegate-applicable-182-interdiff.txt | 3.37 KB | tim.plunkett |
#182 | 2976148-delegate-applicable-182.patch | 25.74 KB | tim.plunkett |
|
#177 | 2976148-delegate-applicable-177-interdiff.txt | 8.86 KB | tim.plunkett |
#177 | 2976148-delegate-applicable-177.patch | 25.63 KB | tim.plunkett |
|
#175 | 2976148-delegate-applicable-175-interdiff.txt | 10.63 KB | tim.plunkett |
#175 | 2976148-delegate-applicable-175.patch | 25.49 KB | tim.plunkett |
|
#174 | 2976148-delegate-applicable-174-interdiff.txt | 2.78 KB | tim.plunkett |
#174 | 2976148-delegate-applicable-174.patch | 25.01 KB | tim.plunkett |
|
#168 | 2976148-delegate-applicable-168-interdiff.txt | 5.59 KB | tim.plunkett |
#168 | 2976148-delegate-applicable-168.patch | 24.26 KB | tim.plunkett |
|
#164 | 2976148-delegate-applicable-164-interdiff.txt | 12.34 KB | tim.plunkett |
#164 | 2976148-delegate-applicable-164.patch | 24.32 KB | tim.plunkett |
|
#162 | 2976148-delegate-162-interdiff.txt | 8.88 KB | tim.plunkett |
#162 | 2976148-delegate-162.patch | 34.65 KB | tim.plunkett |
|
#160 | 2976148-delegate-160-interdiff.txt | 21.01 KB | tim.plunkett |
#160 | 2976148-delegate-160.patch | 32.13 KB | tim.plunkett |
|
#158 | 2976148-delegate-158-interdiff.txt | 10.55 KB | tim.plunkett |
#158 | 2976148-delegate-158.patch | 20.46 KB | tim.plunkett |
|
#155 | 2976148-delegate-155-interdiff.txt | 8.66 KB | tim.plunkett |
#155 | 2976148-delegate-155.patch | 22.16 KB | tim.plunkett |
|
#155 | 2976148-delegate-nocaching-155-interdiff.txt | 7.36 KB | tim.plunkett |
#155 | 2976148-delegate-nocaching-155.patch | 17.48 KB | tim.plunkett |
|
#147 | 2976148-delegate-147-interdiff.txt | 25.51 KB | tim.plunkett |
#147 | 2976148-delegate-147.patch | 16.94 KB | tim.plunkett |
|
#145 | 2976148-delegate-145-interdiff.txt | 5.42 KB | tim.plunkett |
#145 | 2976148-delegate-145.patch | 25.16 KB | tim.plunkett |
|
#141 | 2976148-delegate-141.patch | 23.88 KB | tim.plunkett |
|
#141 | 2976148-delegate-141-interdiff.txt | 2.98 KB | tim.plunkett |
#137 | 2976148-delegate-137-interdiff.txt | 2.34 KB | tim.plunkett |
#137 | 2976148-delegate-137-PASS.patch | 22.9 KB | tim.plunkett |
|
#137 | 2976148-delegate-137-FAIL.patch | 22.11 KB | tim.plunkett |
|
#131 | 2976148-delegate-refactor-131-do-not-test.patch | 21.15 KB | tim.plunkett |
|
#129 | 2976148-delegate-129-interdiff.txt | 2.51 KB | tim.plunkett |
#129 | 2976148-delegate-129.patch | 97.13 KB | tim.plunkett |
|
#127 | 2976148-delegate-127-interdiff.txt | 2.28 KB | tim.plunkett |
#127 | 2976148-delegate-127.patch | 96.96 KB | tim.plunkett |
|
#121 | 2976148-delegate-121-PASS.patch | 98.6 KB | tim.plunkett |
|
#121 | 2976148-delegate-121-FAIL.patch | 86.83 KB | tim.plunkett |
|
#121 | 2976148-delegate-121-interdiff.txt | 28.36 KB | tim.plunkett |
#114 | 2976148-delegate-114.patch | 81.35 KB | tim.plunkett |
|
#113 | 2976148-delegate-113-interdiff.txt | 2.55 KB | tim.plunkett |
#113 | 2976148-delegate-113.patch | 81.29 KB | tim.plunkett |
|
#105 | 2976148-delegate-105-interdiff.txt | 25.66 KB | tim.plunkett |
#105 | 2976148-delegate-105-PASS.patch | 81.11 KB | tim.plunkett |
|
#105 | 2976148-delegate-105-FAIL.patch | 80.39 KB | tim.plunkett |
|
#90 | 2976148-delegate-90-interdiff.txt | 5.93 KB | tim.plunkett |
#90 | 2976148-delegate-90.patch | 66.97 KB | tim.plunkett |
|
#82 | 2976148-delegate-82-interdiff.txt | 1.32 KB | tim.plunkett |
#82 | 2976148-delegate-82.patch | 66.86 KB | tim.plunkett |
|
#79 | 2976148-delegate-79-interdiff.txt | 6.61 KB | tim.plunkett |
#79 | 2976148-delegate-79.patch | 66.85 KB | tim.plunkett |
|
#76 | 2976148-delegate-76-interdiff.txt | 20.19 KB | tim.plunkett |
#76 | 2976148-delegate-76.patch | 65.15 KB | tim.plunkett |
|
#75 | 2976148-delegate-75-interdiff.txt | 9.52 KB | tim.plunkett |
#75 | 2976148-delegate-75.patch | 59.41 KB | tim.plunkett |
|
#74 | 2976148-delegate-74-interdiff.txt | 11.81 KB | tim.plunkett |
#74 | 2976148-delegate-74.patch | 58.29 KB | tim.plunkett |
|
#70 | 2976148-delegate-refactor-70-interdiff.txt | 859 bytes | tim.plunkett |
#70 | 2976148-delegate-refactor-70.patch | 48.25 KB | tim.plunkett |
|
#66 | 2976148-delegate-66-do-not-test.patch | 48.19 KB | tim.plunkett |
|
#65 | 2976148-delegate-64-do-not-test.patch | 58.17 KB | tim.plunkett |
|
#65 | 2976148-delegate-64-combined-2986735-3008431.patch | 71.82 KB | tim.plunkett |
|
#61 | 2976148-delegate-61-interdiff.txt | 15.77 KB | tim.plunkett |
#61 | 2976148-delegate-61-review-do-not-test.patch | 58.9 KB | tim.plunkett |
|
#61 | 2976148-delegate-61-combined.patch | 72.53 KB | tim.plunkett |
|
#59 | 2976148-delegate-59-interdiff.txt | 11.72 KB | tim.plunkett |
#59 | 2976148-delegate-59.patch | 62.33 KB | tim.plunkett |
|
#57 | 2976148-delegate-57-interdiff.txt | 33.65 KB | tim.plunkett |
#57 | 2976148-delegate-57.patch | 51.95 KB | tim.plunkett |
|
#54 | interdiff-2976148-51-54.txt | 2.68 KB | phenaproxima |
#54 | 2976148-54.patch | 38.49 KB | phenaproxima |
|
#51 | interdiff-2976148-48-51.txt | 8.49 KB | phenaproxima |
#51 | 2976148-51.patch | 37.55 KB | phenaproxima |
|
#48 | interdiff-2976148-47-48.txt | 3.62 KB | phenaproxima |
#48 | 2976148-48.patch | 31 KB | phenaproxima |
|
#47 | 2976148-47.patch | 30.71 KB | phenaproxima |
|
#44 | interdiff-2976148-42-44.txt | 5.49 KB | phenaproxima |
#44 | 2976148-44.patch | 17.83 KB | phenaproxima |
|
#42 | interdiff-2976148-38-42.txt | 2.88 KB | phenaproxima |
#42 | 2976148-42.patch | 17.07 KB | phenaproxima |
|
#38 | 2976148-38.patch | 15.74 KB | phenaproxima |
|
#31 | 2976148-31.patch | 15.09 KB | phenaproxima |
|
#30 | 2976148-30.patch | 16.46 KB | phenaproxima |
|
#28 | 2976148-28.patch | 15.54 KB | phenaproxima |
|
#26 | interdiff-2976148-24-26.txt | 741 bytes | phenaproxima |
#26 | 2976148-26.patch | 14.76 KB | phenaproxima |
|
#24 | 2976148-24.patch | 14.35 KB | phenaproxima |
|
#21 | interdiff-2976148-19-21.txt | 2.37 KB | phenaproxima |
#21 | 2976148-21.patch | 14.26 KB | phenaproxima |
|
#19 | interdiff-2976148-16-19.txt | 3.23 KB | phenaproxima |
#19 | 2976148-19.patch | 14.16 KB | phenaproxima |
|
#16 | 2976148-16.patch | 10.56 KB | phenaproxima |
|
#14 | interdiff-2976148-13-14.txt | 2.85 KB | phenaproxima |
#14 | 2976148-14.patch | 11.11 KB | phenaproxima |
|
#13 | interdiff-2976148-8-13.txt | 915 bytes | phenaproxima |
#13 | 2976148-13.patch | 9.86 KB | phenaproxima |
|
#8 | 2976148-8.patch | 8.79 KB | phenaproxima |
|
Comments
Comment #2
phenaproximaRe-titling.
Comment #3
tim.plunkettI'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.Comment #4
phenaproximaOne 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.
Comment #5
phenaproximaAnother 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:
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.
Comment #6
phenaproximaReferencing #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.
Comment #7
samuel.mortensonThis 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.Comment #8
phenaproximaAn initial patch, without tests, using contexts and explicit weights to filter applicable plugins. Would love to hear thoughts on this approach...
Comment #10
phenaproximaAdding #2961822: Support object-based plugin definitions in ContextHandler as a related issue.
Comment #11
phenaproximaThis blocks #2972041: [PP-1] Add ability to select displays from a list when creating or editing a piece of content, so I'm marking it as a contributed project blocker.
Comment #12
phenaproximaPostponing on #2961822: Support object-based plugin definitions in ContextHandler.
Comment #13
phenaproximaThis changes SectionStorageDefinition to use the context-aware plugin definition plumbing added in #2961822: Support object-based plugin definitions in ContextHandler.
Comment #14
phenaproximaChanging 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.
Comment #15
phenaproximaThe blocker is in!
Comment #16
phenaproximaNow 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.
Comment #18
tim.plunkettTracked 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.
Comment #19
phenaproximaHere'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.
Comment #21
phenaproximaThis 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.
Comment #24
phenaproximaRe-roll on top of 8.7.x. Let's see how far this gets.
Comment #26
phenaproximaSigh. I made a dumb.
Comment #28
phenaproximaRerolled on top of 8.7.x, and hopefully fixing tests. They pass locally, anyway.
Comment #30
phenaproximaAnother reroll.
Comment #31
phenaproximaRemoving accidental patch noise.
Comment #32
tim.plunkettComment #33
tim.plunkettThis "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)?
Are there other places this should be used?
This deserves a code comment. It reminds me of ContextAwarePluginBase but not quite?
Nice approach for the test!
Comment #34
tim.plunkettComment #35
tim.plunkettSee 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
Comment #36
tim.plunkettComment #37
tim.plunkettDiscussed 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.
Comment #38
phenaproximaOkay, 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!
Comment #39
tedbowHow 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.
Comment #40
phenaproximaThat's right, which is why this patch allows the storage type plugins to be weighted. It's for precisely that reason.
Comment #41
tedbowLooks 'weight' logic was removed somewhere along the line
Comment #42
phenaproximaLucky #42! This restores weighting, and tests it.
Comment #43
tedbowCan we remove "valid" here. We just loop till we fine 1. there is not validation here.
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 evendisplayContext
as context or otherwise let the storage know about these.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 assumeweight
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?
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.
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.
\Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::preSave()
not assumelayout_builder__layout
Because that seems like same kind of problemComment #44
phenaproximaThanks for the review, @tedbow!
Comment #45
tedbow👍
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.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'?
Comment #46
phenaproxima@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.
Comment #47
phenaproximaHey kids, wanna break some stuff? Because this will break a ton of tests.
Comment #48
phenaproximaAdded a loadFromContext() method to SectionStorageManager and its interface.
Comment #49
phenaproximaTagging for title and issue summary update, since we're definitely gonna be having some API changes here.
Comment #51
phenaproximaThis should fix the unit tests.
Comment #54
phenaproximaThis should fix the remaining tests; all now pass on my local machine.
Comment #55
phenaproximaAlthough 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:
Comment #56
tim.plunkettWorking on this.
Some semi-related issues:
#2273381: Convert ContextAwarePluginBase to traits
#2982183: Conflict between TypedDataManager and TypedConfigManager in ConfigEntityAdapter
#3008431: A context object for a specific entity type will not match a generic requirement for any entity
Comment #57
tim.plunkettHere's a big chunk of changes.
Has @todos pointing to:
#3008924: Callers of LayoutEntityHelperTrait::getEntitySections() do not account for the view mode
#3008943: Clean up todo in InlineBlockEntityOperations::handleEntityDelete() and use isLayoutCompatibleEntity()
#3008431: A context object for a specific entity type will not match a generic requirement for any entity
Obsoletes #2986403: Create an easy way to get layout sections for an entity.
Not 100% sold on this, but wanted to post some progress
Comment #59
tim.plunkettIncludes #2982183 and #3008431, we need both to fix this I believe. Let's see how many fails are left.
Comment #60
tim.plunkettGreat! That passes. Now to push on those two blockers.
Comment #61
tim.plunkettPosting an update with some changes. Still rolled on top of the above two issues.
Comment #62
tim.plunkettThe above patch includes #2986735: Drupal\Core\Plugin\Context\Context needs DependencySerializationTrait as well.
Comment #64
phenaproximaComment #65
tim.plunkettAdded a soft-blocker: #3008943: Clean up todo in InlineBlockEntityOperations::handleEntityDelete() and use isLayoutCompatibleEntity()
Rerolled.
Comment #66
tim.plunkettHere's a slimmed down version moving some code to #2986403: Create an easy way to get layout sections for an entity..
Comment #67
tim.plunkettComment #68
phenaproxima#2986735: Drupal\Core\Plugin\Context\Context needs DependencySerializationTrait landed so we might need another reroll.
Comment #69
tim.plunkettOnce the final blocker lands, I will reupload the patch from #66, unless there are substantive reviews of that code between now and then.
Comment #70
tim.plunkettAll blockers are in!
Updated the comment about the sort to be more clear.
Comment #71
phenaproximaMinor IS changes.
Comment #72
phenaproximaAssigning to myself to write the change record.
Comment #73
phenaproximaChange record written: https://www.drupal.org/node/3012353
Comment #74
tim.plunkettThanks 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:
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()
andbuildLocalTasks()
).This does not call to
$plugin->access()
.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()
.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 ofFactoryInterface::createInstance
and nothing more.)loadFromContext()
is more of a Mapper method, likeMapperInterface::getInstance
.From those docs:
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 becomecreateInstance($type)
loadFromRoute($type, $value, $definition, $name, $defaults)
would becomecreateInstance($type, ['value' => $value, 'definition' => $definition, 'name' => $name, 'defaults' => $defaults])
loadFromContext($contexts)
would becomegetInstance(['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
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.
Comment #75
tim.plunkettProposed:
Comment #76
tim.plunkettAfter discussion with @EclipseGc, I settled on:
This moves the special loadFromRoute() which was only called once to be inline.
Okay I'm done refactoring, I promise.
Updating the CR
Comment #77
tim.plunkettUpdated IS to reflect last round of naming
Comment #78
phenaproximaDidn't read the tests (yet), but I found very little to complain about here.
The @see should probably refer to SectionStorageManagerInterface::findBContext().
Probably should not be an inheritdoc.
This could benefit from a comment.
Not really necessary but I guess it's harmless.
Ideally I'd like a comment explaining why we didn't use ContextAwarePluginManagerTrait.
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.)
Comment #79
tim.plunkettThanks!
#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
Comment #80
phenaproximaLooks great to me! Thanks, @tim.plunkett.
Comment #81
phenaproximaTests look good too. Only minor things.
Why is this needed?
I know it's a test class, so this is NBD, but can we use $this->getSections() instead of directly referencing $this->sections?
Comment #82
tim.plunkett#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 :)
Comment #83
phenaproximaNice. I would RTBC if I hadn't been so involved with this one :)
Comment #84
xjmComment #85
tedbowThis looks great couple questions/points. I haven't reviewed the test coverage yet.
settings to needs work for "extractEntityFromRoute" point
Should this plural because there could be more than one?
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"I think
extractEntityFromRoute()
can now be moved intoSectionStorageBase
because the implementations inOverridesSectionStorage
andDefaultsSectionStorage
are identical.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?
Comment #86
xjm@tim.plunkett filed #3013773: Document LayoutBuilderEntityViewDisplay::getRuntimeSections() and consider renaming it for clarifying the purpose of the method that contains this bug.
Comment #87
EclipseGc CreditAttribution: EclipseGc at Acquia commentedI 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.
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.
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.
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
Comment #88
tedbowre #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
Comment #89
phenaproximaDiscussed 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.
Comment #90
tim.plunkett#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
We're just renaming $id to $type here for consistency with the new
load($type, array $contexts = [])
Comment #91
tedbowI think the #85 and #87 has been addressed.
Great work!
Comment #94
xjmThis 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.
Comment #95
xjmJust a few notes/nits so I don't lose them; still reading the patch.
Both @phenaproxima and @tim.plunkett explained this to me at some point but I still don't understand it from the comment.
Can we really not be any more specific than
mixed
here? From the method body it looks like it's astring|null
?Edit: Sorry, should have highlighted that this is for:
This change isn't in the CR (that
SectionStorageBase
no longer provides a default implementation of this method).This issue is RTBC so we can probably reroll to remove these and make the patch smaller, yay!
Comment #96
phenaproximaLet's just briefly postpone this on #2982626: ContextAwarePluginBase is incompatible with ContextAwarePluginDefinitionInterface.
Comment #97
EclipseGc CreditAttribution: EclipseGc at Acquia commentedFWIW, all my issues were addressed to my satisfaction.
Eclipse
Comment #98
phenaproximaBlocker is in.
Comment #99
phenaproximaMeant to change the status...
Comment #100
tim.plunkettWorking on this
Comment #101
xjmOne other thing (point 2 below):
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.) :)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 forload()
(which is new in this patch anyway)?Comment #102
xjmThis 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.)
Comment #103
xjmThis is also unclear from context (pun!).
These aren't actually being removed; just a diff artifact
But this one appears to be real and is missing.
This is not a great docblock. :P
I didn't see this in the fixtures; is it being inherited? If so should it be documented?
Good test coverage.
Comment #104
phenaproximaIt's inherited from ContextAwarePluginInterface, which SectionStorageInterface now extends.
Comment #105
tim.plunkett#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 thenew
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.Comment #107
tim.plunkett#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!
Comment #108
larowlanCan 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?
Can we provide a base implementation in SectionStorageBase for this? Would minimize API changes in minors, would mean we could consider backporting.
aka layout_library_lite in core ;), kind of like contact_storage
Comment #109
tim.plunkett#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 typeentity:entity_view_display
Overrides has a context named
entity
with the generic typeentity
but an alter does->addConstraint('EntityHasField', '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.
Comment #110
larowlanOK - the need to implement
protected function getSectionList()
isn't listed in the change record.I will defer to a release manager on the changes
Comment #111
tim.plunkettUpdated the IS a bit to explain why we cannot provide more of a BC layer here. Updating the CR now to address #110
Comment #112
tedbowI 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 onSectionStorageManagerInterface
instead of via the methods onPluginManagerInterface
or related.This test using the SimpleConfigSectionStorage storage manager which makes these routes. Since it just relies on simple config
This code
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.Comment #113
tim.plunkettGreat, thanks!
Comment #114
tim.plunkettReroll for #2936360: Remove duplicate references to the "layout_builder__layout" field from Layout Builder, no other changes
Comment #115
tim.plunkettIf 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.Comment #116
tedbowreread 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.
Comment #117
xjmTagging 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
Comment #118
tim.plunkettWould it be reasonable to split the CR into 2? One for those providing implementations of SectionStorage, one for those interacting with the API externally?
Comment #119
xjmI 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:
And then for the release notes we could say:
Comment #120
xjmNW for the work on the CRs and the release notes; I think we're on a good track for that now.
Comment #121
tim.plunkettRepurposed 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 newgetContexts()
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.
Comment #122
tim.plunkettComment #123
xjmGood catch on #121. Just a few notes before I go to sleep:
It's still unclear to me what "but cannot specify the constraint inline" means or why that means we need this alter.
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.
Comment #124
tim.plunkettI missed #95, will address that and the rest of #123 tomorrow
Comment #127
tim.plunkettIn #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.
Comment #129
tim.plunkett#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 alwaysstring|null
in these implementations, it could also beint
... 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.
Comment #131
tim.plunkettSplit 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.
Comment #132
tim.plunkettMoved relevant tags to #3016473: Allow a single SectionStorage plugin to be returned for a set of available contexts
Comment #133
tim.plunkettComment #134
xjmRather than removing this method, we should remove the usage but change it to wrap:
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.
Comment #135
xjmI 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.
Comment #136
xjm#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.
Comment #137
tim.plunkettRe-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.Comment #139
tim.plunkettThis 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
andupdate
operations, because overrides must be able to provide a UI even when there is no data (i.e. when trying to *add* data!)Comment #140
xjmAhh, there's the line that went into our workaround for
getContextsDuringPreview()
.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?
This as private totally makes sense.
Can we add a line to the docblock above these why we picked each weight? (Because it needs to override/be overridden).
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.
Comment #141
tim.plunkett3) 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.
Comment #142
xjmSorry, 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).
"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:
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.
Comment #143
xjmI'd reword this as:
And similarly:
Nit: "For now, all access..."
Should we @see the relevant code?
And maybe:
Comment #144
xjmFor #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?
Comment #145
tim.plunkett#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).
Comment #146
xjmThanks! Review of the interdiff:
This gets a bit run-on so is hard to parse. How about:
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.
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.
permission. See
:P
Comment #147
tim.plunkettInterdiff 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:
$operation = 'view'
part of this patch from OverridesSectionStorage::access() to ::isApplicable()$operation
parameter fromfindByContext()
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.
Comment #148
tim.plunkettJust 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.
Comment #149
tedbowI like the change to isApplicable() this makes much more sense to me!
Comment #150
xjmYeah 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.Comment #151
tim.plunkettOpened #3020330: Consider renaming the 'view' operation in Layout Builder (which may turn out to be a duplicate or subset of #2914486: Add granular permissions to the Layout Builder.)
Comment #152
xjmIs 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.Comment #153
tim.plunkettWe're swapping in the
count()
in place of the previous!...->isEmpty()
checkLB has never supported empty layouts for overrides, not changing here.
Comment #154
tedbowI 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
Comment #155
tim.plunkett#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 theisApplicable()
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 usingAccessResult
return valuesThe 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...
Comment #157
xjmGood 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.
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.
Looking at
buildMultiple()
in theEntityViewDisplay
, we construct a newCacheableMetadata()
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?Comment #158
tim.plunkettHere's a third approach: go back to using ::access() for both.
Comment #159
tedbowOn 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: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.Comment #160
tim.plunkettAgreed with #159. Here's another idea, interdiff is against #155
This does not address #157 yet
Comment #162
tim.plunkettJust 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.
Comment #163
tim.plunkettSplit out #3020677: Even when empty, Overrides returned instead of Defaults when using SectionStorageManagerInterface::findByContext() for now.
Comment #164
tim.plunkettWhy 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.
Comment #165
tim.plunkettThe 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.
Comment #166
xjmJust 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.
Comment #167
tedbowCould 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.
Comment #168
tim.plunkett#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.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:
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.
Comment #169
tedbowI 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.agreed this is pretty standard.
Comment #172
tim.plunkettAdding credit for those that helped on now-duplicate issues.
Comment #173
xjmI 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!
Comment #174
tim.plunkettOpened #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.
Comment #175
tim.plunkettDiscussed 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.
Comment #176
Wim LeersLooks 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. 😅
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.The render array is built based on decisions made by
Nit:
view_mode_id
(since it's just the ID, not the object)?Why is it optional? This is an internal API since it's
protected
and there's only one caller.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?@see \Drupal\Core\Cache\RefinableCacheableDependencyInterface
s/the/this/ ?
Perhaps s/Cacheability metadata object/Refinable cacheability object/
?
Comment #177
tim.plunkett#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
Comment #178
xjmAgain, 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.
Comment #179
tim.plunkett"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.
Comment #180
Wim Leers#177: thanks!
@todo
for making itabstract
in Drupal 9.Comment #181
tedbowNit: 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
These 2 lines for
getCacheContexts()
andgetCacheMaxAge()
can be commented out and the test still passes.We should remove them or test for these in
$cacheability
like we do forgetCacheTags()
Comment #182
tim.plunkett#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.
Comment #183
tim.plunketthttps://www.drupal.org/node/3022118 added
Comment #184
tedbowLooks Good! 🎉
Comment #185
alexpottHow many nodes? :) I'd remove the comment - the code is clear enough.
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.
Comment #186
xjmSorry, 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.The part in
LEVD::buildMultiple()
is maybe okay because theapplyTo()
call is just a few lines after we explicitly construct aCacheableMetadata
object. AndbuildMultiple()
itself isn't getting it from anything else; it's just using the object internally.The fact that
getRuntimeSections()
accepts anything that implementsRCDI
, but (for BC reasons) responds to it being null by constructing specifically aCacheableMetadata
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).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 aRCDI
implementation that doesn't includeapplyTo()
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 whatfindBContext()
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?
Comment #187
xjmDidn't mean to change status.
IMO #185.2 proposing adding (back?) tests for
getRuntimeSections
is 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.
Comment #188
alexpottDeprecating 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.
Comment #189
xjmI 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.
Comment #190
Wim LeersIMHO
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.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.Comment #191
tim.plunkett#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!
Comment #192
tim.plunkett@Wim Leers pointed out that the default value for cacheability can be an empty array.
Comment #193
xjmPer @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?
Comment #194
tim.plunkettFurther 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.
Comment #195
xjmCan 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..
Comment #196
tim.plunkettTurned 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.
Comment #197
Wim LeersLooks 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.
Comment #198
xjmThe refactor and deprecation here seem totally sensible. I'd like to think a little about the implications of making the method private;-
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.)
Comment #199
xjmThe CR could use a little work I think. It says:
I think this could use:
Comment #200
tim.plunkett#200!
This switches from
private
tofinal protected
, which fits the intention more.Also addressed #198 :D
I updated the CR with an example and some more notes.
Comment #201
xjmThis 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:
...Followed by why we're doing that.
Comment #202
tim.plunkettThe 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:
view_mode
an optional context? What happened if you did or didn't provide one?isOverridable()
return TRUE even if LB was disabled?full
even though the other useddefault
?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:
FAIL patch is without that last commit (so the test is in but not the fix).
Comment #203
tim.plunkettAhhhh this needs an "update path" of clearing caches. Will add one next patch. No update path test possible for that.
Note that this issue is still in another @todo in the LB UI code
Comment #204
xjmFail-patch failed but the non-fail patch failed even more. :D
Comment #205
tim.plunkettVery 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.Comment #206
tim.plunkettInterdiff all the way back to #182 when this was last RTBC
Comment #207
xjmThis comment begs the question "why"?
This worked its way back in.
Should that be tested explicitly?
Comment #208
tim.plunkett#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 inbuildMultiple()
. Undoing thatreturn FALSE
causes all of the functional and functional JS tests to fail. Idk that it needs to be explicitly tested moreAlso removed an extra leftover debugging line from the test.
Comment #209
tedbowI went through all the comments since #182 and this seems to be good to go!
Comment #211
xjmAlright, I think this is well and truly done. Committed and pushed to 8.7.x. Thanks!
Comment #212
tim.plunkettWoo! Thanks all.