Problem/Motivation

Currently we have this interface with these methods

interface SectionStorageInterface extends \Countable {
// Section specific
  public function getSections();
  public function getSection($delta);
  public function appendSection(Section $section);
  public function insertSection($delta, Section $section);
  public function removeSection($delta);
// Integral to the runtime usage
  public function getContexts();
// Needed for storage and routing
  public function getStorageId();
  public static function getStorageType();
// Storage
  public function save();
// Only used by Forms and Controllers
  public function label();
  public function getCanonicalUrl();
  public function getLayoutBuilderUrl();
}

The last 3 methods bug me, as they are not necessarily needed to store sections. Just to present them.

Proposed resolution

Split the interface up into SectionStorageInterface and SectionListInterface.
Introduce a new plugin type SectionStorage.
This retains the SectionStorageInterface, which can delegate back to the underlying object (field, config entity, etc.) if it so chooses.
The field/entity/whatever now only implements the SectionListInterface, and is only responsible for maintaining the list of sections.

Remaining tasks

N/A

User interface changes

N/A

API changes

Yes, but to alpha-stability experimental pre-release code.

The following methods on SectionStorageInterface have moved to SectionListInterface

getSections()
getSection()
appendSection()
insertSection()
removeSection()
count()

The following methods are staying on SectionStorageInterface:

getContexts()
getStorageId()
getStorageType()
save()
label()
getLayoutBuilderUrl()
getRedirectUrl() [renamed from getCanonicalUrl()]

The following methods have been added to SectionStorageInterface:

setSectionList()
getSectionListFromId()
buildRoutes()
extractIdFromRoute()

Data model changes

N/A

CommentFileSizeAuthor
#51 Screenshot-2018-2-3 Edit layout for Article 1 Site-Install.png63.54 KBlarowlan
#51 Screenshot-2018-2-3 Layout library Site-Install.png15.31 KBlarowlan
#49 2937483-layout_section_storage-49.patch170.92 KBtim.plunkett
#49 2937483-layout_section_storage-49-interdiff.txt26.6 KBtim.plunkett
#42 Screenshot-2018-2-2 Edit layout for Basic block custom block entities Site-Install.png48.94 KBlarowlan
#42 Screenshot-2018-2-2 Home Site-Install.png354.44 KBlarowlan
#41 2937483-41.patch164.98 KBlarowlan
#39 2937483-layout_section_storage-39.patch164.41 KBtim.plunkett
#39 2937483-layout_section_storage-39-interdiff.txt4.22 KBtim.plunkett
#36 2937483-layout_section_storage-36.patch164.4 KBtim.plunkett
#36 2937483-layout_section_storage-36-interdiff.txt20.78 KBtim.plunkett
#32 2937483-layout_section_storage-32-interdiff.txt543 bytestim.plunkett
#32 2937483-layout_section_storage-32.patch157.7 KBtim.plunkett
#26 2937483-layout_section_storage-26.patch157.7 KBtim.plunkett
#26 2937483-layout_section_storage-26-interdiff.txt1.81 KBtim.plunkett
#23 2937483-layout_section_storage-23.patch157.69 KBtim.plunkett
#23 2937483-layout_section_storage-23-interdiff.txt2.49 KBtim.plunkett
#22 2937483-layout_section_storage-22.patch157.21 KBtim.plunkett
#22 2937483-layout_section_storage-22-interdiff.txt53.83 KBtim.plunkett
#21 2937483-layout_section_storage-20.patch120.36 KBtim.plunkett
#19 2937483-layout_section_storage-19-do-not-test.patch120.47 KBtim.plunkett
#19 2937483-layout_section_storage-19.patch201.11 KBtim.plunkett
#19 2937483-layout_section_storage-19-interdiff.txt7.41 KBtim.plunkett
#8 2937483-layout_section_storage-8-do-not-test.patch117.44 KBtim.plunkett
#8 2937483-layout_section_storage-8.patch167.53 KBtim.plunkett
#8 2937483-layout_section_storage-8-interdiff.txt14.08 KBtim.plunkett
#6 2937483-layout_section_storage-6.patch165.87 KBtim.plunkett
#6 2937483-layout_section_storage-6-interdiff.txt118.02 KBtim.plunkett
#2 2937483-layout_move_methods-2.patch31.11 KBtim.plunkett
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tim.plunkett created an issue. See original summary.

tim.plunkett’s picture

