See meta-issue: #2511554: [meta] Move some parts of Page Manager into CTools

Currently, BlockDisplayVariant implements PageAwareVariantInterface, which couples BlockDisplayVariant to Page Manager Pages. If Panels or anything else wants to use BlockDisplayVariant, they now have to depend on PageAwareVariantInterface. This is problematic because Panels shouldn't depend on Page Manager!

BlockDisplayVariant shouldn't need to be aware of the page, we should find away to implement whatever it's doing with the page in another way.

TODO

  1. Identify what the BlockDisplayVariant is doing with the page from getPage()
  2. Rewrite those bits to not need the page (this might depend on depend on #2511568: Create "context stack" service where available contexts can be registered)
CommentFileSizeAuthor
#69 interdiff.txt2.32 KBdsnopek
#69 page_manager-remove-page-aware-2511570-69.patch29.13 KBdsnopek
#57 interdiff.txt4.06 KBdsnopek
#57 page_manager-remove-page-aware-2511570-57-do-not-test.patch28.47 KBdsnopek
#54 interdiff.txt1.7 KBtim.plunkett
#54 2511570-pageaware-53.patch29.58 KBtim.plunkett
#51 2511570-pageaware-51-do-not-test.patch19.95 KBtim.plunkett
#51 2511570-pageaware-51.patch29.36 KBtim.plunkett
#51 interdiff.txt5.33 KBtim.plunkett
#46 interdiff.txt1.72 KBdsnopek
#46 page_manager-remove-page-aware-2511570-46.patch36.9 KBdsnopek
#39 page_manager-remove-page-aware-2511570-39.patch35.8 KBdsnopek
#33 interdiff.txt1.44 KBdsnopek
#33 page_manager-remove-page-aware-2511570-33.patch35.87 KBdsnopek
#21 interdiff.txt12.87 KBtim.plunkett
#21 2511570-pageaware-21.patch36.11 KBtim.plunkett
#20 interdiff.txt5.17 KBdsnopek
#20 page_manager-remove-page-aware-2511570-20.patch29.14 KBdsnopek
#19 page_manager-remove-page-aware-2511570-19.patch31.48 KBdsnopek
#14 interdiff.txt19.06 KBdsnopek
#14 page_manager-remove-page-aware-2511570-14.patch32.67 KBdsnopek
#13 interdiff.txt5.04 KBdsnopek
#13 page_manager-remove-page-aware-2511570-13.patch19.89 KBdsnopek
#10 interdiff.txt1.46 KBdsnopek
#10 page_manager-remove-page-aware-2511570-10.patch16.97 KBdsnopek
#7 interdiff.txt1.44 KBdsnopek
#7 page_manager-remove-page-aware-2511570-7.patch16.62 KBdsnopek
#6 interdiff.txt6.5 KBdsnopek
#6 page_manager-remove-page-aware-2511570-6.patch15.75 KBdsnopek
#5 page_manager-remove-page-aware-2511570-5.patch11.68 KBdsnopek

Comments

dsnopek’s picture

dsnopek’s picture

Issue tags: +D8panels

I'm adding to the sprint board because a Panels issue is blocked on this one: #2511576: Remove dependency on Page Manager

dsnopek’s picture

Issue summary: View changes
dsnopek’s picture

Assigned: Unassigned » dsnopek

I'm going to give this one a shot!

dsnopek’s picture

Status: Active » Needs review
StatusFileSize
new11.68 KB

Alright! Here's my first pass at this. I'd really appreciate some review particularly around the changes to the render caching stuff.

dsnopek’s picture

This should pass more tests, but it's gotten kind of ugly because any form that needs to edit a variant needs to be passed the page id, because the variant is part of the page config entity. To get around that, I've passed the page_id in as configuration, however, this is totally wrong, wrong, wrong!

In #2550879: Use CTools Wizard API to add/edit Pages (and move plugin UI using PluginWizardInterface), variants are turning into their own independent config entities which means we won't need to pass around the page_id, which is a much better solution to this problem.

dsnopek’s picture

StatusFileSize
new16.62 KB
new1.44 KB

Ok, this one should finally pass! I wasn't getting the render cache stuff quite right before.

saltednut’s picture

Status: Needs review » Reviewed & tested by the community

Functionally, this is working correctly. After applying, I am able to do the following.

  1. Install Drupal
  2. Enable Page Manager and Ctools
  3. Create a new Page Manager page (e.g. /test)
  4. Add new static context
  5. Add and customize a display variant (such as adding blocks)
  6. Add access conditions
  7. Visit the page (e.g. /test) and view the correct output

As long as the bots pass this, I think its good.

tim.plunkett’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/src/Entity/PageViewBuilder.php
    @@ -23,10 +25,27 @@ class PageViewBuilder extends EntityViewBuilder {
    +  protected static function mergeCacheTags(array &$element, EntityInterface $entity) {
    

    a) needs docs
    b) Why do we need this? The recursiveness seems like it shouldn't be our problem

  2. +++ b/src/Form/DisplayVariantAddForm.php
    @@ -79,7 +79,14 @@ class DisplayVariantAddForm extends DisplayVariantFormBase {
    +      'page_title' => $this->page->label(),
    

    Is this going to blindly override a custom label? Or is that a different key? Also label/title mismatch is weird.

  3. +++ b/src/Form/DisplayVariantAddForm.php
    @@ -79,7 +79,14 @@ class DisplayVariantAddForm extends DisplayVariantFormBase {
    +      // @todo Remove in issue #2550879 when variants are config entities.
    +      'page_id' => $this->page->id(),
    

    Hmm, I think we might want to do this first?

  4. +++ b/tests/src/Unit/BlockDisplayVariantTest.php
    @@ -238,10 +235,7 @@ class BlockDisplayVariantTest extends UnitTestCase {
         $page->getCacheTags()
    -      ->willReturn(['page:page_id'])
    -      ->shouldBeCalled();
    ...
    +      ->willReturn(['page:page_id']);
    

    Why remove the shouldBeCalled()?

dsnopek’s picture

Status: Needs work » Needs review
StatusFileSize
new16.97 KB
new1.46 KB

1. Ah, yes, docs! Added. :-) It's needed because of:

+++ b/src/Plugin/DisplayVariant/BlockDisplayVariant.php
@@ -177,11 +163,8 @@ class BlockDisplayVariant extends VariantBase implements ContextAwareVariantInte
-            // Each block needs cache tags of the page and the block plugin, as
-            // only the page is a config entity that will trigger cache tag
-            // invalidations in case of block configuration changes.
-            'tags' => Cache::mergeTags($page->getCacheTags(), $block->getCacheTags()),
+            'keys' => ['page_manager_block_display', $uuid, 'block', $block_id],
+            'tags' => $block->getCacheTags(),

Since we don't have access to the $page when adding the blocks to the render array, we need to add the $page->getCacheTags() a layer up when we do!

The recursiveness is because we don't actually know what the render array from the display variant is going to look like, because it doesn't have to be a BlockDisplayVariant, it could be anything.

2. No, because its DisplayVariantAddForm and the variant was just instantiated a couple lines up.

3. Sure! I put it second so the comment would clearly only apply to that and not 'page_title'.

4. See point #1! It's no longer calling $page->getCacheTags() when building the render array initially, that's been moved to PageViewBuilder.

dsnopek’s picture

Discussed with Tim and we're going to postpone this on making the variant into it's own config entity: #2551633: Make variants into their own config entity

dsnopek’s picture

Status: Postponed » Needs work

I'm been thinking about this more since yesterday, and I think:

  1. It might be possible to work around the "'page_id' in configuration" issue without doing #2551633: Make variants into their own config entity by using the tempstore. If we hold the variant configuration in the tempstore key'd by uuid, we should be able to have our dialog routes take only the variant uuid! Then we copy the configuration from the tempstore into the Page entity on submit.
  2. Passing 'page_title' in via configuration is wrong - it'll break on any display variant that has a schema but doesn't have a 'page_title' key. :-/ I think we're either going to have to add a new interface (VariantWithTitleInterface?) to mark which variants take a title, OR simply live without having a default value for this field.
dsnopek’s picture

StatusFileSize
new19.89 KB
new5.04 KB

Well, it turns out I was right about #12.2, but not #12.1. Just in adding a new block to the variant we need to know the contexts in order to figure out what blocks are available - which means we need to know the page!

So, here's a new patch with #12.2.

I'm going to try a couple more ideas before re-postponing on #2551633: Make variants into their own config entity.

dsnopek’s picture

Status: Needs work » Needs review
StatusFileSize
new32.67 KB
new19.06 KB

Ok! Here's a new version of this patch that manages to kill the "'page_id' in configuration" hack. It does it by moving the form for editing the blocks from BlockDisplayVariant into DisplayVariantEditForm.

This will unfortunately prevent Panels from customizing the UI for editing blocks (which it will need to do, but doesn't now) but I think we can add that back, either by (a) leveraging the wizard stuff in #2550879: Use CTools Wizard API to add/edit Pages (and move plugin UI using PluginWizardInterface) which will (eventually) provide a way for variants to add wizard steps, or (b) by adding the plugin id of the display variant to the form id used by DisplayVariantEditForm, and then altering it. Obviously, (a) is preferable. :-)

Since Panels isn't presently customizing this UI, and we will find a method for doing that eventually, I'm personally OK with merging this now and leaving it temporarily impossible.

dsnopek’s picture

Assigned: dsnopek » Unassigned

The last submitted patch, 6: page_manager-remove-page-aware-2511570-6.patch, failed testing.

The last submitted patch, 5: page_manager-remove-page-aware-2511570-5.patch, failed testing.

dsnopek’s picture

Status: Needs review » Needs work

This needs a re-roll - start working on it last week but ran out of time. But I can at least update the issue status. ;-)

dsnopek’s picture

Assigned: Unassigned » dsnopek
Status: Needs work » Needs review
StatusFileSize
new31.48 KB

Started getting excited about this so I'm doing the re-roll tonight (rather than in the morning as planned :-))! No interdiff as this is a pretty straight re-roll.

This doesn't make any of the changes discussed with @EclipseGC or @timplunkett on IRC. That'll come in the next patch.

dsnopek’s picture

Assigned: dsnopek » Unassigned
StatusFileSize
new29.14 KB
new5.17 KB

And here's a version that remove VariantWithTitleInterface/Trait and the default value that is given to the "Page title" for new variants. As discussed with @timplunkett and @EclipseGC on IRC, it's impractical to provide a default title in all situations and Page Manager in D7 didn't do it - so, we're just dropping that feature. This will have no effect on existing sites, it just means that when adding a new variant, you have to enter the "Page title" from scratch rather than starting with the Page label.

tim.plunkett’s picture

StatusFileSize
new36.11 KB
new12.87 KB

I plan to dig into this more later, just wanted to get started.

Interdiff is using the -w flag.

Biggest issue so far is the addition of variant-specific logic to DisplayVariantEditForm::submitForm().

Status: Needs review » Needs work

The last submitted patch, 21: 2511570-pageaware-21.patch, failed testing.

dsnopek’s picture

Biggest issue so far is the addition of variant-specific logic to DisplayVariantEditForm::submitForm().

Well, it's temporary until we get the Wizard stuff in. :-)

Also, I don't think this any worse than what we're doing for the selection conditions. We have an interface that defines the functions we're calling, and the form (so the form state) comes from the edit form.

If you can come up with a way to do this better, that'd be awesome, but as far as incremental changes go, IMO this isn't so bad...

eclipsegc’s picture

deleted comment because I didn't refresh before posting...

dsnopek’s picture

StatusFileSize
new31.48 KB
new29.14 KB
new5.17 KB
new36.11 KB
new12.87 KB

Attempted some hackery to get the patches added back to the issue. Hopefully it won't mess something else up. :-)

tim.plunkett’s picture

Agreed.

These test fails are unrelated, the branch is failing. Researching in #2565887: Fix test failures stemming from Dynamic Page Cache

dsnopek’s picture

Status: Needs work » Needs review

Looks like it's passing again :-)

tim.plunkett’s picture

+++ b/src/Entity/PageViewBuilder.php
@@ -7,8 +7,10 @@
@@ -23,11 +25,33 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N

@@ -23,11 +25,33 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N
     /** @var $entity \Drupal\page_manager\PageInterface */
     if ($display_variant = $entity->getExecutable()->selectDisplayVariant()) {
       $build = $display_variant->build();
+      static::mergeCacheTags($build, $entity);
     }
     return $build;
   }
 
   /**
+   * Recursively merge the cache tags from the entity into the child elements.
+   *
+   * @param array $element
+   *   A render array.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity who's cache tags we want to merge in.
+   */
+  protected static function mergeCacheTags(array &$element, EntityInterface $entity) {
+    foreach (Element::children($element) as $name) {
+      if (isset($element[$name]['#cache'])) {
+        $cacheability = CacheableMetadata::createFromRenderArray($element[$name]);
+        $cacheability->addCacheableDependency($entity);
+        $cacheability->applyTo($element[$name]);
+      }
+
+      // Recurse to children elements.
+      static::mergeCacheTags($element[$name], $entity);
+    }
+  }

+++ b/src/Plugin/DisplayVariant/BlockDisplayVariant.php
@@ -128,23 +119,22 @@ public static function create(ContainerInterface $container, array $configuratio
-    $cacheability = CacheableMetadata::createFromRenderArray($build['regions'])
-      ->addCacheableDependency($page);
+    $cacheability = CacheableMetadata::createFromRenderArray($build['regions']);

Per @dsnopek:

So, before the patch, it's merging the Page cacheability metadata into the render arrays for each of the blocks
But we want the display variant to not have to know it's in a Page (because it could be in a mini-Panel, or Panelizer, or something else)
So, after building the variant with all the blocks, the Page has to attach the cacheability metadata to whatever is in the render array
But because the variant could contain *anything*, we can't make any assumptions about what it'll contain, so we need to recursively process the render array looking for cacheability metadata

Other question from david:

Would it make sense to not recurse if the parent has '#cache'?

eclipsegc’s picture

I think the cacheable metadata should probably be the DisplayVariant plugin's problem. Each plugin should have the knowledge to gather cacheable metadata specific to its use case and the page object should just have to ask whatever variant it's about to deliver, not recurse over the whole returned render array. Is that not workable?

Eclipse

dsnopek’s picture

I think the cacheable metadata should probably be the DisplayVariant plugin's problem. Each plugin should have the knowledge to gather cacheable metadata specific to its use case ....

So, it is providing all the cacheable metadata specific to it's use-case.

However, for whatever reason, we need to add the page cacheable metadata to that. I'm not entirely sure on why, but the current code does it, and if you remove that, then the tests (and manual testing) break.

I wonder if we could just provide it at the top-level? We'd need someone more knowledgeable about render caching to say if that's really the same thing and doesn't have a negative impact on performance, though.

I may experiment with some variations that add the caching data differently to see if they pass the tests...

wim leers’s picture

So, before the patch, it's merging the Page cacheability metadata into the render arrays for each of the blocks

Why was/is that even necessary? The cacheability metadata applies to the Page. The page just happen to contains those blocks. So why does each block need the cacheability metadata of the page? Is each block somehow being customized depending on the page?

However, for whatever reason, we need to add the page cacheable metadata to that. I'm not entirely sure on why, but the current code does it, and if you remove that, then the tests (and manual testing) break.

Sounds like you don't know the answer to my questions? We need to first figure out why that's necessary. Perhaps some of the tests have wrong assertions?

I wonder if we could just provide it at the top-level?

Yes!

Cacheability metadata bubbles. The Page, when render cached, will automatically inherit all the cacheability metadata of everything it contains, which from what I gather from this issue, is only Blocks.

What happens in this patch (and in HEAD also apparently), is effectively "reverse bubbling". There should be no need for that, unless the blocks actually depend on their parent, in which case this is absolutely the wrong way to do this. Before being able to give some advice in that area, I first need to understand why (and if!) it's actually necessary to do that — as said above.

dsnopek’s picture

StatusFileSize
new35.87 KB
new1.44 KB

Thanks, Wim! My knowledge of render caching is minimal, I was just trying to preserve what the code is already doing, assuming it was right. :-)

Here's a new patch that just applies the cache stuff to the top-level. Unfortunately, I'm having trouble running the tests locally (I get a fun fatal about symfony/translation/TranslatorInterface.php) so let's see if the testbot has better luck!

Status: Needs review » Needs work

The last submitted patch, 33: page_manager-remove-page-aware-2511570-33.patch, failed testing.

The last submitted patch, 33: page_manager-remove-page-aware-2511570-33.patch, failed testing.

dsnopek’s picture

Hrm. Well, that didn't work! Re-testing the patch from #21 just to make sure it wasn't from another change in HEAD.

dsnopek’s picture

Looks like #21 is still passing. So, providing the cache metadata at the top-level doesn't work for some reason. :-/

I think @Berdir is the one who originally wrote the code in HEAD that's adding the page cache metadata to the block cache metadata, so maybe he knows why it's necessary?

For reference here's the code in HEAD:

        $block_build = [
          '#theme' => 'block',
          '#attributes' => [],
          '#weight' => $weight++,
          '#configuration' => $block->getConfiguration(),
          '#plugin_id' => $block->getPluginId(),
          '#base_plugin_id' => $block->getBaseId(),
          '#derivative_plugin_id' => $block->getDerivativeId(),
          '#block_plugin' => $block,
          '#pre_render' => [[$this, 'buildBlock']],
          '#cache' => [
            'keys' => ['page_manager_page', $page->id(), 'block', $block_id],
            // Each block needs cache tags of the page and the block plugin, as
            // only the page is a config entity that will trigger cache tag
            // invalidations in case of block configuration changes.
            'tags' => Cache::mergeTags($page->getCacheTags(), $block->getCacheTags()),
            'contexts' => $block->getCacheContexts(),
            'max-age' => $block->getCacheMaxAge(),
          ],
        ];

        // Merge the cacheability metadata of blocks into the page. This helps
        // to avoid cache redirects if the blocks have more cache contexts than
        // the page, which the page must respect as well.
        $cacheability->addCacheableDependency($block);

http://cgit.drupalcode.org/page_manager/tree/src/Plugin/DisplayVariant/B...

dsnopek’s picture

Hrm, I also just noticed that at some point the patch switched from just merging cache tags to merging all cacheability metadata... Back in #14, we did this in the patch:

+  protected static function mergeCacheTags(array &$element, EntityInterface $entity) {
+    foreach (Element::children($element) as $name) {
+      if (isset($element[$name]['#cache'])) {
+        if (empty($element[$name]['#cache']['tags'])) {
+          $element[$name]['#cache']['tags'] = $entity->getCacheTags();
+        }
+        else {
+          $element[$name]['#cache']['tags'] = Cache::mergeTags($entity->getCacheTags(), $element[$name]['#cache']['tags']);
+        }
+      }
+
+      // Recurse to children elements.
+      static::mergeCacheTags($element[$name], $entity);
+    }
+  }

Not sure why we switched, but it's probably my fault. :-)

Anyway, would that code be more acceptable than what's in #21, since it's targeting only tags specifically?

dsnopek’s picture

Status: Needs work » Needs review
StatusFileSize
new35.8 KB

Status: Needs review » Needs work

The last submitted patch, 39: page_manager-remove-page-aware-2511570-39.patch, failed testing.

wim leers’s picture

One less fail, interesting…

The last submitted patch, 39: page_manager-remove-page-aware-2511570-39.patch, failed testing.

dsnopek’s picture

So, here's an attempt at explaining this based on the comments and the code in HEAD:

  1. The blocks are being cached independently of the page (rather than just the whole page being cached)
  2. The way the blocks are rendered depends on page entity, so if the page entity changes, the cache for all of the blocks needs to be cleared
  3. The way HEAD is making this happen, is by merging the cache tags from the page entity into the cache tags for each block

However, in #32, Wim writes:

What happens in this patch (and in HEAD also apparently), is effectively "reverse bubbling". There should be no need for that, unless the blocks actually depend on their parent, in which case this is absolutely the wrong way to do this.

And so assuming I've got the above right, the question is: What is the right way to do this?

A random idea: Would it be better for display variants to optionally implement RefinableCacheableDependencyInterface so that before $variant->build() we could call $variant->addCacheableDependency($page) and then the variant will attach those cache tags to anything inside? Or is that still the reverse bubbling and done wrong?

Thanks!

saltednut’s picture

Is each block somehow being customized depending on the page?

I would say yes to that potentially for a great many block. It sounds like you are describing the essential concept around panels contexts as they are often used in D7 Panelizer, For example, a Views pane included on a page would need to be able to have access to page context to be inherited by the contextual argument of the View.

Views Panes do not exist yet in Drupal 8, but I assume if we want to do Views Panes and Panelizer we would need allow for blocks to inherit these dynamic page-based contexts.

berdir’s picture

Wim, we have discussed this before :)

The page is the only thing that has cache tags. Neither the block configurations nor the displays have anything that is invalidated if they change, they *must* have the cache tags and contexts of the page. So that every block, which is separately cached (additionally to the page) is invalidated if their configuration changes.

That will change when displays are their own config entity then thy will need to use the cacheability metadata of those. And possibly additionally still the page, if changing the page can change them (e.g., changing static context configuration, assuming that they remain in the page).

dsnopek’s picture

Status: Needs work » Needs review
StatusFileSize
new36.9 KB
new1.72 KB

I've been reading https://www.drupal.org/developing/api/8/render/arrays/cacheability to try and (finally) get a handle on render caching, and I think I understand what we need to do here:

Every element within the render array returned by the variant that has $element['#cache']['keys'] is cached independently. Anything within the render array can potentially depend on the page entity, so, anything that is cached independently needs also get the cache tags of the page entity.

@Berdir / @Wim Leers: Does that sound sane?

Attached is a patch which implements this.

Status: Needs review » Needs work

The last submitted patch, 46: page_manager-remove-page-aware-2511570-46.patch, failed testing.

dsnopek’s picture

Hrm. I suspect that #2558261: Move code in BlockDisplayVariant::build() to a #pre_render is the reason this isn't working. We might have to put the merging of cache tags into $element['#after_build'] or something - whatever we can get to run after $element['#pre_render']. :-/

Probably won't get to work on this more until next week...

The last submitted patch, 46: page_manager-remove-page-aware-2511570-46.patch, failed testing.

tim.plunkett’s picture

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new5.33 KB
new29.36 KB
new19.95 KB

Reroll on top of that issue. The do-not-test patch is after copy/pasting the code first.

Looking into the test failures now.

Status: Needs review » Needs work

The last submitted patch, 51: 2511570-pageaware-51.patch, failed testing.

The last submitted patch, 51: 2511570-pageaware-51.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new29.58 KB
new1.7 KB

@todo fixing and renaming can come later, but this fixes things for me.

dsnopek’s picture

I've been thinking about this a lot, and I'm now pretty convinced that applying the cache tags (and cacheable metadata, in general) in PageViewBuilder isn't the right way to do this.

Here's the problem described really generally:

  1. The variant is passed it's configuration (like any plugin implementing ConfigurablePluginInterface) via $variant->setConfiguration()
  2. Since the variant can be used by multiple systems, ie. in Page Manager, in core (ala Panels Everywhere), in mini-Panels, Panelizer, etc -- it has no idea where that configuration actually came from
  3. However, in order to correctly implement render caching, we need to put information on the render array about the source of the configuration, so that the render cache can be cleared when the configuration changes
  4. The variant itself is kind of a "black box" in that from the outside we don't really know what it's going to put in the render array and it could do clever stuff with caching, like setting $element['#cache']['keys'] on the blocks so they're cached independently or using $element['#pre_render'] to prevent loading the blocks if the full variant is in the cache, etc. These are good performance improvements! But only the variant itself is really fully qualified to know where the cacheable metadata should be added (and what should be added, ex. adding only the cache tags on the blocks)

Now, the current patch works by analyzing the render array after the variant has created it and tries to figure out where the cacheable metadata should go. We trying to "out clever" the variant on point #4, which is itself a little suspect :-)

This totally works for Page Manager, because we have the PageViewBuilder which can post-process the render array before ultimately returning it to the controller. But it won't work in core, where we can only give PageDisplayVariantSelectionEvent the name of the variant and config -- we have no opportunity to post-process the render array.

So, this and point #4 above, leads me to believe that we should actually be passing cacheable metadata to the variant (just like we pass it configuration - since the configuration and the cacheable metadata are linked) and allow the variant to add it to the render array where and how it sees fit.

One way to do this would be to have VariantInterface (in core) also extend RefinableCacheableDependencyInterface, and then we can call $variant->addCacheableDependency($page) before calling $variant->build(). Since this problem extends all the way to core (we'll need to change PageDisplayVariantSelectionEvent so we can pass the cache metadata in with the configuration too) that seems like the appropriate place to try and solve it.

But there's other ways to do this too...

Anyway, please let me know what you think!

dsnopek’s picture

Tim created a core issue to make the changes to VariantInterface and VariantBase that I described in #55:

#2569083: Allow passing cacheable metadata to display variants (to enable Panels Everywhere)

dsnopek’s picture

StatusFileSize
new28.47 KB
new4.06 KB

Here's an attempt at a new patch based on the core patch in #2569083: Allow passing cacheable metadata to display variants (to enable Panels Everywhere) - it won't pass testbot yet, but it is passing tests for me locally. Overall, this feels a lot better to me than the previous attempts :-)

eclipsegc’s picture

That interdiff looks like a big improvement.

Eclipse

wim leers’s picture

#45 Thanks, that is super helpful! We indeed discussed that eons ago :)

The page is the only thing that has cache tags. Neither the block configurations nor the displays have anything that is invalidated if they change, they *must* have the cache tags and contexts of the page. So that every block, which is separately cached (additionally to the page) is invalidated if their configuration changes.

This is quite confusing though.

If a block is render cached, then that means we want to be able to reuse it elsewhere. So the same block may appear in different pages. If that is not true, then there is no point in render caching individual blocks; we should just render cache the Page Manager Page.

However, if the block does depend on the page, then … A) how can we possibly render cache blocks across pages (i.e. aren't we caching each block separately for every Page, and therefore having a terrible cache hit ratio?), B) why doesn't the block actually vary by Page?


#55: great analysis, thank you!

[…] leads me to believe that we should actually be passing cacheable metadata to the variant (just like we pass it configuration - since the configuration and the cacheable metadata are linked) […]

(Emphasis mine.)
This indicates the configuration itself should carry cacheability metadata. Because when the configuration changes, the output changes. So the configuration needs a cache tag (and perhaps a cache context if different configuration may be loaded depending on the request context, think language-specific configuration). This is a solved problem: #2524082: Config overrides should provide cacheability metadata.

I can't imagine that the configuration is coming from somewhere else than Config already, and every piece of config has its own cache tag. AFAICT Page Manager already is using config (page_manager/tree/config/schema/page_manager.schema.yml). ConfigBase already has cacheability metadata. It seems the key problem is the distinction between ConfigurablePluginInterface::setConfiguration(array $configuration) and that array of configuration actually originating from a ConfigBase object, which does carry cacheability metadata. It's that conversion from a config object to a plain array that leads to metadata loss, and that is AFAICT the source of the pain here.

Of course… changing ConfigurablePluginInterface at this time is likely a no-go. Could we add an optional interface that has a setCacheableConfiguration(ConfigBase $configuration) signature? If we can't, then the proposal in #55, with VariantInterface::addCacheableDependency($dependency) seems to be the only option.

dsnopek’s picture

Thanks, Wim!

If a block is render cached, then that means we want to be able to reuse it elsewhere. So the same block may appear in different pages. If that is not true, then there is no point in render caching individual blocks; we should just render cache the Page Manager Page.

Actually, I think this provides value even if the same block doesn't appear on other pages. Take these two scenarios:

Scenario #1: All the blocks on a page are cacheable, so their cacheable metadata bubbles up and the page itself is cacheable. So, the full page will be pulled out of the render cache. The fact that the individual blocks are cached doesn't really matter in practice (but they do take up space in the render cache.)

Scenario #2: One of the blocks isn't cacheable (ie. sets 'max-age' to 0) which bubbles up and the page itself isn't cacheable. But since all the other blocks are cacheable (and cached independently under their own cache key), they'll be pulled individually from the render cache.

I'm guessing this is the reason we're currently using the render cache the way we are. We could remove the cache key from the blocks if the full page is cacheable or something like that, as to not add them to the render cache when it's not necessary?

Could we add an optional interface that has a setCacheableConfiguration(ConfigBase $configuration) signature?

Would this mean passing the full config entity? Or can we extract a ConfigBase that is only a sub-set of the config entity?

The thing is, there's more stuff on the config entity than just the config for a single variant. (In fact, in current page_manager HEAD, the configuration for every variant on the page is stored in a single config entity.) And the variant shouldn't need to understand all the stuff on the full config entity or it's structure, especially since the goal of this issue is to decouple the BlockDisplayVariant from page_manager so we can re-use it in other Panels eco-system modules (ie. Panels, mini-Panels, Panelizer, etc) which will use their own config with it's own structure.

But if it's possible to make a new object which descends from ConfigBase which has only the configuration that applies to the variant, this totally workable!

However, I'm not really sure this can be an optional interface - any display variant that takes configuration at all will need to get the cache metadata information as well or it can't correctly provide cache metadata to the render API. So, if we do it like this, VariantInterface (in core) should extend this new interface and an implementation should be added to VariantBase (also in core).

All that said, I'm not sure this is better than the current proposal? I like that the cache metadata would get passed in the same setter (ie. setCacheableConfiguration()) as the configuration so it's clear they're linked, but since we'll need to muck around with making a ConfigBase that's a subset of the full config entity, it probably has worse DX...

wim leers’s picture

I'm guessing this is the reason we're currently using the render cache the way we are. We could remove the cache key from the blocks if the full page is cacheable or something like that, as to not add them to the render cache when it's not necessary?

That sounds right.

(You're really getting the hang of this render caching and cacheability thing, aren't you? ;) :))

