Problem/Motivation

When generating URLs (and thus also when generating links, which are URLs wrapped in <a> tags), Drupal applies "outbound path processing" and "outbound route processing".

This means that the generated URLs/links depend on request context as well, and thus the outbound path/route processors affect cacheability of the generated URLs/links.

Examples include adding CSRF query parameters for routes that need CSRF tokens, and the language prefix for multilingual sites.

#1805054: Cache localized, access filtered, URL resolved, and rendered menu trees needs this to work correctly, otherwise render caching of menus is not possible.

Proposed resolution

While generating a URL, let outbound path/route processors add their cacheability metadata.

Remaining tasks

None.

User interface changes

None.

API changes

There are two areas:

Associating cacheability metadata
  1. \Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound and \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface::processOutbound() have a new optional parameter: CacheableMetadata $cacheable_metadata. Implementations of this interface can thus append their cacheability metadata to resulting path/route.
  2. Consequently, \Drupal\Core\Routing\UrlGeneratorInterface::generateFrom(Route|Path)() also have a new optional parameter: $collect_cacheability_metadata = FALSE. The default value of FALSE means no cacheability metadata is returned. When it is set to TRUE, a GeneratedUrlobject is returned. This is a subclass of CacheableMetadata (to store cacheability metadata), with one addition: it also stores the generated URL.
  3. Similarly, LinkGeneratorInterface::generate(FromLink)() have also gained this exact parameter and behavior: a GeneratedLink object is returned when $collect_cacheability_metadata === TRUE.
  4. Finally, \Drupal\Core\Url::toString() has also gained this optional parameter. Usually, URLs are rendered as links. Then #type => link takes care of things. But, it is entirely possible to use $url->toString() while generating a render array, and in that case, cacheability metadata should be collected. This is added for those use cases. Also: #type => link uses LinkGenerator, which uses $url->toString().
  5. UrlGenerator::generateFrom(Route|Path)() now also add the 'url.site' cache context when they're asked to generate an absolute URL. So if a site is accessible via multiple sites (in a multisite set-up), we serve the absolute URLs associated with the currently active host.
Note that in the case of outbound path/route processors, we pass a CacheableMetadata object (and PHP passes objects by reference, so consider it "by reference"), and in the case of generating a URL/link, we use a different pattern: we accept an optional boolean parameter that affects the return value.
Ideally, we'd always use that boolean (latter) approach — it's also what we use in \Drupal\Core\Access\AccessibleInterface::access([…], […], $return_as_object = FALSE).
But, in the case of outbound path/route processors, this would be extremely wasteful: we'd be generating a CacheableMetadata object for every single outbound path/route processor, and then be merging it. By instead passing in a CacheableMetadata object, the outbound path/route processors that need to, can just modify that object to add their cacheability.
Bubbling cacheability metadata
These aren't actual API changes; the DX/TX remains exactly the same, they just do more things under the hood.
  1. #type => link (\Drupal\Core\Render\Element\Link) now sets the cacheability metadata of the URL it links to. Thus making every URL contain the necessary cacheability metadata.
  2. Twig's link(), url() and url_from_path() functions now bubble the necessary cacheability metadata too.
CommentFileSizeAuthor
#171 outbound_processors_cacheability-2335661-171.patch116.84 KBWim Leers
#168 interdiff.txt996 bytesWim Leers
#168 outbound_processors_cacheability-2335661-168.patch117.54 KBWim Leers
#165 interdiff.txt1.65 KBWim Leers
#165 outbound_processors_cacheability-2335661-165.patch117.36 KBWim Leers
#157 interdiff.txt1.16 KBWim Leers
#157 outbound_processors_cacheability-2335661-157.patch117.54 KBWim Leers
#153 xhprof runs.zip679.52 KBWim Leers
#148 interdiff.txt1.75 KBWim Leers
#148 outbound_processors_cacheability-2335661-148.patch117.23 KBWim Leers
#146 interdiff.txt4.48 KBWim Leers
#146 outbound_processors_cacheability-2335661-144.patch118.19 KBWim Leers
#139 xhprof runs.zip339.3 KBWim Leers
#137 interdiff.txt37.58 KBWim Leers
#137 outbound_processors_cacheability-2335661-137.patch118.48 KBWim Leers
#122 interdiff.txt924 bytesWim Leers
#122 outbound_processors_cacheability-2335661-117.patch113.28 KBWim Leers
#115 interdiff.txt16.57 KBWim Leers
#115 outbound_processors_cacheability-2335661-115.patch112.83 KBWim Leers
#113 interdiff.txt979 bytesWim Leers
#113 outbound_processors_cacheability-2335661-113.patch109.95 KBWim Leers
#108 interdiff.txt4.41 KBWim Leers
#108 outbound_processors_cacheability-2335661-108.patch109.05 KBWim Leers
#105 interdiff.txt42.27 KBWim Leers
#105 outbound_processors_cacheability-2335661-105.patch106.37 KBWim Leers
#103 interdiff.txt1.01 KBWim Leers
#103 outbound_processors_cacheability-2335661-103.patch65.11 KBWim Leers
#101 interdiff.txt27.47 KBWim Leers
#101 outbound_processors_cacheability-2335661-101.patch64.69 KBWim Leers
#98 interdiff.txt1.76 KBWim Leers
#98 outbound_processors_cacheability-2335661-98.patch59.02 KBWim Leers
#95 interdiff.txt2 KBWim Leers
#95 outbound_processors_cacheability-2335661-95.patch57.72 KBWim Leers
#94 interdiff.txt7.16 KBWim Leers
#94 outbound_processors_cacheability-2335661-94.patch59.67 KBWim Leers
#93 interdiff.txt1.93 KBWim Leers
#93 outbound_processors_cacheability-2335661-93.patch57.66 KBWim Leers
#91 interdiff.txt3.72 KBWim Leers
#91 outbound_processors_cacheability-2335661-91.patch55.8 KBWim Leers
#90 interdiff.txt5.22 KBWim Leers
#90 outbound_processors_cacheability-2335661-90.patch52.98 KBWim Leers
#79 interdiff.txt10.11 KBWim Leers
#79 outbound_processors_cacheability-2335661-79.patch53.11 KBWim Leers
#78 outbound_processors_cacheability-2335661-78.patch45.35 KBWim Leers
#71 interdiff.txt1 KBWim Leers
#71 outbound_processors_cacheability-2335661-71.patch46.33 KBWim Leers
#69 rebase-interdiff.txt897 bytesWim Leers
#69 outbound_processors_cacheability-2335661-69.patch45.39 KBWim Leers
#61 interdiff.txt5.54 KBWim Leers
#61 outbound_processors_cacheability-2335661-61.patch55.29 KBWim Leers
#58 increment.txt5.12 KBpwolanin
#58 2335661-58.patch48.88 KBpwolanin
#56 increment.txt2.43 KBpwolanin
#56 2335661-56.patch48.12 KBpwolanin
#54 increment.txt1.86 KBpwolanin
#54 2335661-54.patch45.69 KBpwolanin
#52 increment.txt1.36 KBpwolanin
#52 2335661-52.patch44.79 KBpwolanin
#49 increment.txt1.23 KBpwolanin
#49 2335661-48.patch43.91 KBpwolanin
#44 increment.txt4.19 KBpwolanin
#44 2335661-44.patch44.01 KBpwolanin
#42 increment.txt4.23 KBpwolanin
#42 2335661-42.patch39.82 KBpwolanin
#40 increment.txt33.58 KBpwolanin
#40 2335661-40.patch39.6 KBpwolanin
#39 increment.txt1.85 KBpwolanin
#39 2335661-39.patch29.67 KBpwolanin
#35 increment.txt4.44 KBpwolanin
#35 2335661-35.patch29.65 KBpwolanin
#32 increment.txt2.91 KBpwolanin
#32 2335661-32.patch26.11 KBpwolanin
#29 2335661-29.patch25.86 KBpwolanin
#6 menulinkcontent_iscacheable-2335661-5-test-only.patch2.37 KBWim Leers
#6 menulinkcontent_iscacheable-2335661-5.patch13.02 KBWim Leers
#4 menulinkcontent_iscacheable-2335661-4-do-not-test.patch11.25 KBWim Leers
#2 menulinkcontent_iscacheable-2335661-2.patch11.29 KBWim Leers
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Issue summary: View changes
Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Status: Active » Needs review
FileSize
11.29 KB

Status: Needs review » Needs work

The last submitted patch, 2: menulinkcontent_iscacheable-2335661-2.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
11.25 KB