Status: Active » Needs work
FileSize
31.11 KB

Naming TBD. Also this patch is on top of #2922033: Use the Layout Builder for EntityViewDisplays

larowlan’s picture

We'd want to do this before beta

tim.plunkett’s picture

Issue summary: View changes

First thing to note: the interface itself might be misnamed.
As outlined by my inline comments in the interface in the issue summary, there are several different things happening here.

  • The retrieval and manipulation of a list of Section objects (get/append/insert/remove, possibly including save)
  • Building a UI representation of the object these sections are stored for (label/getCanonicalUrl/getLayoutBuilderUrl)
  • Identifying and explaining the object these sections are stored for (getContexts/getStorageId/getStorageType)

Right now the code expects a single object to be passed around that can do all of those things.
And all of those methods are equally needed to make the Layout Builder UI work.

A system that wants to provide a Layout Builder UI experience for its data model needs only to implement this interface (and a param converter) and that's it.
An object implementing this interface says "yes, not only do I know what sections I have, but I know *why I have them*" (meaning, it can provide the appropriate contexts for the underlying system, it can provide a human readable label to communicate that to a user, and it knows where it can be viewed and edited).

samuel.mortenson’s picture

To me, a StorageInterface in core should always define CRUD methods that take in an ID where appropriate. This is consistent with almost every *StorageInterface.php file I've found. Following that, I would expect that a SectionStorageInterface is something that can at least load, save, and delete a Section or list of Sections based on an ID. Instead, as #4 outlines, StorageInterface does quite a few things that are mostly unrelated to normal *StorageInterface operations.