Would this mean passing the full config entity?

Yes.

Or can we extract a ConfigBase that is only a sub-set of the config entity?

That's also possible, but what's the point, if we've already loaded the full config entity anyway?

The thing is, there's more stuff on the config entity than just the config for a single variant.

Aha!

But if it's possible to make a new object which descends from ConfigBase which has only the configuration that applies to the variant […]

I'm afraid that doesn't make a whole lot of sense. It'd be an artificial config object, that doesn't correspond to any actual config.

But… this is starting to sound like we need another value object, something like:

class ConfigSubset implements CacheableDependencyInterface {

  protected $config;
  protected $key;

  function __construct(ConfigBase $config, array $key) {
    $this->config = $config;
    $this->key = $key;
  }

  function getConfiguration() {
     return $this->config->get($this->key);
  }

  function getCacheTags() { return $this->config->getCacheTags(); }  
  function getCacheContexts() { return $this->config->getCacheContexts(); }  
  function getCacheMaxAge() { return $this->config->getCacheMaxAge(); }
}
dsnopek’s picture

(You're really getting the hang of this render caching and cacheability thing, aren't you? ;) :))

Thanks largely to the great handbook pages on the topic :-)

But… this is starting to sound like we need another value object

I think the ConfigSubset object would definitely help with passing around config and preserving cacheable metadata! (And probably a good idea independent of this issue!)