Rerolled against HEAD, but will surely fail tests. I just want a review of the approach, so marking as "do not test".

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Menu/MenuLinkBase.php
    @@ -111,7 +163,14 @@ public function getMetaData() {
    +      $route = $this->routeProvider->getRouteByName($this->getUrlObject()->getRouteName());
    +      // If no outbound route processor applies, then this link is cacheable.
    +      return !$this->routeProcessorManager->applies($route);
    

    I am not sure whether this condition is actually the proper way, honestly. Yes for HEAD this might be true, though, the same cacheability issue could easily happen for path processors as well. Also: can we detect the information on save time of the menu item into the tree storage?

  2. +++ b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php
    @@ -50,7 +50,25 @@ public function addOutbound(OutboundRouteProcessorInterface $processor, $priorit
    +  public function applies(Route $route) {
    +    $processors = $this->getOutbound();
    +    foreach ($processors as $processor) {
    +      if ($processor->applies($route)) {
    +        // Return as soon as at least one outbound route processor applies.
    +        return TRUE;
    +      }
    +    }
    +    return FALSE;
    +  }
    +
    +  /**
    +   * {@inheritdoc}
    +   */
       public function processOutbound(Route $route, array &$parameters) {
    +    if (!$this->applies($route)) {
    +      return;
    +    }
    

    Note: This applies patterns usually detects that on route compile time, can we do the same? Store the route processors on route options?

Wim Leers’s picture

In this reroll, I'm no longer changing MenuLinkBase::isCacheable() — it made little sense after all. Now only changing MenuLinkContent. That makes the patch much, much simpler.

Also added test coverage.

Wim Leers’s picture

I am not sure whether this condition is actually the proper way, honestly. Yes for HEAD this might be true, though, the same cacheability issue could easily happen for path processors as well.

Do you mean there might be other ways for links to be dynamic, and therefore non-cacheable? Please clarify.

Also: can we detect the information on save time of the menu item into the tree storage?

I thought the same initially, but I don't think that's worth it. By definition, isCacheable() must be called rarely: only on render cache misses. Hence doing this at save time and saving it into tree storage feels like a premature optimization. Let's make it correct first, in the simplest way possible, and then we can see if it negatively affects performance. (In HEAD, nothing calls isCacheable(), so it's certain this does not introduce a performance regression.)

Note: This applies patterns usually detects that on route compile time, can we do the same? Store the route processors on route options?

This too is an optimization. This issue is about correctness.
Nothing is new here, I only moved the "does this apply?" logic out of the existing method!

Wim Leers’s picture

Issue summary: View changes
Wim Leers’s picture

Issue summary: View changes
Wim Leers’s picture

Title: MenuLinkContent::isCacheable() lies » MenuLinkContent::isCacheable() always returns TRUE, does not detect CSRF token links

Better title.

dawehner’s picture

Crazy shit, this issue already didn't got any comment since three days. Welcome to our world!

Do you mean there might be other ways for links to be dynamic, and therefore non-cacheable? Please clarify.

Well there are also path processors for example, which do change the path as well at the end of the day, and you can't really know how. (url alias by language for example).

This too is an optimization. This issue is about correctness.

Well, you absolutely fail at that yet, though, tbh ...
I also really fear that all our caching optimizations will make it literally impossible to swap out any service,
as we couple core to its own implementation details. (PS. The response would have been less aggressive if not people would push me to answer a 3 days old issue, sorry no offense, but rather invest your
time and review one of the many issues in the queue, less managment, more actual help.)

Wim Leers’s picture

I also really fear that all our caching optimizations will make it literally impossible to swap out any service, as we couple core to its own implementation details.

If that is happening somewhere, it's definitely not here. This issue is only making the implementation comply with the interface/API.

Well there are also path processors for example, which do change the path as well at the end of the day, and you can't really know how. (url alias by language for example).

You're right.

As you say, path processors can make arbitrary changes, and e.g. the language negotiation by URL path processor alters every single path. So, theoretically speaking, whenever any path processor is enabled, MenuLinkInterface::isCacheable() would always have to return FALSE.
But… practically speaking:

  • we render cache everything per language
  • the language negotiation by URL path processor is the only path processor (in core)
  • hence as long as menus are render cached per language, having ::isCacheable() return TRUE when the language negotiation processor running should actually make it return FALSE will not cause any bugs
  • a ::isCacheable() implementation that lies less is better

So, while this patch is definitely imperfect, it also definitely moves us in the right direction. Just like the patch that added MenuLinkInterface::isCacheable() clearly was also imperfect but landed anyway (otherwise this issue wouldn't exist) because it moved us in the right direction.

dawehner’s picture

If that is happening somewhere, it's definitely not here. This issue is only making the implementation comply with the interface/API.

Well, it does not happen in core, but do you know that contrib does? It seems odd that they would have to replace all menu links just because they rewrite URLs in their specific way ... Well, let's HOPE that this doesn't results in some security problem on the longrun.

In general It would be great to actually expand the test coverage of the route process manager to take into account the applies() method because atm. there is no real test coverage here.

PS: I don't really get why this patch is 100% critical, this does not seem to block a review from my perspective, at least atm.?

Wim Leers’s picture

I don't really get why this patch is 100% critical

This blocks #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees, which is critical.

Well, it does not happen in core, but do you know that contrib does?

I agree, and I said that in #12 also. And in fact, this partially fixes an existing security problem in HEAD: the current implementation of ::isCacheable() (which always returns TRUE) causes code to assume that links are cacheable that aren't. This can easily lead to security problems, such as session tokens being stolen.

Let me repeat:

So, while this patch is definitely imperfect, it also definitely moves us in the right direction. Just like the patch that added MenuLinkInterface::isCacheable() clearly was also imperfect but landed anyway (otherwise this issue wouldn't exist) because it moved us in the right direction.

dawehner’s picture

Let me try to rephrase what got responding with "nothing new". So It would be cool to have dedicated test coverage for:

    *
    * @param \Symfony\Component\Routing\Route $route
diff --git a/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php
index 3dc8864..4908b54 100644
--- a/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php
+++ b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php
@@ -50,7 +50,25 @@ public function addOutbound(OutboundRouteProcessorInterface $processor, $priorit
   /**
    * {@inheritdoc}
    */
+  public function applies(Route $route) {
+    $processors = $this->getOutbound();
+    foreach ($processors as $processor) {
+      if ($processor->applies($route)) {
+        // Return as soon as at least one outbound route processor applies.
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function processOutbound(Route $route, array &$parameters) {
+    if (!$this->applies($route)) {
+      return;
+    }
+
     $processors = $this->getOutbound();
     foreach ($processors as $processor) {
       $processor->processOutbound($route, $parameters);
dawehner’s picture

On top of that I wonder whether we can add at least a TODO to figure out how to deal with path processors?

catch’s picture

Outbound path processors would need to be able to indicate their cacheability somehow. It's the same problem area as #2287071: Add cacheability metadata to access checks and #2099137: Entity/field access and node grants not taken into account with core cache contexts vs. #post_render_cache.

Fabianx’s picture

Status: Needs review » Needs work
+++ b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php
@@ -253,6 +277,20 @@ public function isTranslatable() {
+      // If no outbound route processor applies, then this link is cacheable.
+      return !$this->routeProcessorManager->applies($route);
+    }

I think I am okay with that, but it needs a critical follow-up ...

There are many cases to do route altering that is cacheable, so I am meh.

CSRF should be in JS or via confirmation forms anyway and not added to links on generation time to solve more broad use cases.

Can we fix CSRF instead to use #post_render_cache and possibly even just add the tokens to JS in the #post_render_cache, then have a confirmation form on CSRF enabled routes for non-JS users.

Code needs work per #16.

Fabianx’s picture

I opened #2351015: URL generation does not bubble cache contexts for an alternate approach to the problem of CSRF tokens.

catch’s picture

Title: MenuLinkContent::isCacheable() always returns TRUE, does not detect CSRF token links » MenuLinkContent::isCacheable() doesn't take into account outbound path processors
Issue tags: +Needs issue summary update

I've bumped #2351015: URL generation does not bubble cache contexts to critical.

Re-titling this for outbound path processors, but we could also mark duplicate and open a new issue if that'd be clearer.

catch’s picture

Category: Task » Bug report
Priority: Critical » Major

Downgrading this to major for the non-CSRF portion. Also moving to a bug report though.

effulgentsia’s picture

See also #2417793-46: Allow entity: URIs to be entered in link fields, where changes to URL aliases don't result in a cache clear of rendered entities containing formatted links. Maybe that needs its own issue, but seems highly related to this, so just noting it here for now.

Wim Leers’s picture

Title: MenuLinkContent::isCacheable() doesn't take into account outbound path processors » Outbound path & route processors must specify cacheability metadata + MenuLinkInterface::isCacheable() is wrong

Let's slowly get this back on track, step 1: fixing the title.

Fabianx’s picture

xjm’s picture

Priority: Major » Critical

Elevating priority based on #25 and #2351015-57: URL generation does not bubble cache contexts, and discussion with @WimLeers and @pwolanin.

@pwolanin is also working on a helper for this in #2471743: Create a more generic superclass of \Drupal\Core\Render\BubbleableMetadata.

pwolanin’s picture

Wim Leers’s picture

Assigned: Unassigned » pwolanin
pwolanin’s picture

Title: Outbound path & route processors must specify cacheability metadata + MenuLinkInterface::isCacheable() is wrong » Outbound path & route processors must specify cacheability metadata + MenuLinkInterface::isCacheable() is incomplete
Status: Needs work » Needs review
FileSize
25.86 KB

Very rough 1st pass - probably broken, let's see how badly tests fail.

Status: Needs review » Needs work

The last submitted patch, 29: 2335661-29.patch, failed testing.

Wim Leers’s picture

  1. +++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
    @@ -47,6 +48,8 @@ public function processOutbound($route_name, Route $route, array &$parameters) {
    +      // Tokens are per user and per session, no not cacheable.
    

    s/no not/so not/

  2. +++ b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
    @@ -20,17 +20,17 @@
    -  public function processOutbound($path, &$options = array(), Request $request = NULL);
    +  public function processOutbound($path, CacheableMetadata $cacheable_metadata, &$options = array(), Request $request = NULL);
    
    +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
    @@ -288,7 +291,7 @@ public function generateFromRoute($name, $parameters = array(), $options = array
    +    $path = $this->processPath($path, $options, $cacheable_url_value);
    

    Parameter order mismatch is causing the test failures.

  3. +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php
    @@ -7,11 +7,14 @@
    + * @todo - remove this class when we remove UrlGenerator::fromPath().
    

    Is there an issue for this already that this can link to?

  4. +++ b/core/lib/Drupal/Core/Routing/CacheableUrl.php
    @@ -0,0 +1,39 @@
    +   * The string value of the url.
    

    s/url/URL/

  5. +++ b/core/lib/Drupal/Core/Routing/CacheableUrl.php
    @@ -0,0 +1,39 @@
    \ No newline at end of file
    

    .

pwolanin’s picture

Status: Needs work » Needs review
FileSize
26.11 KB
2.91 KB

Fixed those I think.

Status: Needs review » Needs work

The last submitted patch, 32: 2335661-32.patch, failed testing.

Wim Leers’s picture

Fatal error: Cannot use object of type Drupal\Core\Routing\CacheableUrl as array
pwolanin’s picture

Status: Needs work » Needs review
FileSize
29.65 KB
4.44 KB

still pretty broken, but some progress

Status: Needs review » Needs work

The last submitted patch, 35: 2335661-35.patch, failed testing.

Wim Leers’s picture

Issue tags: +Novice, +Needs reroll
+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -486,9 +487,11 @@ protected function processPath($path, &$options = array(), CacheableMetadata $ca
+   * @param CacheableMetadata $cacheable_metadata

Needs FQCN.

dawehner’s picture

So here is a question. Sadly the issue summary doesn't help with it. Why don't we implement it with multiple return values.
This is a way to a) don't break the API b) make it clear what is input and output in such function and c) we can still properly document it.

  1. +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php
    @@ -7,11 +7,14 @@
      * Processes the inbound path by resolving it to the front page if empty.
    + *
    + * @todo - remove this class when we remove UrlGenerator::fromPath().
      */
     class PathProcessorFront implements InboundPathProcessorInterface, OutboundPathProcessorInterface {
    

    This doesn't make sense, its just the outbound, we no longer need.

  2. +++ b/core/lib/Drupal/Core/Routing/CacheableUrl.php
    @@ -0,0 +1,39 @@
    +
    +class CacheableUrl extends CacheableMetadata {
    

    We should clearly document what this is about

  3. +++ b/core/lib/Drupal/Core/Routing/CacheableUrl.php
    @@ -0,0 +1,39 @@
    +   * @return self
    

    @return $this

pwolanin’s picture

Issue tags: -Novice, -Needs reroll
FileSize
29.67 KB
1.85 KB

Those are the easy ones - but it's more deeply broken.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
39.6 KB
33.58 KB

ok, new approach with just passing an object around to collect the data

Status: Needs review » Needs work

The last submitted patch, 40: 2335661-40.patch, failed testing.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
39.82 KB
4.23 KB

fix some leftover incorrect calls

dawehner’s picture

I seriously don't really like how cacheable stuff cripples into everything.
When we talked about that yesterday I though that we discussed that we add that cache information while rendering into the render metadata state and for menus call a specific method.

pwolanin’s picture

FileSize
44.01 KB
4.19 KB

Status: Needs review » Needs work

The last submitted patch, 44: 2335661-44.patch, failed testing.

pwolanin queued 44: 2335661-44.patch for re-testing.

The last submitted patch, 44: 2335661-44.patch, failed testing.

pwolanin’s picture

Issue summary: View changes
Status: Needs work » Needs review
Issue tags: -Needs issue summary update

Forgot to change the 2nd version of the template

pwolanin’s picture

FileSize
43.91 KB
1.23 KB

The last submitted patch, 39: 2335661-39.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 49: 2335661-48.patch, failed testing.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
44.79 KB
1.36 KB

Fixing return value map in a unit test.

Status: Needs review » Needs work

The last submitted patch, 52: 2335661-52.patch, failed testing.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
45.69 KB
1.86 KB

Fix a couple little bugs, including a pre-existing one in MenuForm

Status: Needs review » Needs work

The last submitted patch, 54: 2335661-54.patch, failed testing.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
48.12 KB
2.43 KB

fix 2 more unit test value maps.

Wim Leers’s picture

Status: Needs review » Needs work

Green patch! :)


dreditor review:

  1. +++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
    @@ -37,7 +36,7 @@ function __construct(CsrfTokenGenerator $csrf_token) {
    +  public function processOutbound($route_name, Route $route, array &$parameters, CacheableMetadata $cacheable_metadata = NuLL) {
    

    s/NuLL/NULL/

  2. +++ b/core/lib/Drupal/Core/Cache/CacheableMetadata.php
    @@ -129,4 +129,74 @@ public function setCacheMaxAge($max_age) {
    +   *   The other bubbleable metadata object.
    

    s/bubbleable/cacheable/

  3. +++ b/core/lib/Drupal/Core/Menu/MenuLinkTree.php
    @@ -226,12 +226,17 @@ public function build(array $tree, $level = 0) {
           $element = array();
           $element['attributes'] = new Attribute();
           $element['attributes']['class'] = $class;
    -      $element['title'] = $link->getTitle();
    -      $element['url'] = $link->getUrlObject();
    -      $element['url']->setOption('set_active_class', TRUE);
    +      $element['link']['#type'] = 'link';
    +      $element['link']['#title'] = $link->getTitle();
    +      $element['link']['#url'] = $link->getUrlObject();
    +      $element['link']['#url']->setOption('set_active_class', TRUE);
    +      $element['link']['#cache'] = array();
    +      if (!$link->isCacheable()) {
    +        $element['#cache']['max-age'] = 0;
    +      }
           $element['below'] = $data->subtree ? $this->build($data->subtree, $level + 1) : array();
           if (isset($data->options)) {
    -        $element['url']->setOptions(NestedArray::mergeDeep($element['url']->getOptions(), $data->options));
    +        $element['link']['#url']->setOptions(NestedArray::mergeDeep($element['link']['#url']->getOptions(), $data->options));
           }
           $element['original_link'] = $link;
    

    First thought: why keep ['attributes'] and not just move that to ['link']['#attributes']?

    Second thought: why even have ['link'] be at a nested level, why not set all of this directly on $element?

    Third thought: oh, we have ['original_link'] and especially ['below'], that's probably why. We definitely can't get rid of the latter.

    That then just leaves the first thought/question.

    EDIT: the menu.html.twig template gave me the answer: because the attributes apply to the menu link item, not the link itself. I.e. the template applies these attributes to the <li>, not the <a>.

  4. +++ b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
    @@ -20,17 +20,17 @@
    +   *   Optional object to collect information about cache contexts and tags.
    
    +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
    @@ -483,9 +487,11 @@ protected function processPath($path, &$options = array()) {
    +   *   An object to collect information about cache contexts and tags.
    
    +++ b/core/lib/Drupal/Core/Url.php
    @@ -728,15 +729,18 @@ public function setAbsolute($absolute = TRUE) {
    +   *  Optional object to collect cache tags and contexts for the URL.
    

    Also max-age. Why not just say (optional) Object to collect path processors' cacheability metadata.

  5. +++ b/core/lib/Drupal/Core/Render/Element/Link.php
    @@ -9,6 +9,8 @@
    +use Drupal\Core\Url as CoreUrl;
    

    Why is this aliasing necessary?

  6. +++ b/core/lib/Drupal/Core/Render/Element/Link.php
    @@ -75,9 +77,14 @@ public static function preRenderLink($element) {
    +      $merged = $cacheable_metadata->merge(CacheableMetadata::createFromRenderArray($element));
    +      $merged->applyTo($element);
    

    Alternatively: RendererInterface::addDependency().

    So these two lines would then become:

    $renderer->addDependency($element, $cacheable_metadata);
    

Next: applying patch + deeper review.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
48.88 KB
5.12 KB

re: use Drupal\Core\Url as CoreUrl; if you don't alias you get a fatal due to the presence of the class from core/lib/Drupal/Core/Render/Element/Url.php

I think keeping the merge code is more understandable (and more performant).

Wim Leers’s picture

For the deeper review, the main thing I'm interested in, is confirming that rendered menus are now indeed varied by cache contexts as necessary. This is the main problem of #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees.

This is easy to check: manually added a route:<current> menu link item to the "Tools" menu, which renders a link to the current route (route name + route parameters). This means that the menu must now be varied by route name + parameters, of which there can be zillions of combinations. Confirm it is working by navigating your Drupal 8 site, and confirm that the menu link you created indeed always points to the current URL!

The remaining problem to solve is to avoid creating three zillion variations: to automatically turn the parts that vary by a "high-frequency cache context" (such as the route and user cache contexts) into #post_render_cache placeholders, so that we can render only those dynamically. We can deal with that in #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees.
If you tried the example scenario above, you can look at the cache_render DB table, looking at the entries that contain the string "tools", and confirm that there are many different entries, one per route you visited.

We'll want test coverage that verifies this is working correctly. I can do that. Basically: an integration test for the changes made in this patch, to confirm that the cacheability metadata for the generated links bubbles up to the render cached menu block.

Fabianx’s picture

+++ b/core/themes/classy/templates/navigation/menu.html.twig
@@ -31,7 +31,7 @@
-          {{ link(item.title, item.url) }}
+          {{ item.link }}

This should not be necessary.

Because this is twig the link() function can just be changed to return a render array.

Overall it is great that this works! Thanks so much, Peter!

The cacheable meta data still feels like an after thought atm, but I can see that Urls are pure value objects.

The DX I would have liked is something to:

$url = Url::fromRoute(...);
$build['#markup'] = $url->generateLink('Link me to X');
$renderer->addDependency($build, $url);

But I can see that it can be beneficial to keep Urls as pure value objects (and injecting the link generator in them makes no sense, I can see that).

Still (asking does not hurt):

Could we at least for the link generator add the cacheable_metadata to the CoreUrl instead?

Or is it a no go that we change the url itself?

--

Also:

Not yet discussed with anyone, but maybe having a Link object might be beneficial, but on the other hand there is still Url->toString() ...

Overall:

I am not yet convinced that the approach of passing the cacheable_metadata everywhere is the right one. It is a huge step forward, but especially the ease of use of Url->toString() and having no meta data available inside the url even though an url was generated makes me dizzy ...

Or would the following work:

$url = Url::fromRoute('admin.run_cron'); // no idea what that is in real, but it has a CSRF token ...
$cacheable_metadata = new CacheableMetaData();

$build = [];
$build['#markup'] = $url->toString($cacheable_metadata);
$cacheable_metadata->applyTo($build);

Why not:

$url = Url::fromRoute('admin.run_cron'); // no idea what that is in real, but it has a CSRF token ...
$build = $url->toRenderArray();

instead?

We can then also enhance twig (and that is my biggest worry right now) to call toRenderArray() on an object if it exists instead of __toString().

Will discuss more with Wim tomorrow.

Wim Leers’s picture

Finish the changes to CacheableMetadata, including test coverage. It'd be better if we'd move all of those changes into a separate issue; they're not actually in scope for this issue, but they are necessary.

Wim Leers’s picture

Voila, now split #61's changes that affected CacheableMetadata + BubbleableMetadata to #2474121: CacheableMetadata should get BubbleableMetadata's merge/applyTo/createFromRenderArray/createFromObject methods.

Wim Leers’s picture

#58:

  • RE: CoreUrl: d'oh, right!
  • RE: keeping the merge code: I'm fine with that. But I don't think it's more understandable.

#60:

  • This should not be necessary. Because this is twig the link() function can just be changed to return a render array.

    Ah, interesting, we didn't think of that :)

  • The DX I would have liked is something to: […]

    That is interesting. We currently have this complex mess interaction between 4 things, with 2 symmetries: Url + Link value objects and UrlGenerator(Interface) + LinkGenerator(Interface) services. If we'd only have Url, and only UrlGenerator, and LinkGenerator would be deleted in favor of a Url::generateLink($text) or Url::toLink($text)method on the Url value object (much like it already has toString()), and a matching UrlGenerator::generateLink(string $text, Url $url), then things would be simpler: there'd be less confusion about which value objects/services to use best: there'd only be one choice.
    But… I think such changes are too late by now.

  • Could we at least for the link generator add the cacheable_metadata to the CoreUrl instead?

    Or is it a no go that we change the url itself?

    This is what pwolanin did at first, but it requires changes all over core, and with >90% certainty, also lots of changes to contrib. There are also still valid uses of generating URLs and links where cacheability doesn't matter (e.g. e-mails — I wanted to say feeds and REST responses too, but that's not really true).
    So, yes, this was done to minimize BC break.

  • Not yet discussed with anyone, but maybe having a Link object might be beneficial, but on the other hand there is still Url->toString() ...

    We already have that: \Drupal\Core\Link. The problem, as I said above, is that it's not clear which to best use when.

  • I am not yet convinced that the approach of passing the cacheable_metadata everywhere is the right one. It is a huge step forward, but especially the ease of use of Url->toString() and having no meta data available inside the url even though an url was generated makes me dizzy ...

    Until #39, pwolanin was letting UrlGenerator return a CacheableUrl object, which had a __toString implementation. We though that'd be sufficient for BC. Turns out it wasn't.
    So starting in #40, he embarked on the current route (pun intended).

  • Why not […] $url->toRenderArray() […] instead?

    We considered that also. I'm not sure anymore why we dismissed that. IIRC there was some problem with that. Could you chime in WRT that, @pwolanin?


+++ b/core/lib/Drupal/Core/Render/Element/Link.php
@@ -75,9 +77,14 @@ public static function preRenderLink($element) {
+    if (!empty($element['#url']) && $element['#url'] instanceof CoreUrl) {

Why is this instanceof check actually even necessary? It wasn't there before…

dawehner’s picture

I'm still confused by the fact that we don't use return parameters ... I mean really, I thought with ->access() we have kinda a similar pattern

  1. +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php
    @@ -114,6 +115,9 @@ public function processOutbound($path, &$options = array(), Request $request = N
    +        if ($cacheable_metadata) {
    +          $cacheable_metadata->addCacheContexts(['languages:' . LanguageInterface::TYPE_URL]);
    +        }
    

    This certainly varies by session, if you are honest, read the previous code in that class

  2. +++ b/core/modules/menu_ui/src/MenuForm.php
    @@ -344,7 +344,7 @@ protected function buildOverviewTreeForm($tree, $delta) {
    -        $form[$id]['title']['#markup'] = $this->linkGenerator->generate($link->getTitle(), $link->getUrlObject(), $link->getOptions());
    +        $form[$id]['title']['#markup'] = $this->linkGenerator->generate($link->getTitle(), $link->getUrlObject());
    

    This change is a bit odd ... given that we have different options ...

pwolanin’s picture

re: $link->getOptions() in MenuForm - previously, the 3rd param was thrown away, so that was just a bug.

The last submitted patch, 42: 2335661-42.patch, failed testing.

isntall queued 42: 2335661-42.patch for re-testing.

The last submitted patch, 42: 2335661-42.patch, failed testing.

Wim Leers’s picture

#2474121: CacheableMetadata should get BubbleableMetadata's merge/applyTo/createFromRenderArray/createFromObject methods landed, rebased this patch on top.

The rebase-interdiff.txt shows a small change that should've been made as part of #2474121, but was forgotten. But, instead of representing >10 KB of the changes in this patch, it's now only a few lines of changes, thanks to #2474121 having already landed.

Status: Needs review » Needs work

The last submitted patch, 69: outbound_processors_cacheability-2335661-69.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
46.33 KB
1 KB

The test coverage added in #2474055: Performance regression in contact_help() now needs to be updated too, which explains the first test failure. The second failure is AFAICT unrelated — it even occurs on my system with HEAD (probably because my computer has apc.enable_cli = 0).

Wim Leers’s picture

catch’s picture

+++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
@@ -47,6 +46,10 @@ public function processOutbound($route_name, Route $route, array &$parameters) {
+        // Tokens are per user and per session, so not cacheable.

After #2432837: Make cache contexts hierarchical (e.g. 'user' is more specific than 'user.roles') we can add session as a cache context (via the cookie one), so why not use that rather than setting not cacheable?

cpj’s picture

@catch #73, I'm presuming (hoping) this would also work for non cookie-authenticated sessions (as per Decouple session from cookie based user authentication) ?

Fabianx’s picture

The clean clean way is to add a csrf_token cache context, which uses the csrf_token manager to get a token ...

pwolanin’s picture

The cost of generating a token on the page is likely less than the cost of added cache complexity?

Wim Leers’s picture

#73: #74 is exactly why we did not add a session cache context in that issue.

Wim Leers’s picture

Wim Leers’s picture

And voila, test coverage.

There's no real good place to test this, unless we create mocked route & path processors. So instead, I opted for an integration test that tests existing outbound route/path processors where available, to test the entire gamut of possibilities.

If not deemed elegant enough, then it should at least be a good starting point, that shows what needs to be tested.

Wim Leers’s picture

I'll update the IS later today.

pwolanin’s picture

Patch looks fine - seems the only major change is the added kernel test. That's not the most readable, but I see basically what's going on in terms of collecting the cachability into the render array.

So barring any nits, and with an issue summary, looks ready to me.

Fabianx’s picture

Sorry, some parts of #60 still need to be addressed, some can be follow-up though.

pwolanin’s picture

@Fabianx - I'm not sure what's left from #60 - I guess my feeling is that we should move towards using link type render arrays and then in most cases this will just work.

I don't think the Url object should retain the cachability info, since it doesn't retain the generated string either - the two are directly linked.

Changing the twig link() function seems a little odd to me - I thought the switch there to creating a link-type render array in the menu tree code is a lot clearer and would make it easier to do something else in a template.

dawehner’s picture

Issue tags: +API change

Let's be fair about the tag.

Maybe someone please add to the issue summary why we went with the passed parameter and not different return values?,
as that was the approach for access result objects ...

webchick’s picture

Issue tags: +blocker

Just the tag fairy, carry on.

Wim Leers’s picture

#81: yeah, the only major change is the KTB test. I agree that it's far from ideal. But cleaning up all the test coverage for path & route processors would be out of scope too.

#82+ #83: it's not clear to me either what needs to happen. Also note that I replied to everything you wrote in #60 point-by-point in #63.

Could you please clarify your concerns?

Fabianx’s picture

Assigned: pwolanin » Fabianx

{{ my_url }}
e.g.
{% set url = url_from_path('system/cron') %}
{{ link(url.path, url.options) }}

are all valid use cases on the theme layer.

Will discuss with Wim later today.

I don't want to get into a situation where we need yet another BC break on the link / url layer.

dawehner’s picture

#81: yeah, the only major change is the KTB test. I agree that it's far from ideal. But cleaning up all the test coverage for path & route processors would be out of scope too.

I'm confused, given that the patch doesn't add test coverage for the various changes in the route processors out there. Don't we want to check whether the cache contexts are adding for those ones? Isn't that the entire point of the issue, so why should we not expand the test coverage.

Anyone mind to have a look at #84? I still don't get why we can't use a similar approach to the access result object one.

Wim Leers’s picture

Title: Outbound path & route processors must specify cacheability metadata + MenuLinkInterface::isCacheable() is incomplete » Outbound path & route processors must specify cacheability metadata
Assigned: Fabianx » Wim Leers
Issue summary: View changes
Related issues: +#2479767: Fix MenuLinkInterface::isCacheable(): remove it in favor of implementing CacheableDependencyInterface
FileSize
52.98 KB
5.22 KB

Addressed the immediate concerns in #82/#83/#87: menu.html.twig no longer using link(). There is an obvious solution, that I double-checked with Fabianx to be acceptable: let link() return a #type => link render array! Then we just get the same bubbling of cacheability metadata like we have for any render array. As a consequence, we can then remove a bunch of the changes we were making to templates, and simultaneously really make sure that outbound path & route processors' cacheability affect all links, not just those in menus.

The one thing that is incompatible with that is:

     if (!$link->isCacheable()) {
       $element['#cache']['max-age'] = 0;
     }

But, really, that is a separate problem. Ironically, it was the original goal of this issue ("fix MenuLinkInterface::isCacheable()"), but clearly the focus of this issue has shifted to the outbound route & path processing. That applies to all links & URLs. The MenuLinkInterface::isCacheable() part, and therefore the bit of cited code above, that really belongs in a separate issue.
So I opened #2479767: Fix MenuLinkInterface::isCacheable(): remove it in favor of implementing CacheableDependencyInterface for that.

Also, first step in cleaning up the IS.


This reroll makes any Twig template using link() bubble cacheability metadata. But what's still missing, is url(). Doing that next.

After that, I'll address #88. (Which will include making the IS 200% better.)

Wim Leers’s picture

This reroll makes any Twig template using link() bubble cacheability metadata. But what's still missing, is url(). Doing that next.

That's what this does.

Next: #88, and absolutely agreed that we need more/better test coverage, especially with the interdiffs in #90 and here!

Status: Needs review » Needs work

The last submitted patch, 91: outbound_processors_cacheability-2335661-91.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
57.66 KB
1.93 KB

First, fixing test failures.

Wim Leers’s picture

A first round of making the IS actually say what the patch does. Still working towards answering #88…

Fixing many minor things that I encountered while writing this IS update.

Wim Leers’s picture

While doing #94, I think I also noticed these two out-of-scope changes. Let's see if I was right in thinking these were out of scope: if they were, they shouldn't cause any test failures.

Status: Needs review » Needs work

The last submitted patch, 95: outbound_processors_cacheability-2335661-95.patch, failed testing.

The last submitted patch, 94: outbound_processors_cacheability-2335661-94.patch, failed testing.

Wim Leers’s picture

Failures because:

  • in #94, I fixed UrlGeneratorInterface to have the necessary updates, but I forgot to update a test, thus causing a fatal.
  • in #95, the first hunk is a necessary bugfix, restoring that.
Wim Leers’s picture

Status: Needs work » Needs review

Such n00b.

Wim Leers’s picture

I re-read the entire issue, to correctly answer #88.


#23:

See also #2417793-46: Allow entity: URIs to be entered in link fields, where changes to URL aliases don't result in a cache clear of rendered entities containing formatted links. Maybe that needs its own issue, but seems highly related to this, so just noting it here for now.

This is exactly what I feared was the case. Note that when changing the URL alias of a node, the node updates a MenuLinkContent entity. Which means everything is updated correctly. It is only when updating URL aliases via /admin/config/search/path, that no invalidation happens. But, to be honest, that's really a problem independent of this issue: URL aliases don't track changes and hence also not invalidations in any way. This issue is about fixing the more general problem, that sits a level higher: to make it possible for those outbound path/route processors that do have cacheability metadata, to be able to associate that cacheability with generated URLs.
So we should fix URL aliases in a separate issue. #2480077: Using the URL alias UI to change aliases doesn't do necessary invalidations: path aliases don't have cache tags

#88:

I'm confused, given that the patch doesn't add test coverage for the various changes in the route processors out there. Don't we want to check whether the cache contexts are adding for those ones? Isn't that the entire point of the issue, so why should we not expand the test coverage.

The problem is that none of those outbound path/route processors, with the sole exception of the CSRF token route processor, already has test coverage. Creating test coverage for all these seems out of scope here. Providing integration test coverage to show that this works from the lowest level (i.e. the path/route processors adding cacheability metadata) up to the highest level (i.e. the aforementioned cacheability metadata is present on rendered menus that contain links using path/route processors) seems sufficient to prove that this issue is working correctly?

Anyone mind to have a look at #84? I still don't get why we can't use a similar approach to the access result object one.

To properly answer this, I re-read the entire issue. Hence also my reply to #23 above, which wasn't addressed yet either. Apparently you've been saying this since #38, then again in #40, then in #84. Every single time, I didn't understand what you meant; I thought you meant that we shouldn't pass CacheableMetadata around, but instead return a value object. I was going to answer that that is what @pwolanin had tried until #40, but he changed direction because it'd have required too many changes.
But what you actually were trying to say was:

Why not use something like this?

   * @return bool|\Drupal\Core\Access\AccessResultInterface
   *   The access result. Returns a boolean if $return_as_object is FALSE (this
   *   is the default) and otherwise an AccessResultInterface object.
   *   When a boolean is returned, the result of AccessInterface::isAllowed() is
   *   returned, i.e. TRUE means access is explicitly allowed, FALSE means
   *   access is either explicitly forbidden or "no opinion".
   */
  public function access(AccountInterface $account, $return_as_object = FALSE);

And that is an excellent, excellent question! Trying that now :)

Sorry for all the confusion & frustration :(

Wim Leers’s picture

Having done #101, I was forced to look at all changes made by this patch. And noticed that several spots were missed in the prior patches.

And that … got me in the fun, sweet "D8 URL generation, WTF-is-this'-reactions" land.

IMHO the real cause of that is the absolutely horrendous(ly fragmented) system we have to generate URLs and links in Drupal 8. It doesn't make sense to have both UrlGenerator and Url::toString(), to have both #type => link and LinkGenerator. In all of these cases, there are far too many options. Url::toString() is about generating a string, and for strings, we shouldn't care about cacheability; we only care about cacheability of render arrays.
If this patch is frustrating, then >=80% of that frustration originates from that area. It's probably completely due to legacy reasons that were all reasonable along the way, but we're definitely going to be bitten by broken edge cases there during the D8 cycle.

</rant>

Anyway, this is what that approach would look like. Do you think this is better, @dawehner? I honestly don't care, because as per the above rant, it's already horrible anyway.

Status: Needs review » Needs work

The last submitted patch, 101: outbound_processors_cacheability-2335661-101.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
65.11 KB
1.01 KB

Forgot to update one small thing; AFAICT that's causing all these test failures.

Status: Needs review » Needs work

The last submitted patch, 103: outbound_processors_cacheability-2335661-103.patch, failed testing.

Wim Leers’s picture

Issue summary: View changes
Status: Needs work » Needs review
Issue tags: -sprint, -Needs issue summary update, -Needs tests
FileSize
106.37 KB
42.27 KB

Those last 7 failures I also discovered locally, while working on test coverage. Fixed it.

Also, while working on the test coverage, I noticed #2480233: Incorect docblocks in route processor tests and @xjm kindly fixed it.

This patch adds complete test coverage, including for path processors that didn't even have any explicit test coverage previously. As you can see in the interdiff, the test coverage adds another ~40 KB of patch weight: 12 files changed, 391 insertions(+), 62 deletions(-).

This took me ~12 hours to get sorted out! I could write a complementary rant (see #101) about URL generation test coverage, but I won't.

Wim Leers’s picture

Fabianx’s picture

CR looks great, missing \Drupal::l() and \Drupal::url() for now still (and other ways I might have not thought about) ...

Wim Leers’s picture

Fixed \Drupal::url() (99% of that is in tests and in hook_help()), and \Drupal::l(). CR updated.

dawehner’s picture

I don't think \Drupal:url() and \Drupal::l() has to support every feature ... given that they are convenience wrappers anyway.

Fabianx’s picture

#109: Especially those need to support those features, as else:

  $build['my_link']['#markup'] = \Drupal::l('Click here to run cron', 'system_cron');

While that is still horribly broken, the API should at least speak of the cacheability, though we need to harden that more ...

Status: Needs review » Needs work

The last submitted patch, 108: outbound_processors_cacheability-2335661-108.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
109.95 KB
979 bytes

Hah, this uncovered a small bug in \Drupal\views\Plugin\views\field\FieldPluginBase, which would've prevented Views' "more" links from having the views-more-link class :) That's what caused the two failures.

dawehner’s picture

Did a big review of the entire patch. Overall the patch looks really great.

+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -1249,7 +1249,7 @@ public function renderText($alter) {
-        $more_link = \Drupal::l($more_link_text, CoreUrl::fromUserInput('/' . $more_link_path), array('attributes' => array('class' => array('views-more-link'))));
+        $more_link = \Drupal::l($more_link_text, CoreUrl::fromUserInput('/' . $more_link_path, array('attributes' => array('class' => array('views-more-link')))));

Right that fix is indeed needed.

  1. +++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
    @@ -47,6 +46,10 @@ public function processOutbound($route_name, Route $route, array &$parameters) {
    +      if ($cacheable_metadata) {
    +        // Tokens are per user and per session, so not cacheable.
    +        $cacheable_metadata->setCacheMaxAge(0);
    +      }
    

    This is odd, don't we want to rather add the session / user cache contexts as well, or would that not be needed once we mark something as not cacheable?

  2. +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php
    @@ -107,10 +108,10 @@ public function addOutbound(OutboundPathProcessorInterface $processor, $priority
    +  public function processOutbound($path, &$options = array(), Request $request = NULL, CacheableMetadata $cacheable_metadata = NULL) {
    

    It would be great to explain in the issue summary why we return once and once we alter.

  3. +++ b/core/lib/Drupal/Core/Render/Element/Link.php
    @@ -75,9 +77,13 @@ public static function preRenderLink($element) {
    +      list($element['#markup'], $cacheability) = $link_generator->generate($element['#title'], $element['#url']->setOptions($options), TRUE);
    

    wow, I wasn't aware that you can write into variables.

  4. +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
    @@ -349,13 +354,19 @@ public function generateFromRoute($name, $parameters = array(), $options = array
    +    $cacheable_metadata = ($collect_cacheability_metadata) ? new CacheableMetadata() : NULL;
    

    Why do we use () here? its a simple boolean check, isn't it?

  5. +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
    @@ -449,15 +461,20 @@ public function generateFromPath($path = NULL, $options = array()) {
    +    if ($options['absolute'] && $cacheable_metadata) {
    +      $cacheable_metadata->addCacheContexts(['url.host']);
    +    }
    

    Theoretically the base path could change, is that included inside url.host?

  6. +++ b/core/modules/language/src/HttpKernel/PathProcessorLanguage.php
    index a1547c0..9e5489c 100644
    --- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php
    
    +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php
    @@ -114,6 +115,9 @@ public function processOutbound($path, &$options = array(), Request $request = N
    +        if ($cacheable_metadata) {
    +          $cacheable_metadata->addCacheContexts(['languages:' . LanguageInterface::TYPE_URL]);
    +        }
    

    It uses session information so its not just ::TYPE_URL, isn't it?

  7. +++ b/core/modules/language/tests/src/Unit/LanguageNegotiationUrlTest.php
    @@ -54,14 +56,124 @@ protected function setUp() {
    +    $config_object = $this->getMockBuilder('Drupal\Core\Config\Config')
    +      ->disableOriginalConstructor()
    +      ->getMock();
    +    $config_object->expects($this->any())
    +      ->method('get')
    +      ->with('url')
    +      ->will($this->returnValue($config_data));
    ...
    +    $config = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface');
    +    $config->expects($this->any())
    +      ->method('get')
    +      ->with('language.negotiation')
    +      ->will($this->returnValue($config_object));
    

    You can use \Drupal\Tests\UnitTestCase::getConfigFactoryStub and write that much nicer

  8. +++ b/core/modules/language/tests/src/Unit/LanguageNegotiationUrlTest.php
    @@ -154,3 +275,13 @@ public function providerTestDomain() {
    +namespace {
    +  if (!function_exists('base_path')) {
    +    function base_path() {
    +      return '/';
    +    }
    +  }
    +}
    

    Do you mind opening up a follow up to replace that usage of base_path()? It would be ne of the last ones ...

Wim Leers’s picture

  1. catch asked this in #73, see #74 + #77 for the answer. In other words: yes, that'd be ideal, and Fabianx has been proposing to add a "CSRF token" cache context. But in any case: that can easily be done in a follow-up. In the reality of HEAD, max-age = 0 is appropriate.
  2. Good point. Done.
  3. Heh :)
  4. No reason; that's my preferred code style, and therefore it's still in my fingers. Changed that to your liking — I don't care.
  5. Hah, I was thinking about that too! This only captures \Symfony\Component\HttpFoundation\Request::getSchemeAndHttpHost(), so, no, base path is not included. Fixed that. Renamed the cache context to SiteCacheContext, and the cache context ID to url.site.
  6. Well, this code is definitely very tricky. For authenticated users, no outbound processing is ever done. For the anonymous user, we only do outbound processing if the current request actually has the configured query parameter. So, in other words: this was plain wrong. It is nonsensical to say that the output varied by the URL language, because this enabled other code to depend on the URL language. But this code itself actually depended on the configured query parameter, for anonymous users. i.e. it ensures that if anonymous users can specify "mysite.com/blog?lang=de", that all generated links on that page would also continue to have that "?lang=de" query parameter.
  7. That was just copy/pasted from the other test method on that class, but sure, just updated both to use that. Much nicer indeed :)
  8. Done: #2481833: Remove LanguageNegotiationUrl's usage of base_path().
Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Cache/SiteCacheContext.php
    @@ -2,32 +2,34 @@
    - * A "host" is defined as the combination of URI scheme, domain name and port.
    + * A "host" is defined as the combination of URI scheme, domain name, port and
    + * base path.
    

    shouldn't this say "site" now?

  2. +++ b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
    @@ -152,7 +160,8 @@ protected function buildLocalUrl($uri, array $options = []) {
    +    $url = $base . $options['script'] . $uri . $query . $options['fragment'];
    +    return $collect_cacheability_metadata ? [$url, $cacheable_metadata] : $url;
    

    Different return structure based on an argument is something we've tried to get rid off for a long time :(

    I didn't read most of the comments and this probably has been discussed already. Just want to through out an idea: Pass the cacheability metadata out as a by-reference argument.

Wim Leers’s picture

  1. Good catch, fixed.
  2. HAH! See the #101 interdiff — until that patch, that's exactly what this was doing. @dawehner felt it'd be much better to go this way. See #100. This approach is consistent with how AccessibleInterface::access() works (the $return_as_object = FALSE thing).
    I don't have a preference. So it's up to you & dawehner.
dawehner’s picture

Different return structure based on an argument is something we've tried to get rid off for a long time :(

Well, but I think altering some random variable is worse to do. I'm a huge fan of separating methods which return something from methods which change state somehow.
If you do both, its something you really don't expect as an API, if you ask me.

znerol’s picture

I'm with @Berdir, varying the return type based on a parameter is a terrible code smell. It should be possible to circumvent this problem by introducing a helper method. I.e. one that calls into the proper method which returns always both (url and metadata) and just throws away the metadata and only returns the url. Martin Fowler on flag arguments.

Wim Leers’s picture

#119: But that's a bigger API change.

Fabianx’s picture

  1. +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
    @@ -449,15 +461,20 @@ public function generateFromPath($path = NULL, $options = array()) {
         $base = $options['absolute'] ? $options['base_url'] : $current_base_path;
         $prefix = empty($path) ? rtrim($options['prefix'], '/') : $options['prefix'];
     
    +    if ($options['absolute'] && $cacheable_metadata) {
    +      $cacheable_metadata->addCacheContexts(['url.site']);
    +    }
    

    I think we need to always add a 'url.base' cache context here?

    Or maybe multi-site should not share caches and its not needed?

    In any case: a possible follow-up

  2. +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php
    @@ -180,6 +184,9 @@ public function processOutbound($path, &$options = array(), Request $request = N
    +        if ($cacheable_metadata) {
    +          $cacheable_metadata->addCacheContexts(['languages:' . LanguageInterface::TYPE_URL, 'url.site']);
    +        }
    

    So url.site now contains also the language prefix?

2 questions, but RTBC, except that the patch for #117 typo is missing.

Wim Leers’s picture

Sorry, forgot to upload the patch accompanying #117. Here it is.

Wim Leers’s picture

#121: Thanks for the review!

  1. Hm… looks like you're right. That makes sense, actually. So then I need to add a new cache context. What about url.site.basepath?
  2. No, url.site = host + port + base path, i.e. the value for the url.site cache context is calculated without path processing.
larowlan’s picture

Consequently, \Drupal\Core\Routing\UrlGeneratorInterface::generateFrom(Route|Path)() also have a new optional parameter: $collect_cacheability_metadata = FALSE. The default value of FALSE means no cacheability metadata is returned. When it is set to TRUE, an array is returned, with the first value being the generated URL (i.e. the same value that would be returned if it'd be set to FALSE), and the second value being the cacheability metadata for the URL. So: [$url, $cacheability].

That feels wrong. We had a similar issue with varying returns in Form caching and we went with a new value object.
Magic returns is a code smell in my books.
Can we not have a new value object CacheableUrl or CacheableLink which have {getUrl|getLink}/getCacheabilityData methods?

larowlan’s picture

For the issue I'm referring to see #2242749: Port Form API security fix SA-CORE-2014-002 to Drupal 8 and the resultant class 'Drupal\system\FileAjaxForm' which has comment :
Wrapper for Ajax forms data and commands, avoiding a multi-return-value tuple.

larowlan’s picture

Also sorry to come in at comment 124 and possibly derail stuff, but link and url generation are massive touch-points for developers so we need a decent DX.

larowlan’s picture

oh after reading back over comments, seems I'm not the only one, phew

larowlan’s picture

Any reason we can't have generateFromPathWithCache, generateFromRouteWithCache etc?

larowlan’s picture

or the converse generateWithout if we want to discourage the without caching - although major API break
That will be my last comment, sorry for the spam

larowlan’s picture

last comment, promise - I see you discussed this at 100/101 but bool|AccessResult isn't the same thing as string|[string, array]

Wim Leers’s picture

#130: I'm fine with string|CacheableUrl. That's basically what we had in #40. But back then, we were always returning CacheableUrl, and that is what was infeasible. (EDIT: And I agree that that is better, and more similar to AccessibleInterface::access([…], $return_as_object = FALSE).)

Would we all be fine with that?

Fabianx’s picture

#131: I am fine with returning a value object instead of a string, then in 9.x deprecating the string all together.

The filters also return a FilerProcessResult, which was a DX I very much liked.

dawehner’s picture

Issue tags: +needs profiling

Let's ensure how much this changes the performance on an ordinary site like admin/index.

Wim Leers’s picture

Status: Needs review » Needs work
14:05:10 dawehner: WimLeers: if you implement string|array now and you should be pretty much possible to implement an object with BC layer later
14:05:33 WimLeers: dawehner: but why not do it the other way around then? Won't that be more elegant in the future?
14:05:36 WimLeers: Fabianx-screen: ^^
14:07:05 dawehner: WimLeers: well, the other way round IMHO doesn't make sense as we already have the object
14:07:18 dawehner: i was proposing that in order to move forward on that issue
14:07:30 WimLeers: hehe I know :)
14:07:39 WimLeers: And I appreciate that!
14:08:00 dawehner: WimLeers: yeah no idea about #31 to be honest
14:08:03 WimLeers: But this will be seen by almost every Drupal developer, so if an object is better, then I'm totally fine with rerolling it
14:08:22 dawehner: WimLeers: i thought that return as string will be still the default?
14:08:46 WimLeers: yes, it will be
14:09:04 WimLeers: dawehner: but returning an object would be consistent with how AccessResults are done
14:09:31 WimLeers: dawehner: and it'd shows the right pattern for contrib developers: value objects
14:09:34 WimLeers: not multiple return values
14:09:52 WimLeers: All across core we use value objects in favor of multiple return values AFAIK?
14:11:11 dawehner: yeah right
14:11:14 dawehner: so let's go with an object

So we'll go with #130/#131.

Wim Leers’s picture

#133:

(Refreshed until the number of function calls stabilized.)

/admin/index

HEAD
241,178 function calls (yes, 241K!)
Patch
242,586 function calls
Conclusion
1408 additional function calls, or 0.58% more.

/node/1

HEAD
90,146 function calls (90K)
Patch
91,540 function calls
Conclusion
1394 additional function calls, or 1.5% more.

However, this is being done to allow for #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees, which provides much bigger gains than what we regress here.

Wim Leers’s picture

Issue summary: View changes
Issue tags: -needs profiling
Wim Leers’s picture

Status: Needs work » Needs review
FileSize
118.48 KB
37.58 KB

Let's see how well this fares. Did #130/#134.

All unit tests pass, clicking around the UI works. Let's see if I missed anything.

dawehner’s picture

1394 additional function calls, or 1.5% more.

Wow, that is not nothing, where is this primarily coming from?

Wim Leers’s picture

FileSize
339.3 KB

I did another round of profiling to answer #138, and to my surprise… the numbers are vastly different now. And reproducibly so.

HEAD (e0aae8c26ae3367b96d2e49a8beb05a441a9f8fc) still has the same number of function calls as in #135, thus indicating I didn't test incorrectly.

But the number of function calls with patch #137 is not 90,164 at /node/1 (as was tested in #135 with patch #117), but … 87,435. Or a reduction of 2729 function calls, which means 3.0% fewer function calls with this patch!

I don't get it either. But I can consistently reproduce this with the patch in #137. The main reasons: 226 fewer function calls (than HEAD!) for Cache::mergeContexts(), 213 fewer for Cache::mergeTags(), which then adds up to ~3K less function calls because of the logic in those two methods. Which makes it unsurprising that #2408013: Adding Assertions to Drupal - Test Tools. brings such a nice performance boost.

It'd be great if somebody could repeat the profiling to confirm my numbers.

Wim Leers’s picture

+++ b/core/lib/Drupal/Core/Cache/CacheableMetadata.php
@@ -140,8 +140,8 @@ public function setCacheMaxAge($max_age) {
-    $result->contexts = Cache::mergeContexts($this->contexts, $other->contexts);
-    $result->tags = Cache::mergeTags($this->tags, $other->tags);
+    $result->contexts = empty($this->contexts) ? $other->contexts : Cache::mergeContexts($this->contexts, $other->contexts);
+    $result->tags = empty($this->tags) ? $other->tags : Cache::mergeTags($this->tags, $other->tags);

This is why I thought I was going insane.

Many thanks to @dawehner for pointing that out and helping me recover my sanity.

Opening #2483433: Optimize CacheableMetadata::merge() + BubbleableMetadata::merge() to do that first, since it presents such a significant performance boost.


YAY, green!

Wim Leers’s picture

So, ignore #139.

Profiling /node/1 again, with #137:

HEAD
90,164 function calls
HEAD + #2483433-1: Optimize CacheableMetadata::merge() + BubbleableMetadata::merge()
(which now is responsible for landing the code cited in #140, which was the reason why #139 showed those confusing numbers)
86,298 function calls
HEAD + #2483433-1: Optimize CacheableMetadata::merge() + BubbleableMetadata::merge() + #137
87,435 function calls

That's 1137 additional function calls. The difference indeed lies in the collecting and merging of cacheability metadata.

But:

  1. that can be optimized, which is what we're doing in #2483433: Optimize CacheableMetadata::merge() + BubbleableMetadata::merge() — the latest patch there shaves another few thousand function calls off of /node/1, and would cause that 1137 additional function calls to drop to <1000 additional function calls
  2. we always knew there would be *some* cost to this; #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees needs it, and is able to save many more function calls
  3. finally: with #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees applied, the delta in number of function calls for "regular links" should be zero, because we only do the extra work for links that indicate they want to collect cacheability metadata
dawehner’s picture

finally: with #1805054: [PP-1] Cache localized, access filtered, URL resolved, (and rendered?) menu trees applied, the delta in number of function calls for "regular links" should be zero, because we only do the extra work for links that indicate they want to collect cacheability metadata

Well, I think we should have a close look at what costs these additional function calls, ... we can not just look at entirely warm requests.

larowlan’s picture

+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -277,7 +278,11 @@ public function generate($name, $parameters = array(), $absolute = FALSE) {
+      $generated_url = new GeneratedUrl();
+      $cacheable_metadata = new CacheableMetadata();

@@ -293,6 +298,10 @@ public function generateFromRoute($name, $parameters = array(), $options = array
+      $generated_url = $generated_url->merge($cacheable_metadata);

Can we avoid this merge if we instead work the other way? Given GeneratedUrl extends from CacheableMetadata could we not just do

$cacheable_metadata = new GeneratedUrl();
// ...
$cacheable_metadata->setGeneratedUrl($url);
// ...

That would save some merges right? Or am I missing extra context not in the diff.

larowlan’s picture

rolling a patch to illustrate 143

larowlan’s picture

FileSize
5.63 KB
115.29 KB

This passes all unit tests and we lose the ::merge calls

Wim Leers’s picture

FileSize
118.19 KB
4.48 KB

#143: glad you saw that :) I considered that too, but figured it'd be more important to keep the number of changes as low as possible in a first iteration :) Let's do it now!

Wim Leers’s picture

Cross-posted :) I think mine is more clear (because I remove $cacheable_metadata, rather than renaming $generated_url to $cacheable_metadata), and you forgot to update UnroutedUrlAssembler. However, you also did find one more possible optimization than I did: the one in LinkGenerator. Fixing that too now.

Wim Leers’s picture

Did #147.

Also, thanks to @larowlan/#145/#146/#148, this no longer uses any CacheableMetadata::merge() calls, thus allowing me to revert the changes cited in #140. That's now entirely in the domain of #2483433: Optimize CacheableMetadata::merge() + BubbleableMetadata::merge(), meaning that this is no longer blocked on that issue!

Wim Leers’s picture

Issue summary: View changes

Updated IS for #134/#137–#148. Also updated CR.

The last submitted patch, 146: outbound_processors_cacheability-2335661-144.patch, failed testing.

larowlan’s picture

+1 to new changes, this looks great to me

Wim Leers’s picture

Well, I think we should have a close look at what costs these additional function calls, ... we can not just look at entirely warm requests.

Another round of profiling to answer this.

/node/1/

HEAD
90,164 function calls
Patch #148.
91,720 function calls

+1556 function calls

/node/1/ + #2483433: Optimize CacheableMetadata::merge() + BubbleableMetadata::merge()

HEAD + #2483433: Optimize CacheableMetadata::merge() + BubbleableMetadata::merge()
82,961 function calls
Patch #148 + #2483433: Optimize CacheableMetadata::merge() + BubbleableMetadata::merge()
83,538 function calls

+557 function calls


And yes, when looking at it with XHProf, we can clearly see it's completely due to the cacheability metadata (this is with HEAD, without #2483433: Optimize CacheableMetadata::merge() + BubbleableMetadata::merge()):

Function Name Calls Diff Calls
Diff%
Incl. Wall
Diff
(microsec)
IWall
Diff%
Excl. Wall
Diff
(microsec)
EWall
Diff%
Incl.
MemUse
Diff
(bytes)
IMemUse
Diff%
Excl.
MemUse
Diff
(bytes)
EMemUse
Diff%
Incl.
PeakMemUse
Diff
(bytes)
IPeakMemUse
Diff%
Excl.
PeakMemUse
Diff
(bytes)
EPeakMemUse
Diff%
array_merge 148 9.5% 16 0.9% 16 0.9% 12,992 44.3% 12,992 44.3% 0 0.0% 0 0.0%
func_get_args 135 8.7% -17 -1.0% -17 -1.0% 79,008 269.2% 79,008 269.2% 544 1.8% 544 1.8%
Drupal::service 74 4.8% -422 -23.9% 79 4.5% -1,264 -4.3% 664 2.3% -13,176 -43.9% 0 0.0%
sort 74 4.8% -3 -0.2% -3 -0.2% 0 0.0% 0 0.0% 0 0.0% 0 0.0%
array_unique 74 4.8% -16 -0.9% -16 -0.9% 6,512 22.2% 6,512 22.2% 0 0.0% 0 0.0%
Drupal\Core\Cache\Cache::Drupal\Core\Cache\{closure} 74 4.8% 2 0.1% 2 0.1% 3,552 12.1% 3,552 12.1% 0 0.0% 0 0.0%
Symfony\Component\DependencyInjection\Container::get 61 3.9% 2,087 118.4% -145 -8.2% -1,856 -6.3% 344 1.2% -12,280 -40.9% 232 0.8%
Drupal::getContainer 61 3.9% -6 -0.3% -6 -0.3% 0 0.0% 0 0.0% 632 2.1% 632 2.1%
Drupal\Core\Cache\Cache::mergeTags 37 2.4% 154 8.7% 179 10.2% 5,032 17.1% -27,520 -93.8% -8 -0.0% 0 0.0%
Drupal\Core\Cache\Cache::validateTags 37 2.4% -5 -0.3% -14 -0.8% 0 0.0% 0 0.0% -8 -0.0% -8 -0.0%
Drupal\Core\Cache\Cache::mergeMaxAges 37 2.4% 132 7.5% 91 5.2% 1,776 6.1% -18,944 -64.6% 0 0.0% 0 0.0%
Drupal\Core\Cache\CacheContextsManager::validateTokens 37 2.4% -6 -0.3% -21 -1.2% -16 -0.1% 0 0.0% 88 0.3% 0 0.0%
array_filter 37 2.4% 31 1.8% 35 2.0% 4,440 15.1% 888 3.0% 40 0.1% 0 0.0%
Drupal\Core\Cache\Cache::mergeContexts 37 2.4% 299 17.0% 200 11.3% 5,016 17.1% -27,528 -93.8% 88 0.3% 0 0.0%
in_array 37 2.4% 11 0.6% 11 0.6% -32 -0.1% -32 -0.1% 0 0.0% 0 0.0%
Drupal\Core\Cache\CacheableMetadata::merge 37 2.4% 640 36.3% 86 4.9% 17,984 61.3% 6,144 20.9% 0 0.0% 0 0.0%
Drupal\Component\Utility\SafeMarkup::set 32 2.1% 3 0.2% 3 0.2% -58,240 -198.5% -58,240 -198.5% -79,160 -263.9% -79,160 -263.9%
Drupal\Core\Cache\CacheableMetadata::applyTo 29 1.9% 17 1.0% 17 1.0% 7,912 27.0% 7,912 27.0% 0 0.0% 0 0.0%
Drupal\Core\Cache\CacheableMetadata::createFromRenderArray 29 1.9% 52 2.9% 52 2.9% 9,728 33.2% 9,728 33.2% 0 0.0% 0 0.0%
Drupal\Component\Utility\NestedArray::mergeDeep 24 1.5% 41 2.3% 53 3.0% 5,640 19.2% -17,152 -58.5% 1,872 6.2% 128 0.4%
Drupal\Component\Utility\NestedArray::mergeDeepArray 24 1.5% -3 -0.2% -15 -0.9% 5,632 19.2% 5,648 19.2% 1,192 4.0% 1,192 4.0%
SplDoublyLinkedList::push 24 1.5% 1 0.1% 1 0.1% 1,152 3.9% 1,152 3.9% 48 0.2% 48 0.2%
SplDoublyLinkedList::pop 24 1.5% 11 0.6% 11 0.6% -2,304 -7.9% -2,304 -7.9% 0 0.0% 0 0.0%
Drupal\Core\GeneratedUrl::setGeneratedUrl 21 1.3% 0 0.0% 0 0.0% 688 2.3% 688 2.3% 0 0.0% 0 0.0%
Drupal\Core\GeneratedLink::setGeneratedLink 21 1.3% 0 0.0% 0 0.0% 688 2.3% 688 2.3% 0 0.0% 0 0.0%
Drupal\Core\Cache\CacheableMetadata::getCacheTags 21 1.3% 0 0.0% 0 0.0% 8 0.0% 8 0.0% -424 -1.4% -424 -1.4%
Drupal\Core\Cache\CacheableMetadata::createFromObject 21 1.3% 69 3.9% 67 3.8% 3,816 13.0% 3,816 13.0% -1,560 -5.2% -288 -1.0%
Drupal\Core\Cache\CacheableMetadata::getCacheContexts 21 1.3% 0 0.0% 0 0.0% 56 0.2% 56 0.2% -424 -1.4% -424 -1.4%
Drupal\Core\GeneratedLink::getGeneratedLink 21 1.3% 0 0.0% 0 0.0% 688 2.3% 688 2.3% 0 0.0% 0 0.0%
Drupal\Core\Cache\CacheableMetadata::getCacheMaxAge 21 1.3% 0 0.0% 0 0.0% 0 0.0% 0 0.0% -424 -1.4% -424 -1.4%
Drupal\Core\GeneratedUrl::getGeneratedUrl 21 1.3% 0 0.0% 0 0.0% 680 2.3% 680 2.3% 0 0.0% 0 0.0%
is_integer 16 1.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0%
Drupal\Core\Render\BubbleableMetadata::merge 16 1.0% 165 9.4% 45 2.6% 11,896 40.5% 0 0.0% 0 0.0% 0 0.0%
array_merge_recursive 16 1.0% -3 -0.2% -3 -0.2% 1,408 4.8% 1,408 4.8% 0 0.0% 0 0.0%
Drupal\Core\Render\Renderer::mergeAttachments 16 1.0% 7 0.4% 5 0.3% 2,168 7.4% 768 2.6% 0 0.0% 0 0.0%
Drupal::l -13 -0.8% -3,121 -177.0% -44 -2.5% -110,416 -376.3% -992 -3.4% -302,152 -1007.2% 576 1.9%
SplDoublyLinkedList::count 8 0.5% 0 0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0%
Drupal\Core\Render\Renderer::updateStack 8 0.5% 105 6.0% 31 1.8% -3,504 -11.9% -872 -3.0% -200 -0.7% 0 0.0%
Drupal\Core\Render\Renderer::bubbleStack 8 0.5% 165 9.4% 32 1.8% -1,008 -3.4% -5,808 -19.8% 0 0.0% 0 0.0%
count 8 0.5% 4 0.2% 9 0.5% -16 -0.1% -16 -0.1% 0 0.0% 0 0.0%
Drupal\Core\Render\Renderer::doRender@6 8 0.5% 4,789 271.6% 196 11.1% 93,784 319.6% -7,520 -25.6% 163,936 546.5% 1,616 5.4%
Drupal\Core\Url::getOptions 8 0.5% 0 0.0% 0 0.0% 0 0.0% 0 0.0% 624 2.1% 624 2.1%
Drupal\Core\Render\BubbleableMetadata::applyTo 8 0.5% 25 1.4% 24 1.4% -7,648 -26.1% -2,176 -7.4% -200 -0.7% -200 -0.7%
is_string 8 0.5% 39 2.2% 39 2.2% 0 0.0% 0 0.0% 0 0.0% 0 0.0%
Drupal\Core\Render\ElementInfoManager::getInfo 8 0.5% 2 0.1% 55 3.1% 5,032 17.1% 5,056 17.2% 1,168 3.9% 1,088 3.6%
is_object 8 0.5% -12 -0.7% -12 -0.7% 0 0.0% 0 0.0% 0 0.0% 0 0.0%
Drupal\Component\Utility\SafeMarkup::isSafe -8 -0.5% -11 -0.6% -11 -0.6% -384 -1.3% -384 -1.3% 0 0.0% 0 0.0%
Drupal\Core\Render\BubbleableMetadata::createFromRenderArray 8 0.5% 26 1.5% -3 -0.2% -552 -1.9% 0 0.0% 0 0.0% 0 0.0%
Drupal\Core\Url::setOptions 8 0.5% 0 0.0% 0 0.0% 7,168 24.4% 7,168 24.4% 0 0.0% 0 0.0%
Drupal\Core\Render\Element\Link::preRenderLink 8 0.5% 4,887 277.2% 199 11.3% 140,976 480.4% -19,176 -65.3% 196,248 654.2% 1,040 3.5%

But, with #2483433: Optimize CacheableMetadata::merge() + BubbleableMetadata::merge() applied, that picture becomes quite different, as the much lower increase in number of function calls suggested:

Function Name Calls Diff Calls
Diff%
Incl. Wall
Diff
(microsec)
IWall
Diff%
Excl. Wall
Diff
(microsec)
EWall
Diff%
Incl.
MemUse
Diff
(bytes)
IMemUse
Diff%
Excl.
MemUse
Diff
(bytes)
EMemUse
Diff%
Incl.
PeakMemUse
Diff
(bytes)
IPeakMemUse
Diff%
Excl.
PeakMemUse
Diff
(bytes)
EPeakMemUse
Diff%
Drupal\Core\Cache\CacheableMetadata::merge 37 6.4% 155 0.5% 22 0.1% 6,144 21.1% 6,144 21.1% 0 0.0% 0 0.0%
Drupal\Component\Utility\SafeMarkup::set 32 5.5% -11 -0.0% -11 -0.0% -58,256 -199.6% -58,256 -199.6% -82,488 -272.5% -82,488 -272.5%
Drupal\Core\Cache\CacheableMetadata::createFromRenderArray 29 5.0% 51 0.2% 51 0.2% 9,728 33.3% 9,728 33.3% 0 0.0% 0 0.0%
Drupal\Core\Cache\CacheableMetadata::applyTo 29 5.0% 21 0.1% 21 0.1% 7,912 27.1% 7,912 27.1% 0 0.0% 0 0.0%
SplDoublyLinkedList::push 24 4.2% 1 0.0% 1 0.0% 1,152 3.9% 1,152 3.9% 48 0.2% 48 0.2%
SplDoublyLinkedList::pop 24 4.2% 0 0.0% 0 0.0% -2,304 -7.9% -2,304 -7.9% 0 0.0% 0 0.0%
Drupal\Core\Cache\CacheableMetadata::getCacheMaxAge 21 3.6% 0 0.0% 0 0.0% 0 0.0% 0 0.0% -424 -1.4% -424 -1.4%
Drupal\Core\GeneratedLink::setGeneratedLink 21 3.6% 1 0.0% 1 0.0% 688 2.4% 688 2.4% 0 0.0% 0 0.0%
Drupal\Core\Cache\CacheableMetadata::getCacheTags 21 3.6% 0 0.0% 0 0.0% 40 0.1% 40 0.1% -424 -1.4% -424 -1.4%
Drupal\Core\Cache\CacheableMetadata::getCacheContexts 21 3.6% 0 0.0% 0 0.0% 0 0.0% 0 0.0% -424 -1.4% -424 -1.4%
Drupal\Core\GeneratedUrl::setGeneratedUrl 21 3.6% 0 0.0% 0 0.0% 688 2.4% 688 2.4% 0 0.0% 0 0.0%
Drupal\Core\GeneratedUrl::getGeneratedUrl 21 3.6% 0 0.0% 0 0.0% 680 2.3% 680 2.3% 0 0.0% 0 0.0%
Drupal\Core\GeneratedLink::getGeneratedLink 21 3.6% 0 0.0% 0 0.0% 688 2.4% 688 2.4% 0 0.0% 0 0.0%
Drupal::service 21 3.6% 35 0.1% 63 0.2% -1,272 -4.4% 664 2.3% -13,728 -45.3% 0 0.0%
Drupal\Core\Cache\CacheableMetadata::createFromObject 21 3.6% 81 0.3% 78 0.3% 3,816 13.1% 3,816 13.1% -1,560 -5.2% -288 -1.0%
is_integer 16 2.8% 2 0.0% 2 0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0%
Drupal\Core\Render\BubbleableMetadata::merge 16 2.8% 338 1.1% 88 0.3% 2,456 8.4% 0 0.0% -112 -0.4% -112 -0.4%
Drupal::l -13 -2.3% -3,280 -11.0% -54 -0.2% -110,416 -378.3% -992 -3.4% -294,192 -971.8% 576 1.9%
Drupal\Core\Render\ElementInfoManager::getInfo 8 1.4% 13 0.0% 38 0.1% 5,088 17.4% 5,088 17.4% 1,088 3.6% 1,088 3.6%
Drupal\Core\Render\BubbleableMetadata::applyTo 8 1.4% 18 0.1% 11 0.0% -7,648 -26.2% -2,176 -7.5% 0 0.0% 0 0.0%
SplDoublyLinkedList::count 8 1.4% 0 0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0%
Drupal\Core\Render\Renderer::bubbleStack 8 1.4% 193 0.6% 98 0.3% -1,600 -5.5% -1,664 -5.7% -112 -0.4% 0 0.0%
Drupal\Core\Render\Element\Link::preRenderLink 8 1.4% 5,444 18.2% 243 0.8% 141,088 483.4% -12,376 -42.4% 196,808 650.1% 1,040 3.4%
Symfony\Component\DependencyInjection\Container::get 8 1.4% 4,239 14.2% 149 0.5% -2,024 -6.9% 320 1.1% -13,472 -44.5% 88 0.3%
count 8 1.4% -1 -0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0%
is_string 8 1.4% 3 0.0% 3 0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0%
is_object 8 1.4% 0 0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0% 0 0.0%
Drupal\Core\Render\BubbleableMetadata::createFromRenderArray 8 1.4% 31 0.1% 8 0.0% -552 -1.9% 0 0.0% 0 0.0% 0 0.0%
Drupal\Component\Utility\SafeMarkup::isSafe -8 -1.4% 5 0.0% 5 0.0% -384 -1.3% -384 -1.3% 0 0.0% 0 0.0%
Drupal\Core\Render\Renderer::updateStack 8 1.4% 247 0.8% 54 0.2% -7,648 -26.2% -280 -1.0% 0 0.0% 0 0.0%
call_user_func@1 8 1.4% 5,234 17.5% 24 0.1% 97,352 333.6% 272 0.9% 156,848 518.1% 96 0.3%
func_get_args 8 1.4% 6 0.0% 6 0.0% 7,272 24.9% 7,272 24.9% 568 1.9% 568 1.9%
Drupal\Component\Utility\NestedArray::mergeDeepArray 8 1.4% 130 0.4% 52 0.2% 3,488 12.0% 3,488 12.0% 1,104 3.6% 1,104 3.6%
Drupal\Component\Utility\NestedArray::mergeDeep 8 1.4% 89 0.3% 1 0.0% 3,480 11.9% -7,272 -24.9% 1,800 5.9% 128 0.4%
Drupal\Core\Url::setOptions 8 1.4% 0 0.0% 0 0.0% 7,168 24.6% 7,168 24.6% 0 0.0% 0 0.0%
Drupal\Core\Render\Element::children 8 1.4% 210 0.7% 192 0.6% 1,040 3.6% 360 1.2% 0 0.0% 0 0.0%
Drupal::getContainer 8 1.4% 27 0.1% 27 0.1% 0 0.0% 0 0.0% 632 2.1% 632 2.1%
Drupal\Core\Render\Renderer::render@2 8 1.4% 5,762 19.3% 11 0.0% 129,968 445.3% 288 1.0% 204,128 674.3% 48 0.2%
Drupal\Core\Url::getOptions 8 1.4% 0 0.0% 0 0.0% 24 0.1% 24 0.1% 648 2.1% 648 2.1%
Wim Leers’s picture

FileSize
679.52 KB

Forgot to include the xhprof runs.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Cache/SiteCacheContext.php
    @@ -0,0 +1,36 @@
    + * @file
    + * Contains \Drupal\Core\Cache\SiteCacheContext.
    + */
    

    OH holy moly, this should really be in a subnamespace, given how many we have. Is that something which is still doable? \Drupal\Core\Cache\Contexts maybe?

  2. +++ b/core/lib/Drupal/Core/Cache/SiteCacheContext.php
    @@ -0,0 +1,36 @@
    +    $request = $this->requestStack->getCurrentRequest();
    +    return $request->getSchemeAndHttpHost() . $request->getBaseUrl();
    

    You could achieve the exact same thing with using the request context and \Drupal\Core\Routing\RequestContext::getCompleteBaseUrl

  3. +++ b/core/lib/Drupal/Core/GeneratedLink.php
    @@ -0,0 +1,46 @@
    +/**
    + * Used to return generated links, along with associated cacheability metadata.
    + */
    +class GeneratedLink extends CacheableMetadata {
    

    We should make it clear in the documentation when to use Url/Link and when to use GeneratedLink/GeneratedUrl

  4. +++ b/core/lib/Drupal/Core/GeneratedLink.php
    @@ -0,0 +1,46 @@
    +  public function setGeneratedLink($generated_link) {
    

    I'd kind of expected that you could pass the link also as part of the constructor

  5. +++ b/core/lib/Drupal/Core/GeneratedUrl.php
    @@ -0,0 +1,46 @@
    +class GeneratedUrl extends CacheableMetadata {
    

    ... so for now those objects are not renderable inside twig, isn't that something we want?

  6. +++ b/core/lib/Drupal/Core/Template/TwigExtension.php
    @@ -201,8 +181,14 @@ public function getPath($name, $parameters = array(), $options = array()) {
    +    $build = ['#markup' => $generated_url->getGeneratedUrl()];
    +    $generated_url->applyTo($build);
    +    return $build;
    
    @@ -220,8 +206,14 @@ public function getUrl($name, $parameters = array(), $options = array()) {
    +    $build = ['#markup' => $generated_url->getGeneratedUrl()];
    +    $generated_url->applyTo($build);
    +    return $build;
    

    So yeah in case those objects would be printable, so for example implement (__)toString we would not have to do that custom handling here.

Status: Needs review » Needs work

The last submitted patch, 148: outbound_processors_cacheability-2335661-148.patch, failed testing.

The last submitted patch, 145: outbound-path-cache.145.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
117.54 KB
1.16 KB

Testbot terminated, twice, gaahhhh. Testbot--


#154:

  1. Sure, we can do that. The API is not the location of the class, but the cache context ID, which is determined by the service ID. So we can move the code without breaking anything. Filed an issue: #2483781: Move cache contexts classes from \Drupal\Core\Cache to \Drupal\Core\Cache\Context.
  2. I prefer this, because it is A) more explicit, B) it is then able to extend RequestStackCacheContextBase, which more clearly communicates where its data is coming from.
  3. Done.
  4. It was deemed bad for DX to have CacheableMetadata and BubbleableMetadata objects to be filled with values via the constructor, because the argument order is then painful to remember. That's why others asked me to remove constructor-based setting of values. For that reason, I don't want to add it here, that'd be out of scope.
  5. I could easily add toString() method if you want, but … see the next point.
  6. That's wrong. They would indeed be printable, but they should not be printed directly, they should be rendered by the Renderer, because that allows us to bubble cacheability metadata from links! We could make Twig detect if an object with toString() also implements CacheableDependencyInterface, and if so, bubble that cacheable metadata. But that is a quite important modification for Twig, which would make that out of scope for this issue AFAICT.
Fabianx’s picture

#157.6 I agree, if we want to support twig directly add a toRenderArray() method instead and have twig honor that.

Status: Needs review » Needs work

The last submitted patch, 157: outbound_processors_cacheability-2335661-157.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review

#157 failed due to testbot errors. Re-testing.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Things look pretty much alright for me.

  1. +++ b/core/lib/Drupal/Core/GeneratedLink.php
    @@ -0,0 +1,49 @@
    +  public function setGeneratedLink($generated_link) {
    +    $this->generatedLink = $generated_link;
    +    return $this;
    +  }
    
    +++ b/core/lib/Drupal/Core/GeneratedUrl.php
    @@ -0,0 +1,49 @@
    +   * Sets the generated URL.
    +   *
    +   * @param string $generated_url
    +   *   The generated URL.
    +   *
    +   * @return $this
    +   */
    +  public function setGeneratedUrl($generated_url) {
    +    $this->generatedUrl = $generated_url;
    +    return $this;
    +  }
    

    The nice advantage of having a constructor is that these objects could be immutable, at least for the link, because, seriously, they kinda should, right? But I see, implementation wise, it could be a little bit tricky, nevermind.

  2. +++ b/core/lib/Drupal/Core/Render/Element/Link.php
    @@ -75,9 +77,14 @@ public static function preRenderLink($element) {
    +      /** @var \Drupal\Core\Utility\LinkGenerator $link_generator */
    +      $link_generator = \Drupal::service('link_generator');
    

    Nitpick: the link generator has an interface ...

Wim Leers’s picture

Thanks!

  1. Indeed, would be tricky here implementation-wise.
  2. True, good catch — could be fixed upon commit, and otherwise can be a superminor follow-up.
catch’s picture

Status: Reviewed & tested by the community » Needs work

Generally looks fine. A couple of nits/things to clarify. Some of the link generation changes aren't very pretty but I don't see an alternative, and this is blocking the second oldest core critical, so not enough to hold up on.

  1. +++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php
    @@ -47,6 +46,10 @@ public function processOutbound($route_name, Route $route, array &$parameters) {
    +        // Tokens are per user and per session, so not cacheable.
    

    I think we should add a @todo here for either the csrf cache context or the placeholdering in #2351015: URL generation does not bubble cache contexts.

    If we CSRF-protected the logout link, since that's on every page, per-session caching is actually viable there (or any 'every page' menu link).

    Where caching gets horrible is CSRF-protected flag/quick operations links on entities.

    Either way better to have the @todo - this feels like something people could take at face value then either not CSRF-protect things 'for performance' or skip caching on things that might be.

  2. +++ b/core/lib/Drupal/Core/Cache/SiteCacheContext.php
    @@ -0,0 +1,36 @@
    + * Defines the SiteCacheContext service, for "per site" caching (in multisite).
    

    If you have a true multisite, then you'd never need this cache context since the databases will be different. This is really for when the site is available at multiple URLs - either domain module/language domains, but also visiting directly by IP address, with port specified etc. I think the same of the cache context is OK ('site') but it should not mention multisite and be clear it's about representations of the same site via different entry points.

  3. +++ b/core/lib/Drupal/Core/GeneratedLink.php
    @@ -0,0 +1,49 @@
    diff --git a/core/lib/Drupal/Core/GeneratedUrl.php b/core/lib/Drupal/Core/GeneratedUrl.php
    

    This is a bit sad, but it's a consequence of our horrible link generation API in 8.x and the fact we didn't fully move away from early rendering in either url()/l() (and t() has similar problems). Seems fine to work around for now and something that hopefully can be (gently) revisited in 9.x.

  4. +++ b/core/lib/Drupal/Core/Link.php
    @@ -121,9 +121,18 @@ public function setUrl(Url $url) {
    +  public function toString($collect_cacheability_metadata = FALSE) {
    +    return $this->getLinkGenerator()->generateFromLink($this, $collect_cacheability_metadata);
       }
     
    

    eek but again don't have better ideas and the discussion has already covered this.

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
117.36 KB
1.65 KB
  1. Done.
  2. Fixed.
  3. Indeed.
  4. Indeed.

Back to RTBC since this is only changing docs.

catch’s picture

+++ b/core/lib/Drupal/Core/Cache/SiteCacheContext.php
@@ -8,10 +8,12 @@
+ * base path. It allows for varying between the *same* being accessed via

same site? And should we give examples like http:// and http://www.?

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

nw for #166

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
117.54 KB
996 bytes

Done.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 168: outbound_processors_cacheability-2335661-168.patch, failed testing.

plach’s picture

Issue tags: +Needs reroll
Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs reroll
FileSize
116.84 KB

Rebased.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 7342df6 and pushed to 8.0.x. Thanks!

diff --git a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
index 528c2ad..551171b 100644
--- a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
+++ b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\Core\Routing;
 
-use Drupal\Core\Cache\CacheableMetadata;
 use Symfony\Cmf\Component\Routing\VersatileGeneratorInterface;
 
 /**
diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php
index 60ce2f7..1e7a71f 100644
--- a/core/lib/Drupal/Core/Template/TwigExtension.php
+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -13,7 +13,6 @@
 namespace Drupal\Core\Template;
 
 use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\Url;
diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php
index 36085d6..5e1a1e2 100644
--- a/core/lib/Drupal/Core/Url.php
+++ b/core/lib/Drupal/Core/Url.php
@@ -9,7 +9,6 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\UrlHelper;
-use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Routing\UrlGeneratorInterface;
diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php
index 8b8f05c..ab2d232 100644
--- a/core/lib/Drupal/Core/Utility/LinkGenerator.php
+++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php
@@ -9,7 +9,6 @@
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\GeneratedLink;
 use Drupal\Core\Link;
diff --git a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
index 77cf511..b65526a 100644
--- a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
+++ b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\Core\Utility;
 
-use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Link;
 use Drupal\Core\Url;
 
diff --git a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
index 83a10d8..d86615a 100644
--- a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
+++ b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
@@ -9,7 +9,6 @@
 
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\UrlHelper;
-use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\GeneratedUrl;
 use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
diff --git a/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php b/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php
index 7449536..dc7a864 100644
--- a/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php
+++ b/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php
@@ -94,7 +94,7 @@ public function testProcessOutboundDynamicOne() {
     $parameters = array('slug' => 100);
 
     $cacheable_metadata = new CacheableMetadata();
-    $this->assertNull($this->processor->processOutbound('test', $route, $parameters, $cacheable_metadata));
+    $this->processor->processOutbound('test', $route, $parameters, $cacheable_metadata);
     // Cacheability of routes with a _csrf_token route requirement is max-age=0.
     $this->assertEquals((new CacheableMetadata())->setCacheMaxAge(0), $cacheable_metadata);
   }
@@ -112,7 +112,7 @@ public function testProcessOutboundDynamicTwo() {
     $parameters = array('slug_1' => 100, 'slug_2' => 'test');
 
     $cacheable_metadata = new CacheableMetadata();
-    $this->assertNull($this->processor->processOutbound('test', $route, $parameters, $cacheable_metadata));
+    $this->processor->processOutbound('test', $route, $parameters, $cacheable_metadata);
     // Cacheability of routes with a _csrf_token route requirement is max-age=0.
     $this->assertEquals((new CacheableMetadata())->setCacheMaxAge(0), $cacheable_metadata);
   }
diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php
index b8becb0..e25bea3 100644
--- a/core/tests/Drupal/Tests/Core/UrlTest.php
+++ b/core/tests/Drupal/Tests/Core/UrlTest.php
@@ -9,7 +9,6 @@
 
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Access\AccessManagerInterface;
-use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\GeneratedUrl;
 use Drupal\Core\Routing\RouteMatch;

Fixed a load of unused use statements and some silly assertions that only exist to meet PHPUnit's strict requirement that all tests make at least one assertion.

  • alexpott committed 7342df6 on 8.0.x
    Issue #2335661 by Wim Leers, pwolanin, dawehner, Fabianx, larowlan,...
Wim Leers’s picture

Fixed a load of unused use statements

Sorry about that :(

joelpittet’s picture

+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -201,8 +181,14 @@ public function getPath($name, $parameters = array(), $options = array()) {
-    return $this->urlGenerator->generateFromRoute($name, $parameters, $options);
+    $generated_url = $this->urlGenerator->generateFromRoute($name, $parameters, $options, TRUE);
...
+    // Return as render array, so we can bubble the cacheability metadata.
+    $build = ['#markup' => $generated_url->getGeneratedUrl()];
+    $generated_url->applyTo($build);
+    return $build;

This causes a DX issue:

<div>link via the linkgenerator: {{ link("register", url('user.register'), { 'id': 'kitten' }) }}</div>

becomes the hard to understand:

<div>link via the linkgenerator: {{ link("register", url('user.register')|render, { 'id': 'kitten' }) }}</div>

Because link() doesn't take render arrays and now url() returns an array.

Do we really need to cache URLs?

joelpittet’s picture

Wim Leers’s picture

We're not caching URLs. We're making sure that the overall response is aware of the cacheability of the generated links. That's not the same thing.

i.e. if the resulting URL depends on the current language, then the overall response depends on the current language. And so on.


I don't understand why you're saying things become harder to understand, because everything continues to function exactly the same. What you describe is passing the output of one Twig function to another Twig function, which very much seems like an edge case. You can just use URIs too. See #2342745-55: Allow Twig link function to pass in HTML attributes.

joelpittet’s picture

Sorry for the distress call, the API change worries me a bit here not only for Twig but being a string before was much simpler to deal with.

I'm a bit unclear on when the non string is returned from the url generator. I fear it will cause problems and trying to come up with a decent example to show this.

Fabianx’s picture

BC is kept, but I am worried, too.

If anyone had the time, it would be great to create a document on how to generate urls / links in Drupal 8 ...

I think I once counted 22 ways ...

However there is hope on the horizon with #2450993-24: Rendered Cache Metadata created during the main controller request gets lost and I think we found a good compromise and maybe early-rendering from twig is a good thing (tm). We'll have to see when we analyze this more.

Overall urlGenerator should be deprecated by now anyway as Url::fromWhatever()->toString() should be the way to go ... (I did not know this however ...)

joelpittet’s picture

An example that may hang people up is if they were to use new RedirectResponse from Symfony when people are moving away from drupal_goto().

https://www.drupal.org/node/2023537

$url = \Drupal::url('user.page', [], [], $collect_cacheability_metadata = TRUE);
return new RedirectResponse($url); // FAIL expects string.

Naive question: What are the likelihood of the above happening?

+++ b/core/lib/Drupal.php
@@ -504,17 +504,22 @@ public static function urlGenerator() {
-   * @return string
...
+   * @return string|\Drupal\Core\GeneratedUrl

@@ -537,15 +542,20 @@ public static function linkGenerator() {
-   * @return string
+   * @return string|\Drupal\Core\GeneratedLink

+++ b/core/lib/Drupal/Core/Link.php
@@ -121,9 +121,18 @@ public function setUrl(Url $url) {
+   * @return string|\Drupal\Core\GeneratedLink
...
-  public function toString() {
...
+  public function toString($collect_cacheability_metadata = FALSE) {

+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -349,13 +355,20 @@ public function generateFromRoute($name, $parameters = array(), $options = array
-    return $scheme . '://' . $host . $port . $base_url . $path . $fragment;
...
+    $url = $scheme . '://' . $host . $port . $base_url . $path . $fragment;
+    return $collect_cacheability_metadata ? $generated_url->setGeneratedUrl($url) : $url;

+++ b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
@@ -136,9 +143,14 @@ public function getPathFromRoute($name, $parameters = array());
-   * @return string
+   * @return string|\Drupal\Core\GeneratedUrl

+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -232,14 +224,19 @@ public function getUrlFromPath($path, $options = array()) {
-   * @return string
...
+   * @return array

+++ b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php
@@ -72,17 +78,22 @@
-   * @return string
...
+   * @return string|\Drupal\Core\GeneratedLink

+++ b/core/lib/Drupal/Core/Utility/UnroutedUrlAssemblerInterface.php
@@ -45,13 +44,18 @@
-   * @return
+   * @return string|\Drupal\Core\GeneratedUrl

Worrying parts:S

Fabianx’s picture

#180: The likely hood is rather high, but that it fails is a good thing ...

Because they would need to use a CacheableRedirectResponse() instead [does not yet exist] and set the cacheability metadata they retrieved on the response, exactly for that reason ...

That makes less sense when coming from an uncacheable POST request, but much sense when coming via e.g. /user/me/edit, so the url /user/2/edit would then depend on the current user, hence need the 'user' cache context (or be uncacheable in the first place).

In the ideal world:

- If you want it cacheable, retrieve the cacheable metadata and apply it on the cacheable response
- If you don't want it cacheable, just return the RedirectResponse directly (we are not yet there, but close ...)

Wim Leers’s picture

Yep, we all agree that URL/link generation is horrifying and terrible in D8. Far too many ways to generate URLs/links. There should be one way, and one way only, or if that's not reasonable, then perhaps a few. But not the >dozen that we have today.

Add to that the fact that we never fixed in Drupal 8 the decade-old problem of url() and l() relying on global state/global configuration (see http://wimleers.com/talk-making-drupal-fly-fastest-drupal-ever-near/#/2/2), and that's why this issue indeed feels nasty. But what's important, is to remember that this is a pre-existing problem. This issue may indeed make DX marginally worse (given it's already horrendous), but it's an opt-in-to-more-awfulness thing, rather than forced awfulness (which is the pre-existing part).

Drupal 8 should've rationalized URL/link generation, but it didn't. Instead, we now have the worst of all worlds: Drupal 4/5/6/7's awfulness, plus Symfony's awfulness, plus a whole lot in between.

IMHO we shouldn't be discussing this on this issue, but in a separate issue. I filed #2491981: There are too many ways to generate URLs and links for that.

joelpittet’s picture

@Wim Leers and @Fabianx Just a thought... couldn't that new parameter be a reference object to collect that information and leave the return value as a string in all cases?

Wim Leers’s picture

#183: hah… that was exactly what we did in of the many iterations in this patch, but people didn't like that.

That's the thing: there are downsides to every possible solution. But … the real problem is that this was ignored for so long, so it's always an afterthought, an add-on, a pain-in-the-ass.

joelpittet’s picture

Stupid me, should dig up: https://www.youtube.com/watch?v=b97zJxKEqAk

#117 was the same

HAH

from @Berdir's exact same suggestion and @dawehner knocked it down.

Status: Fixed » Closed (fixed)

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

pwolanin’s picture

Ugh, if I had been watching this issue I would have strongly objected to the solution. Having a variable return value is nonsense.