With that, I would suggest renaming the interface to something like SectionListInterface, which is consistent with the getSections/getSection/appendSection/insertSection methods. The other methods are tricky, here are my thoughts on them:

  1. getContexts - Shouldn't a section be provided contexts by the thing using them? I wouldn't expect that the thing that stores/lists sections to also be capable of building its own contexts.
  2. getStorageId/getStorageType - Could I ever use there IDs to generically load any section storage? For example, could I get the section storage for an entity with only this data (i.e. $something->load('entity:node:1')->getSections()? From what I understand, these are only used to determine a unique tempstore ID, but can't be used to perform CRUD operations on the storage from another place.
  3. label/save - These make sense, but seeing the save method being implemented in LayoutSectionItemList is a little scary. What about creating a new revision, forward revisions, validation, etc.? Should a field ever have a method that updates the parent entity?
  4. getCanonicalUrl/getLayoutBuilderUrl - I would expect a method like this to be provided by a handler, not a storage. Should a storage be aware of its user interface?

Splitting these methods up is hard, but here's a hypothetical setup:

  1. The LayoutSectionItemList field should keep the getSections/getSection/appendSection/insertSection methods.
  2. Create a new service that deals with section storage generically, which can perform CRUD operations on any storage based on a type and ID.
    User interfaces can then be written generically and invoke the service like $service->load('entity:node:1')->insertSection(...)->save(). The service would know to load Node 1 and pass relevant data to the LayoutSectionItemList field.
  3. There should be a new service or an addition to the above that knows how to provide contexts to sections based on the current entity.
tim.plunkett’s picture

I need to update the IS, but here's a big refactor to address the above concerns.

Built on top of #2922033: Use the Layout Builder for EntityViewDisplays. Interdiff is just this issue's worth.

Status: Needs review » Needs work

The last submitted patch, 6: 2937483-layout_section_storage-6.patch, failed testing. View results

tim.plunkett’s picture

Title: Consider moving label(), getCanonicalUrl(), and getLayoutBuilderUrl() out of SectionStorageInterface » [pp-1] Defining a new type of section storage relies on magic strings and hidden assumptions
Issue summary: View changes
Status: Needs work » Needs review
FileSize
14.08 KB
167.53 KB
117.44 KB

As we (@EclipseGC, @samuel.mortenson, and I) dug into this issue, one thing became clear:
The steps necessary to define a new type of section storage (overrides, defaults, etc.) were not clear.

First there is the magic string returned by a *static method* (yuck) that has to match a portion of a magic string on a service definition.
Then there is the situation described in the original issue summary here, about the underlying object being responsible for way too much knowledge about itself (why should the field or config entity care how the URL parameters for the Layout Builder UI are structured?)
Finally, there is the way routes are defined for each. Without adding more code directly into LayoutBuilderRoutes, there was no reasonable way to hook into the Layout Builder UI.

Hence, a new plugin type SectionStorage.
This retains the SectionStorageInterface, which can delegate back to the underlying object (field, config entity, etc.) if it so chooses.
The field/entity/whatever now only implements the SectionListInterface, and is only responsible for maintaining the list of sections.

Still blocked on defaults, the do-not-test patch is just this issue's code.

EclipseGc’s picture

I find the naming between LayoutBuilderRoutes and LayoutBuilderRoutesTrait to be confusing. One is an event subscriber and the other is a trait the plugins (which the event subscriber delegates to) happen to us. And that extra layer of abstraction between the two makes drawing a line between them sort of weird. Maybe I'm being overly nitpicky here.

  1. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php
    @@ -0,0 +1,64 @@
    +  public function loadEmpty($id) {
    +    return $this->createInstance($id);
    +  }
    

    ?? is this like... necessary rather than just calling createInstance()?

  2. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php
    @@ -0,0 +1,64 @@
    +  public function loadFromRoute($id, $value, $definition, $name, array $defaults) {
    +    $plugin = $this->createInstance($id);
    +    if ($section_list = $plugin->convert($value, $definition, $name, $defaults)) {
    +      return $plugin->setSectionList($section_list);
    +    }
    +  }
    

    ok, so forgive my complete craziness, but when I see this method I expect to see loadFromRoute(Route $route). I know the ID is from the route defaults. Feels weird to parse it out before it gets to this method. Is that nuts to say?

I'll review for strings and more detail shortly.

Eclipse

tim.plunkett’s picture

Those methods (the two above plus loadFromObject()) were all quickly named. I would love feedback on this.

The intention behind loadEmpty vs createInstance was that 99% of the time you'd expect your plugin to be able to respond to all methods. loadEmpty() only exists for the single usecase of routing. Which maybe isn't worth an API level method?

But the point was, if you call createInstance() yourself, you're not going to get what you want.
In fact, I went so far as to have interface SectionStorageManagerInterface extends DiscoveryInterface instead of extending PluginManagerInterface, since I only want getDefinition[s] and these load* methods to be called, and the fact that it uses createInstance internally is an implementation detail.

I'd appreciate further feedback on the manager here, since it's the New Big Thing, but I'm also welcome to hashing this stuff out in realtime in the #layouts channel on Drupal Slack.

larowlan’s picture

  1. +++ b/core/modules/layout_builder/src/SectionStorageInterface.php
    @@ -95,7 +31,71 @@ public function getStorageId();
    +  public function convert($value, $definition, $name, array $defaults);
    

    Not sure of the naming of this method, I realise its a param converter in disguise, but perhaps we can find a name that means more in the domain of layout builder.

  2. +++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php
    @@ -169,10 +169,11 @@ public function providerTestLayoutSectionFormatter() {
    -    $this->drupalGet($node->toUrl('layout-builder'));
    +    $this->drupalGet($canonical_url->toString() . '/layout');
    

    Can you elaborate a bit on why this has changed? I assume it is because of

    (why should the field or config entity care how the URL parameters for the Layout Builder UI are structured?)

    but its not obvious

larowlan’s picture

We'd want to do this before beta

and then some!

larowlan’s picture

?? is this like... necessary rather than just calling createInstance()?

I originally was going to say the same thing, but then realised that `loadEmpty` is the correct naming in the context of a layout domain, while `createInstance` is a plugin-api naming.

I think it is fine to name the methods in the context of the domain.

EclipseGc’s picture

I think it is fine to name the methods in the context of the domain.

FWIW, I completely agree, I just think a pure passthrough method is sort of weird. Not a hill I care to go die on, and Tim's reasoning seems sound.

Eclipse

tim.plunkett’s picture

#11
1) Yep, that'd be good
2) Oops, that's left over from an earlier version where I dropped the bit in hook_entity_type_alter() that sets up the link template. Those hunks should be reverted.

larowlan’s picture

So thinking about a use case for another section type I came up with the following scenario.

  • A module called 'layout library'
  • Module defines a config entity which defines a layout
  • Admin/site builder builds up a suite of N approved layouts with each layout being associated with a given content entity type and bundle
  • Admin/site builder configure M node-types to have a ER field allowing a reference to a layout library config entity
  • Content editors can pick layout from edit form
  • Special form mode for content editors that shows live preview as they change layout (like the change view mode form in the header on node preview) - saving sets the value of that field.