If only ConfigurablePluginInterface::setConfiguration() took a ConfigBase, then this would be the obvious way to solve this. :-/

Returning to the proposal of adding a VariantInterface::setCacheableConfiguration(ConfigBase $config):

Since we're stuck with ConfigurablePluginInterface as it is, and cachable metadata is basically required for every variant that takes configuration, we'd have to say something like "on classes implementing VariantInterface, please don't ever call $variant->setConfiguration(), only ever call $variant->setCacheableConfiguration()". Which would be super weird if you're passing the $variant to some function that works generically with ConfigurablePluginInterface and doesn't know (or care) that this is a VariantInterface with special rules.

The DX of saying "if you call $variant->setConfiguration() you must also be sure to call $variant->addCacheableDependency() for anything the configuration varies on" (like with the currently proposed core patch) - while totally not ideal, seems more straight forward to me.

wim leers’s picture

Thanks largely to the great handbook pages on the topic :-)

Oh, yay! :) Very glad to hear that! Feel free to ping me with any and all feedback, small and big. It does need to become better, and without criticism, that won't happen. But, great to see it has done the job at least for you :)

The DX of saying "if you call $variant->setConfiguration() you must also be sure to call $variant->setCacheableDependency() for anything the configuration varies on" (like with the currently proposed core patch) - while totally not ideal, seems more straight forward to me.