How would that look here?

  • the config entity would implement SectionListInterface
  • getStorageId would return the ID of the config entity
  • getStorageType would return 'layout_library'
  • setSectionList would defer to the base handler after checking the passed argument was an instance of the config entity
  • alterRoutes would provide a route to add a new layout library entry for each enabled content entity type + bundle
  • not sure what getCanonicalUrl would do
  • getLayoutBuilderUrl would redirect to editing in the library for the given content entity type + bundle
  • convert would do the job of upcasting as per the others (loading the config entity). Although this may be possible with existing entity converters
  • getContexts would do the job of creating a sample entity context based on the given content entity type + bundle
  • label would return the name defined in the library
  • save would save the config entity

So I think this is a pretty good fit, just wondering how/where getCanonicalUrl fits

PS, when this goes beta, I might actually build such a solution.

Whilst I like the idea of content-editors being able to customise the layout of their content entity, I also like the structure this provides to keep them on the rails.

tim.plunkett’s picture

That sounds cool!
Thanks for walking through that.

Currently for Defaults we have the canonical URL returning the user to Manage Display.
In an earlier iteration, I just had getCanonicalUrl return getLayoutBuilderUrl...

larowlan’s picture

  1. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php
    @@ -0,0 +1,64 @@
    +    $plugin = $this->createInstance($id);
    +    if ($section_list = $plugin->convert($value, $definition, $name, $defaults)) {
    +      return $plugin->setSectionList($section_list);
    +    }
    

    missing return point? also means missing a test for when no section list

  2. +++ b/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php
    @@ -50,15 +51,15 @@ public function testConvert($success, $expected_entity_id, $value, array $defaul
    +    $result = $this->plugin->convert($value, [], 'the_parameter_name', $defaults);
    
    @@ -124,10 +125,10 @@ public function testConvertCreate() {
    +    $result = $this->plugin->convert($value, [], 'the_parameter_name', [], 'the_parameter_name', []);
    

    One of these has 6 args, the other has 4. I think 4 is the right amount.

tim.plunkett’s picture

Rerolled for latest changes in #2922033: Use the Layout Builder for EntityViewDisplays

#18

1)
No return missing, but a missing @return typehint and docblock.
Added a test to prove the expectations.

2)
Also fixed the 2nd point

Additionally, renamed s/loadFromObject/loadFromSectionList as per a recommendation of @samuel.mortenson.

larowlan’s picture

Title: [pp-1] Defining a new type of section storage relies on magic strings and hidden assumptions » Defining a new type of section storage relies on magic strings and hidden assumptions
tim.plunkett’s picture

tim.plunkett’s picture

LayoutBuilderRoutes was split up but the test coverage wasn't. Fixing that now.

tim.plunkett’s picture

Per @EclipseGc's request, I cleaned up SectionStorageBase's handling of sectionList. Since it's not guaranteed to be set, we should check first.

EclipseGc’s picture

This is a review of 21. I'll post subsequent reviews of interdiffs. I was fairly picky with this and even gave the regular text strings a pretty good looking at during the review just to make sure we didn't have obvious typos or other english-weirdness.

  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php
    @@ -0,0 +1,90 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function getSections() {
    +    return $this->sectionList->getSections();
    +  }
    

    Ok, so I mentioned this to Tim on chat already, but both of the actual plugin classes have proxy methods they use instead of going directly to $this->sectionList. That's beneficial for a number of reasons but especially since we can throw a sane exception there if there is no SectionList object present (since setSectionList() is sort of like a views plugin's init() method). Subsequent calls to methods of the SectionListInterface could in theory be called on a null value, which throws an error to the effect of "called getSections() method on NULL object". That would be especially confusing in the case of this method since it proxies to a method of the exact same name. I suspect Tim will have a fix for this before I even finish this review, but I'm including it for completeness on this patch.

  2. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php
    @@ -0,0 +1,64 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function loadEmpty($id) {
    +    return $this->createInstance($id);
    +  }
    +
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function loadFromSectionList($id, SectionListInterface $section_list) {
    +    return $this->createInstance($id)->setSectionList($section_list);
    +  }
    +
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function loadFromRoute($id, $value, $definition, $name, array $defaults) {
    +    $plugin = $this->createInstance($id);
    +    if ($section_list = $plugin->convert($value, $definition, $name, $defaults)) {
    +      return $plugin->setSectionList($section_list);
    +    }
    +  }
    

    ok, I still think this is totally weird. That being said, there's a certain degree of "I like this" going on too. In a perfect world, we'd overwrite createInstance() with some additional logic that could tell if the createInstance method were called internally or externally and would throw an exception for externally sourced calls that gave some reasoning for using these methods instead. Still that is likely a huge degree of overkill and I DO really like how clean and utilitarian these methods are.

  3. +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageTrait.php
    @@ -0,0 +1,114 @@
    +  /**
    +   * Indicates if there is a section at the specified delta.
    +   *
    +   * @param int $delta
    +   *   The delta of the section.
    +   *
    +   * @return bool
    +   *   TRUE if there is a section for this delta, FALSE otherwise.
    +   */
    +  protected function hasSection($delta) {
    +    return isset($this->getSections()[$delta]);
    +  }
    

    Tim and I discussed this at length. We're both generally in favor of exposing "hasThingy" methods, but Tim chose not to in this case because hasSection(7) seems sort of ambiguous and unhelpful. I agree completely, and I'm just documenting it for future reviewers.