+1 — with one caveat: it'll still be easy to break things: by simply forgetting to call addCacheableDependency(). OTOH, few people will actually be calling VariantInterface::setConfiguration(), so the damage is limited. And thanks to test coverage you'd quickly find it's broken.

So, yes, for BC reasons, this is the best choice. But… what about the dozens, possibly hundreds of implementations of ConfigurablePluginInterface in the lifetime of Drupal 8? Many of them will be plugins that end up rendering things. And thus will need cacheability metadata for the configuration they're given. And so this work-around will need to be repeated many, many times in D8 contrib/custom.

That's why I just want to pose the question: Wouldn't it be better to break BC in D8 HEAD now, so that D8 contrib modules won't have to figure this out all over again?

dsnopek’s picture

So, yes, for BC reasons, this is the best choice. But… what about the dozens, possibly hundreds of implementations of ConfigurablePluginInterface in the lifetime of Drupal 8? Many of them will be plugins that end up rendering things. And thus will need cacheability metadata for the configuration they're given. And so this work-around will need to be repeated many, many times in D8 contrib/custom.

That's why I just want to pose the question: Wouldn't it be better to break BC in D8 HEAD now, so that D8 contrib modules won't have to figure this out all over again?

I agree with this completely, and would definitely follow/support/+1 a core issue around this! If you open it, I'll join you there. :-)