Overall, this patch moves a lot of things around to organize them better, introduces a new plugin system and puts almost all the domain knowledge for introducing a new layout builder use case into that plugin. What is left is fairly standard sorts of things (like local tasks) which are considered temporary UI solutions by the layout builder team for our UI. All in all, I'm very happy with the state of this patch and it moves the LB code base in a really good direction without sacrificing any flexibility and instead really improves our DX.

Eclipse

EclipseGc’s picture

Status: Needs review » Needs work
+++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php
@@ -50,6 +50,13 @@ protected function buildRoute(RouteCollection $collection, SectionStorageDefinit
+    if ($route_name_prefix) {
+      $route_name_prefix = "layout_builder.$type.$route_name_prefix";
+    }
+    else {
+      $route_name_prefix = "layout_builder.$type";
+    }
+

I think this makes a lot of sense. There are a few places where we just might not need an additional prefix, so ++ to this change.

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
@@ -0,0 +1,260 @@
+  /**
+   * Gets the entity storing the overrides.
+   *
+   * @return \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface
+   *   The entity storing the defaults.
+   */
+  protected function getDisplay() {
+    return $this->sectionList;
+  }

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -0,0 +1,234 @@
+  /**
+   * Gets the entity storing the overrides.
+   *
+   * @return \Drupal\Core\Entity\FieldableEntityInterface
+   *   The entity storing the overrides.
+   */
+  protected function getEntity() {
+    return $this->sectionList->getEntity();
+  }

Would it make sense for both of these methods to call to $this->getSectionList() so that they and their methods also benefit from the exception that method can throw?

Eclipse

tim.plunkett’s picture

#24

1)
Fixed in #23

2)
Interesting to note:
SectionStorageManagerInterface doesn't have a method createInstance(), since we extend DiscoveryInterface directly instead of PluginManagerInterface. (We do use createInstance internally since we extend DefaultPluginManager)

#25

2) Fixed here, and made SectionStorageBase::$sectionList private to enforce use of the getter.

EclipseGc’s picture

Status: Needs review » Reviewed & tested by the community

So I've spent a bit of time looking at #2924058: Discuss using Layout Builder to control full site layout and replace Block UI which has an implementation for both before and after this patch and I think it's telling. Tim's gone the extra mile, so there's some non-essential-to-this-review stuff in that patch, so I'm going to highlight what is essential for the sake of this patch.

Routing

I'm comparing the patches provided in posts 2 and 13. The first real section of note is the LayoutBuilderRoutes class. The patch in 2 actually edits this class, which is something core modules can get away with, but a contrib module wishing to implement a Layout Builder solution would have to provide a whole new event subscriber, so that's an entry in the services.yml file and a php class, just to get at the route alterations which need to happen.

The patch in this issue removes that need and is illustrated by the patch on comment 13 of #2924058: Discuss using Layout Builder to control full site layout and replace Block UI because there is an alterRoutes() method on the plugin type which was implemented.

Parameter conversion/upcasting

In the patch on 2, Tim had to provide a custom class to do parameter upcasting. This means another entry in the ole services.yml file and a corresponding php class. After this issue lands, the patch in 13 illustrates again that this is delegated to a method of the plugin which again simplifies the dx of building one of these implementation.

Section Storage