I'm not terribly confident that the rest of the core team will go for it given how close D8-rc1 seems to be, and how much code this will affect, but at least we have a fallback option ready.

wim leers’s picture

Pinging Tim Plunkett to get his thoughts.

dsnopek’s picture

So, since this issue is blocking loads of other issues in page_manager, ctools and panels, I personally think we should commit the patch from #54 right away. However, this will need to get fixed the "right way" at some point or Panels Everywhere will be impossible. :-/ I've e-mailed Tim for his thoughts on this!

As far as the "right way," I created an issue for @Wim's (and other) proposals to fix this for all plugins that implement ConfigurablePluginInterface:

#2571009: Plugins implementing ConfigurablePluginInterface that generate render arrays need to be passed cacheability metadata for their configuration! (or it's impossible to build render arrays with cacheability metadata)

If the core maintainers seem OK with any of those solutions, we can close the other core issue that's just for VariantInterface (ie. #2569083: Allow passing cacheable metadata to display variants (to enable Panels Everywhere)) -- if not, we have that as a fallback.

If Tim commits #54, I'll open a new issue to hold the patch from #57.

berdir’s picture

Mostly +1 on the discussions here.

@dsnopek is exactly right. You might have 20 blocks on a page, 15 of them are cached indefinitely (until invalidated), some are often validated, some aren't. One is cached for 10 minutes one might not be cachable at all, and so on.