The section storage system as it stands in core currently revolves around a magic naming convention in the services.yml file. This puts us up to three services.yml entries and 3 corresponding php files just to setup your own implementation of layout builder. Again, after this patch, we're still just creating the plugin class.

There are of course a few things this doesn't resolve (like the local task deriver) but I've explained all of those in previous reviews. This patch radically improves the dx of using layout builder, and moves the whole of this code base in a clearly better and cleaner direction. I've looked over this code pretty carefully and fell good about rtbcing it pending tests passing.

Eclipse

The last submitted patch, 2: 2937483-layout_move_methods-2.patch, failed testing. View results

The last submitted patch, 2: 2937483-layout_move_methods-2.patch, failed testing. View results

EclipseGc’s picture

re-affirming testbot is weird and my rtbc.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 26: 2937483-layout_section_storage-26.patch, failed testing. View results

tim.plunkett’s picture

No idea why private causes this to fail, but I think there is some strange interaction between the base class and a trait.
Switching to protected (which is the 99.999% standard in Drupal) and putting back.

tim.plunkett’s picture

@andypost, no idea what you're describing. Please update the IS as you see fit, or ask specific questions and I can answer them.

tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett
Status: Reviewed & tested by the community » Needs work

I reread the issue, and realized I skipped over #11.1
Working on this now.

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned
Status: Needs work » Needs review
FileSize
20.78 KB
164.4 KB

After discussion with @EclipseGc and @larowlan, split ::convert() into ::extractIdFromRoute() and ::getSectionListFromId()

larowlan’s picture

  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
    @@ -196,25 +196,34 @@ protected function getEntityTypes() {
    +      list($entity_type_id, $bundle, $view_mode) = explode('.', $id);
    

    nit, should we use the third arg here, passing 3?

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -108,22 +108,30 @@ public function getStorageId() {
    +      list($entity_type_id, $entity_id) = explode(':', $id);
    

    same deal here

changes look good in my book

EclipseGc’s picture

Status: Needs review » Reviewed & tested by the community

Assuming this passes tests, I am completely on board with this further detangling of the dubiously named convert() method into two new methods that actually say what they do. RTBC pending passing tests.

Eclipse

tim.plunkett’s picture

#37
I had originally not done this on purpose, but on looking again my reason has been refactored away.
And also switching the Overrides case to use `.` too instead of `:` for consistency.
(Also, a small joy of this patch: before that change of separator would have required changes in 2 classes and 2 tests, now it's 1 class and 1 class)

Leaving at RTBC.

EclipseGc’s picture

re-affirming rtbc pending passing tests.

Eclipse

larowlan’s picture

larowlan’s picture

Took for a manual test, no issues. Tested on block content and node.

tim.plunkett’s picture

Reroll looks good to me, thanks @larowlan!

larowlan’s picture

Couple of things I picked up so far, going through the process of building the module described in #16 to really put this through its paces

  1. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -28,6 +28,15 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function getTargetEntityType() {
    +    return $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId());
    +  }
    

    I'm not sure we need this to be public. The base class already has \Drupal\Core\Entity\EntityDisplayBase::getTargetEntityTypeId

    There are three calls:

    - one from default storage - which is a plugin that can inject the entity type manager (and already does)
    - one from the form, which already has the entity type manager injected
    - one from self, which obviously can still be called if it is made protected

    Feels like unnecessary API surface that can be removed

  2. +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
    @@ -258,29 +176,7 @@ public function buildMultiple(array $entities) {
    -  protected function getSampleEntity($entity_type_id, $bundle_id) {
    +  public function getSampleEntity($entity_type_id, $bundle_id) {
    

    I don't think this API belongs on this object. If another module needs it, it can't call it without instantiating the display, but there is no use of $this, so the display is not needed.

    This whole method can be removed from the interface and moved to a standalone service in my opinion. Again, smaller API surface for the view display (albeit moved elsewhere)

    Then the only usage (in the section storage) can have the service injected via CFPI.

    And other modules can use it.

larowlan’s picture

+++ b/core/modules/layout_builder/src/SectionStorageInterface.php
@@ -2,100 +2,118 @@
+  public function getCanonicalUrl();

When implementing this, it took me a while to work out what this method is for.

It is used to redirect the user back to somewhere after saving the layout.

So I think perhaps getRedirectUrl might be a more intuitive name?

larowlan’s picture

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php
@@ -0,0 +1,281 @@
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRouteParameters() {

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -0,0 +1,242 @@
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRouteParameters() {

we need real docs here, there is no parent method for the {@inheritdoc}

larowlan’s picture

OK, I think I've completed the process of implementing one of these plugins, my observations are as above comments.

I think it works well, the alterRoutes still feels a bit light on for docs, that was probably the most confusing/difficult bit to implement/understand - this is what we have

/**
   * Alters the route collection during route building.
   *
   * @param \Symfony\Component\Routing\RouteCollection $collection
   *   The route collection.
   */
  public function alterRoutes(RouteCollection $collection);

If it could go into detail about what routes to alter, and how to work with the buildRoutes method, that'd go a long way to making this a largely smooth process.

The project WIP is https://www.drupal.org/project/layout_library, @tim.plunkett, @samuel.mortenson, @EclipseGC you all have push access.

The plan is I'll flesh it out further to the point of being useful in coming months.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

For #44 to #47

tim.plunkett’s picture

#44.1
Good point, fixed

#44.2
Added a new service. Purposefully did not include an interface, as this should eventually be considered for a generic service.

#45
Agreed!

#46
Inlined the one instance, and documented the other.

#47
Documented, and renamed from alterRoutes to buildRoutes. I don't think the altering aspect is relevant.

151ced9687 Remove getTargetEntityType()
72308253c6 Rename getCanonicalUrl to getRedirectUrl
d60210cd16 Fix getRouteParameters()
f37d225c9e Rename and document plugin route building method
813c5f24c2 Add sample entity generator

https://github.com/timplunkett/d8-layouts/commits/2937483-layout_section...

EclipseGc’s picture

Status: Needs review » Reviewed & tested by the community

The interdiff looks really good to me. RTBC pending passing tests.

Eclipse

larowlan’s picture

Validated the API of a new layout section storage plugin with Layout library module

Screenshots of editing layouts from the library

Winning!

larowlan’s picture

The only extension point/API missing is in this code on \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::getRuntimeSections

  /**
   * Gets the runtime sections for a given entity.
   *
   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
   *   The entity.
   *
   * @return \Drupal\layout_builder\Section[]
   *   The sections.
   */
  protected function getRuntimeSections(FieldableEntityInterface $entity) {
    if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) {
      return $entity->get('layout_builder__layout')->getSections();
    }

    return $this->getSections();
  }

It effectively embeds knowledge of the override storage handler in the defaults, but doesn't allow other modules to interact, limiting us to two plugin implementations without needing to do some hacky stuff like #2941808: Subclass \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::getRuntimeSections.

I'm not sure how to unpick that though.

This is an entity, so using module handlers or plugin managers is even more hacky.

Open to suggestions but I don't really see any better way of doing it

tim.plunkett’s picture

\Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple() is effectively just a hook_entity_view_alter() moved into OO code.
It's only coincidentally located in the implementation of a SectionListInterface, it could easily be moved back out to demonstrate that this is a separate system you are hooking into, like this https://gist.github.com/timplunkett/0443bdd20d1d80ed2dbb8ce9b86ed761

Not all implementations will be related to rendering fieldable entities, which is why this isn't something on the plugin itself.

It is unfortunate indeed that we're spreading around knowledge of how to get at the sections directly.
This is why I opened #2936360: Remove duplicate references to the "layout_builder__layout" field from Layout Builder, where we can continue this discussion.

But I feel that it is out of scope for this issue, as we are currently not changing either buildMultiple() or getRuntimeSections() here.

tim.plunkett’s picture

Priority: Normal » Major
Issue summary: View changes
larowlan’s picture

But I feel that it is out of scope for this issue, as we are currently not changing either buildMultiple() or getRuntimeSections() here

Agree

Regardless, I like the current code more than that in the gist that uses hook_entity_view_alter()

And #2941808: Subclass \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::getRuntimeSections can just use hook_entity_view_alter() if it has to.

larowlan’s picture

saving review credits

  • larowlan committed 10eb692 on 8.6.x
    Issue #2937483 by tim.plunkett, larowlan, EclipseGc, samuel.mortenson:...
larowlan’s picture

Status: Reviewed & tested by the community » Fixed

This has been reviewed extensively, and I even took the extraordinary step of writing a contrib module to verify the API.

Committed as 10eb692 and pushed to 8.6.x

🦅🍒

tim.plunkett’s picture

Component: layout.module » layout_builder.module

Status: Fixed » Closed (fixed)

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