The interdiff looks like a great simplification, however, if you do this, you should also merge in the cache contexts and max age. As you can see, this makes things a bit more complicated, I'll get back to that in the next paragraph :)

That said, #2569083: Allow passing cacheable metadata to display variants (to enable Panels Everywhere) seems much more actionable and reasonable right now than #2571009: Plugins implementing ConfigurablePluginInterface that generate render arrays need to be passed cacheability metadata for their configuration! (or it's impossible to build render arrays with cacheability metadata), since we don't really know how those use cases would look like. Our existing plugins that return render arrays (blocks, field formatters, some views plugins) currently don't really need it and it would make implementing them much more complicated since they'd always have to consider the possibly added cacheability metadata and dozens of block, field formatter and views plugins would have to be updated without known use cases.

So, I'd definitely vote to go with the first, more specific issue. We could even, at least as a first step, just make the interface optional and check with an instanceof before calling those methods.. Then we don't have to wait on core at all and can just remove the instanceof at the end.

dsnopek’s picture

Status: Needs review » Needs work

We could even, at least as a first step, just make the interface optional and check with an instanceof before calling those methods.. Then we don't have to wait on core at all and can just remove the instanceof at the end.

Discussed this with @Berdir on IRC. I think this makes sense! Then the committed patch will look much closer to the final solution once we have one of the patches in core. I'll roll a new patch in a few hours (hopefully) or tomorrow if I run out of time.

dsnopek’s picture

Status: Needs work » Needs review
StatusFileSize
new29.13 KB
new2.32 KB

Alright! Here's an interim patch we can use until we fix this in core. It still definitely needs to be fixed in core to allow Panels Everywhere, but this keeps the Page Manager code pretty clean (although, the interdiff will need to be reverted when the core fix is in).

tim.plunkett’s picture

Status: Needs review » Fixed

Still not happy about the @todo we're adding, but let's make progress.

  • tim.plunkett committed 5c96be6 on 8.x-1.x authored by dsnopek
    Issue #2511570 by dsnopek, tim.plunkett: Remove Drupal\page_manager\...
dsnopek’s picture

Woohoo, thanks! :-)

The goal is to fix that @todo in #2550879: Use CTools Wizard API to add/edit Pages (and move plugin UI using PluginWizardInterface) when we can move the block configuration to a wizard step that's returned by the variant. I suppose we could have put a link in the code comment, but hopefully, we'll kick ass on moving that forward at the Barcelona sprint. ;-)

wim leers’s picture

  1. +++ b/src/Form/DisplayVariantEditForm.php
    @@ -140,6 +304,16 @@ class DisplayVariantEditForm extends DisplayVariantFormBase {
       public function submitForm(array &$form, FormStateInterface $form_state) {
    +    // @todo This feels very wrong.
    +    if ($this->displayVariant instanceof BlockVariantInterface) {
    +      // If the blocks were rearranged, update their values.
    +      if (!$form_state->isValueEmpty(['display_variant', 'blocks'])) {
    +        foreach ($form_state->getValue(['display_variant', 'blocks']) as $block_id => $block_values) {
    +          $this->displayVariant->updateBlock($block_id, $block_values);
    +        }
    +      }
    +    }
    +
         parent::submitForm($form, $form_state);
    

    Just for my understanding: this is completely unrelated to the cacheability aspects in this issue, right?

  2. +++ b/src/Plugin/DisplayVariant/BlockDisplayVariant.php
    @@ -39,12 +38,13 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
    +  use RefinableCacheableDependencyTrait;
    

    :)

dsnopek’s picture

Re: #73.1: Yeah, that's unrelated to the cacheabliity stuff - it's about the main point of this issue which was to decouple the display variant provided by page_manager from the Page entity, so that we could move it to CTools and re-use it in Panels, and other Panels eco-system modules.

Status: Fixed » Closed (fixed)

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