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
-
\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.- Consequently,
\Drupal\Core\Routing\UrlGeneratorInterface::generateFrom(Route|Path)()
also have a new optional parameter:$collect_cacheability_metadata = FALSE
. The default value ofFALSE
means no cacheability metadata is returned. When it is set toTRUE
, aGeneratedUrl
object is returned. This is a subclass ofCacheableMetadata
(to store cacheability metadata), with one addition: it also stores the generated URL. - Similarly,
LinkGeneratorInterface::generate(FromLink)()
have also gained this exact parameter and behavior: aGeneratedLink
object is returned when$collect_cacheability_metadata === TRUE
. - 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
usesLinkGenerator
, which uses$url->toString()
. 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 aCacheableMetadata
object for every single outbound path/route processor, and then be merging it. By instead passing in aCacheableMetadata
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.
-
#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.- Twig's
link()
,url()
andurl_from_path()
functions now bubble the necessary cacheability metadata too.
Comment | File | Size | Author |
---|---|---|---|
#171 | outbound_processors_cacheability-2335661-171.patch | 116.84 KB | Wim Leers |
#168 | interdiff.txt | 996 bytes | Wim Leers |
#168 | outbound_processors_cacheability-2335661-168.patch | 117.54 KB | Wim Leers |
#165 | interdiff.txt | 1.65 KB | Wim Leers |
#165 | outbound_processors_cacheability-2335661-165.patch | 117.36 KB | Wim Leers |
Comments
Comment #1
Wim LeersComment #2
Wim LeersComment #4
Wim LeersRerolled against HEAD, but will surely fail tests. I just want a review of the approach, so marking as "do not test".
Comment #5
dawehnerI 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?
Note: This applies patterns usually detects that on route compile time, can we do the same? Store the route processors on route options?
Comment #6
Wim LeersIn this reroll, I'm no longer changing
MenuLinkBase::isCacheable()
— it made little sense after all. Now only changingMenuLinkContent
. That makes the patch much, much simpler.Also added test coverage.
Comment #7
Wim LeersDo you mean there might be other ways for links to be dynamic, and therefore non-cacheable? Please clarify.
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 callsisCacheable()
, so it's certain this does not introduce a performance regression.)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!
Comment #8
Wim LeersComment #9
Wim LeersComment #10
Wim LeersBetter title.
Comment #11
dawehnerCrazy shit, this issue already didn't got any comment since three days. Welcome to our world!
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).
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.)
Comment #12
Wim LeersIf that is happening somewhere, it's definitely not here. This issue is only making the implementation comply with the interface/API.
You're right.
As you say, path processors can make arbitrary changes, and e.g. the
path processor alters every single path. So, theoretically speaking, whenever any path processor is enabled,MenuLinkInterface::isCacheable()
would always have to returnFALSE
.But… practically speaking:
::isCacheable()
returnTRUE
when the language negotiation processor running should actually make it returnFALSE
will not cause any bugs::isCacheable()
implementation that lies less is betterSo, 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.Comment #13
dawehnerWell, 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.?
Comment #14
Wim LeersThis blocks #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees, which is critical.
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 returnsTRUE
) 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:
Comment #16
dawehnerLet me try to rephrase what got responding with "nothing new". So It would be cool to have dedicated test coverage for:
Comment #17
dawehnerOn top of that I wonder whether we can add at least a TODO to figure out how to deal with path processors?
Comment #18
catchOutbound 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.
Comment #19
Fabianx CreditAttribution: Fabianx commentedI 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.
Comment #20
Fabianx CreditAttribution: Fabianx commentedI opened #2351015: URL generation does not bubble cache contexts for an alternate approach to the problem of CSRF tokens.
Comment #21
catchI'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.
Comment #22
catchDowngrading this to major for the non-CSRF portion. Also moving to a bug report though.
Comment #23
effulgentsia CreditAttribution: effulgentsia commentedSee 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.
Comment #24
Wim LeersLet's slowly get this back on track, step 1: fixing the title.
Comment #25
Fabianx CreditAttribution: Fabianx commentedI am gonna steal this issue from its meta based on its dependency on what we do with cache contexts.
Comment #26
xjmElevating 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.
Comment #27
pwolanin CreditAttribution: pwolanin at Acquia commented#2471743: Create a more generic superclass of \Drupal\Core\Render\BubbleableMetadata is committed, so I can start taking the next steps.
Comment #28
Wim LeersComment #29
pwolanin CreditAttribution: pwolanin at Acquia commentedVery rough 1st pass - probably broken, let's see how badly tests fail.
Comment #31
Wim Leerss/no not/so not/
Parameter order mismatch is causing the test failures.
Is there an issue for this already that this can link to?
s/url/URL/
.
Comment #32
pwolanin CreditAttribution: pwolanin at Acquia commentedFixed those I think.
Comment #34
Wim LeersComment #35
pwolanin CreditAttribution: pwolanin at Acquia commentedstill pretty broken, but some progress
Comment #37
Wim LeersNeeds FQCN.
Comment #38
dawehnerSo 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.
This doesn't make sense, its just the outbound, we no longer need.
We should clearly document what this is about
@return $this
Comment #39
pwolanin CreditAttribution: pwolanin at Acquia commentedThose are the easy ones - but it's more deeply broken.
Comment #40
pwolanin CreditAttribution: pwolanin at Acquia commentedok, new approach with just passing an object around to collect the data
Comment #42
pwolanin CreditAttribution: pwolanin at Acquia commentedfix some leftover incorrect calls
Comment #43
dawehnerI 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.
Comment #44
pwolanin CreditAttribution: pwolanin at Acquia commentedComment #48
pwolanin CreditAttribution: pwolanin at Acquia commentedForgot to change the 2nd version of the template
Comment #49
pwolanin CreditAttribution: pwolanin at Acquia commentedComment #52
pwolanin CreditAttribution: pwolanin at Acquia commentedFixing return value map in a unit test.
Comment #54
pwolanin CreditAttribution: pwolanin at Acquia commentedFix a couple little bugs, including a pre-existing one in MenuForm
Comment #56
pwolanin CreditAttribution: pwolanin at Acquia commentedfix 2 more unit test value maps.
Comment #57
Wim LeersGreen patch! :)
dreditor review:
s/NuLL/NULL/
s/bubbleable/cacheable/
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>
.Also max-age. Why not just say
Why is this aliasing necessary?
Alternatively:
RendererInterface::addDependency()
.So these two lines would then become:
Next: applying patch + deeper review.
Comment #58
pwolanin CreditAttribution: pwolanin at Acquia commentedre:
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.phpI think keeping the merge code is more understandable (and more performant).
Comment #59
Wim LeersFor 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
anduser
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.
Comment #60
Fabianx CreditAttribution: Fabianx for Acquia commentedThis 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:
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:
Why not:
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.
Comment #61
Wim LeersFinish 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.Comment #62
Wim LeersVoila, now split #61's changes that affected
CacheableMetadata
+BubbleableMetadata
to #2474121: CacheableMetadata should get BubbleableMetadata's merge/applyTo/createFromRenderArray/createFromObject methods.Comment #63
Wim Leers#58:
CoreUrl
: d'oh, right!#60:
Ah, interesting, we didn't think of that :)
That is interesting. We currently have this complex
messinteraction between 4 things, with 2 symmetries:Url
+Link
value objects andUrlGenerator(Interface)
+LinkGenerator(Interface)
services. If we'd only haveUrl
, and onlyUrlGenerator
, andLinkGenerator
would be deleted in favor of aUrl::generateLink($text)
orUrl::toLink($text)
method on theUrl
value object (much like it already hastoString()
), and a matchingUrlGenerator::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.
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.
We already have that:
\Drupal\Core\Link
. The problem, as I said above, is that it's not clear which to best use when.Until #39, pwolanin was letting
UrlGenerator
return aCacheableUrl
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).
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?
Why is this
instanceof
check actually even necessary? It wasn't there before…Comment #64
dawehnerI'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 patternThis certainly varies by session, if you are honest, read the previous code in that class
This change is a bit odd ... given that we have different options ...
Comment #65
pwolanin CreditAttribution: pwolanin at Acquia commentedre: $link->getOptions() in MenuForm - previously, the 3rd param was thrown away, so that was just a bug.
Comment #69
Wim Leers#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.Comment #71
Wim LeersThe 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
).Comment #72
Wim LeersFixing what's in #69's rebase-interdiff in #2475397: Tiny follow-up for #2474121 + unit test coverage for (Bubbleable|Cacheable)Metadata::merge().
Comment #73
catchAfter #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?
Comment #74
cpj CreditAttribution: cpj commented@catch #73, I'm presuming (hoping) this would also work for non cookie-authenticated sessions (as per Decouple session from cookie based user authentication) ?
Comment #75
Fabianx CreditAttribution: Fabianx for Acquia commentedThe clean clean way is to add a csrf_token cache context, which uses the csrf_token manager to get a token ...
Comment #76
pwolanin CreditAttribution: pwolanin at Acquia commentedThe cost of generating a token on the page is likely less than the cost of added cache complexity?
Comment #77
Wim Leers#73: #74 is exactly why we did not add a session cache context in that issue.
Comment #78
Wim Leers#2475397: Tiny follow-up for #2474121 + unit test coverage for (Bubbleable|Cacheable)Metadata::merge() landed. Rebased.
Next: tests.
Comment #79
Wim LeersAnd 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.
Comment #80
Wim LeersI'll update the IS later today.
Comment #81
pwolanin CreditAttribution: pwolanin at Acquia commentedPatch 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.
Comment #82
Fabianx CreditAttribution: Fabianx for Acquia commentedSorry, some parts of #60 still need to be addressed, some can be follow-up though.
Comment #83
pwolanin CreditAttribution: pwolanin at Acquia commented@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.
Comment #84
dawehnerLet'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 ...
Comment #85
webchickJust the tag fairy, carry on.
Comment #86
Wim Leers#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?
Comment #87
Fabianx CreditAttribution: Fabianx for Acquia commented{{ 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.
Comment #88
dawehnerI'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.
Comment #90
Wim LeersAddressed the immediate concerns in #82/#83/#87:
menu.html.twig
no longer usinglink()
. There is an obvious solution, that I double-checked with Fabianx to be acceptable: letlink()
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:
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. TheMenuLinkInterface::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, isurl()
. Doing that next.After that, I'll address #88. (Which will include making the IS 200% better.)
Comment #91
Wim LeersThat's what this does.
Next: #88, and absolutely agreed that we need more/better test coverage, especially with the interdiffs in #90 and here!
Comment #93
Wim LeersFirst, fixing test failures.
Comment #94
Wim LeersA 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.
Comment #95
Wim LeersWhile 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.
Comment #98
Wim LeersFailures because:
UrlGeneratorInterface
to have the necessary updates, but I forgot to update a test, thus causing a fatal.Comment #99
Wim LeersSuch n00b.
Comment #100
Wim LeersI re-read the entire issue, to correctly answer #88.
#23:
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:
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?
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:
And that is an excellent, excellent question! Trying that now :)
Sorry for all the confusion & frustration :(
Comment #101
Wim LeersHaving 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
andUrl::toString()
, to have both#type => link
andLinkGenerator
. 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.
Comment #103
Wim LeersForgot to update one small thing; AFAICT that's causing all these test failures.
Comment #105
Wim LeersThose 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.
Comment #106
Wim LeersCR created: https://www.drupal.org/node/2480761.
Comment #107
Fabianx CreditAttribution: Fabianx for Acquia commentedCR looks great, missing \Drupal::l() and \Drupal::url() for now still (and other ways I might have not thought about) ...
Comment #108
Wim LeersFixed
\Drupal::url()
(99% of that is in tests and inhook_help()
), and\Drupal::l()
. CR updated.Comment #109
dawehnerI don't think \Drupal:url() and \Drupal::l() has to support every feature ... given that they are convenience wrappers anyway.
Comment #111
Fabianx CreditAttribution: Fabianx for Acquia commented#109: Especially those need to support those features, as else:
While that is still horribly broken, the API should at least speak of the cacheability, though we need to harden that more ...
Comment #113
Wim LeersHah, this uncovered a small bug in
\Drupal\views\Plugin\views\field\FieldPluginBase
, which would've prevented Views' "more" links from having theviews-more-link
class :) That's what caused the two failures.Comment #114
dawehnerDid a big review of the entire patch. Overall the patch looks really great.
Right that fix is indeed needed.
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?
It would be great to explain in the issue summary why we return once and once we alter.
wow, I wasn't aware that you can write into variables.
Why do we use () here? its a simple boolean check, isn't it?
Theoretically the base path could change, is that included inside url.host?
It uses session information so its not just ::TYPE_URL, isn't it?
You can use
\Drupal\Tests\UnitTestCase::getConfigFactoryStub
and write that much nicerDo you mind opening up a follow up to replace that usage of base_path()? It would be ne of the last ones ...
Comment #115
Wim Leers\Symfony\Component\HttpFoundation\Request::getSchemeAndHttpHost()
, so, no, base path is not included. Fixed that. Renamed the cache context toSiteCacheContext
, and the cache context ID tourl.site
.Comment #116
Berdirshouldn't this say "site" now?
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.
Comment #117
Wim LeersAccessibleInterface::access()
works (the$return_as_object = FALSE
thing).I don't have a preference. So it's up to you & dawehner.
Comment #118
dawehnerWell, 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.
Comment #119
znerol CreditAttribution: znerol commentedI'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.
Comment #120
Wim Leers#119: But that's a bigger API change.
Comment #121
Fabianx CreditAttribution: Fabianx for Acquia commentedI 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
So url.site now contains also the language prefix?
2 questions, but RTBC, except that the patch for #117 typo is missing.
Comment #122
Wim LeersSorry, forgot to upload the patch accompanying #117. Here it is.
Comment #123
Wim Leers#121: Thanks for the review!
url.site.basepath
?url.site
= host + port + base path, i.e. the value for theurl.site
cache context is calculated without path processing.Comment #124
larowlanThat 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?
Comment #125
larowlanFor 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.
Comment #126
larowlanAlso 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.
Comment #127
larowlanoh after reading back over comments, seems I'm not the only one, phew
Comment #128
larowlanAny reason we can't have generateFromPathWithCache, generateFromRouteWithCache etc?
Comment #129
larowlanor 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
Comment #130
larowlanlast comment, promise - I see you discussed this at 100/101 but bool|AccessResult isn't the same thing as string|[string, array]
Comment #131
Wim Leers#130: I'm fine with
string|CacheableUrl
. That's basically what we had in #40. But back then, we were always returningCacheableUrl
, and that is what was infeasible. (EDIT: And I agree that that is better, and more similar toAccessibleInterface::access([…], $return_as_object = FALSE)
.)Would we all be fine with that?
Comment #132
Fabianx CreditAttribution: Fabianx for Acquia commented#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.
Comment #133
dawehnerLet's ensure how much this changes the performance on an ordinary site like admin/index.
Comment #134
Wim LeersSo we'll go with #130/#131.
Comment #135
Wim Leers#133:
(Refreshed until the number of function calls stabilized.)
/admin/index
/node/1
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.
Comment #136
Wim LeersComment #137
Wim LeersLet'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.
Comment #138
dawehnerWow, that is not nothing, where is this primarily coming from?
Comment #139
Wim LeersI 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 forCache::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.
Comment #140
Wim LeersThis 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!
Comment #141
Wim LeersSo, ignore #139.
Profiling
/node/1
again, with #137:That's 1137 additional function calls. The difference indeed lies in the collecting and merging of cacheability metadata.
But:
/node/1
, and would cause that 1137 additional function calls to drop to <1000 additional function callsComment #142
dawehnerWell, I think we should have a close look at what costs these additional function calls, ... we can not just look at entirely warm requests.
Comment #143
larowlanCan we avoid this merge if we instead work the other way? Given GeneratedUrl extends from CacheableMetadata could we not just do
That would save some merges right? Or am I missing extra context not in the diff.
Comment #144
larowlanrolling a patch to illustrate 143
Comment #145
larowlanThis passes all unit tests and we lose the ::merge calls
Comment #146
Wim Leers#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!
Comment #147
Wim LeersCross-posted :) I think mine is more clear (because I remove
$cacheable_metadata
, rather than renaming$generated_url
to$cacheable_metadata
), and you forgot to updateUnroutedUrlAssembler
. However, you also did find one more possible optimization than I did: the one inLinkGenerator
. Fixing that too now.Comment #148
Wim LeersDid #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!Comment #149
Wim LeersUpdated IS for #134/#137–#148. Also updated CR.
Comment #151
larowlan+1 to new changes, this looks great to me
Comment #152
Wim LeersAnother round of profiling to answer this.
/node/1/
+1556 function calls
/node/1/
+ #2483433: Optimize CacheableMetadata::merge() + BubbleableMetadata::merge()+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()):
Diff%
Diff
(microsec)
Diff%
Diff
(microsec)
Diff%
MemUse
Diff
(bytes)
Diff%
MemUse
Diff
(bytes)
Diff%
PeakMemUse
Diff
(bytes)
Diff%
PeakMemUse
Diff
(bytes)
Diff%
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:
Diff%
Diff
(microsec)
Diff%
Diff
(microsec)
Diff%
MemUse
Diff
(bytes)
Diff%
MemUse
Diff
(bytes)
Diff%
PeakMemUse
Diff
(bytes)
Diff%
PeakMemUse
Diff
(bytes)
Diff%
Comment #153
Wim LeersForgot to include the xhprof runs.
Comment #154
dawehnerOH 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?You could achieve the exact same thing with using the request context and
\Drupal\Core\Routing\RequestContext::getCompleteBaseUrl
We should make it clear in the documentation when to use Url/Link and when to use GeneratedLink/GeneratedUrl
I'd kind of expected that you could pass the link also as part of the constructor
... so for now those objects are not renderable inside twig, isn't that something we want?
So yeah in case those objects would be printable, so for example implement (__)toString we would not have to do that custom handling here.
Comment #157
Wim LeersTestbot terminated, twice, gaahhhh. Testbot--
#154:
RequestStackCacheContextBase
, which more clearly communicates where its data is coming from.CacheableMetadata
andBubbleableMetadata
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.toString()
method if you want, but … see the next point.Renderer
, because that allows us to bubble cacheability metadata from links! We could make Twig detect if an object withtoString()
also implementsCacheableDependencyInterface
, 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.Comment #158
Fabianx CreditAttribution: Fabianx for Acquia commented#157.6 I agree, if we want to support twig directly add a toRenderArray() method instead and have twig honor that.
Comment #160
Wim Leers#157 failed due to testbot errors. Re-testing.
Comment #162
dawehnerThings look pretty much alright for me.
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.
Nitpick: the link generator has an interface ...
Comment #163
Wim LeersThanks!
Comment #164
catchGenerally 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.
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.
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.
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.
eek but again don't have better ideas and the discussion has already covered this.
Comment #165
Wim LeersBack to RTBC since this is only changing docs.
Comment #166
catchsame site? And should we give examples like http:// and http://www.?
Comment #167
alexpottnw for #166
Comment #168
Wim LeersDone.
Comment #170
plachComment #171
Wim LeersRebased.
Comment #172
alexpottCommitted 7342df6 and pushed to 8.0.x. Thanks!
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.
Comment #174
Wim LeersSorry about that :(
Comment #175
joelpittetThis causes a DX issue:
becomes the hard to understand:
Because link() doesn't take render arrays and now url() returns an array.
Do we really need to cache URLs?
Comment #176
joelpittetReference: #2342745-46: Allow Twig link function to pass in HTML attributes
Comment #177
Wim LeersWe'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.
Comment #178
joelpittetSorry 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.
Comment #179
Fabianx CreditAttribution: Fabianx for Acquia commentedBC 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 ...)
Comment #180
joelpittetAn 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
Naive question: What are the likelihood of the above happening?
Worrying parts:S
Comment #181
Fabianx CreditAttribution: Fabianx for Acquia commented#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 ...)
Comment #182
Wim LeersYep, 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()
andl()
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.
Comment #183
joelpittet@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?
Comment #184
Wim Leers#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.
Comment #185
joelpittetStupid me, should dig up: https://www.youtube.com/watch?v=b97zJxKEqAk
#117 was the same
from @Berdir's exact same suggestion and @dawehner knocked it down.
Comment #187
pwolanin CreditAttribution: pwolanin at Acquia commentedUgh, if I had been watching this issue I would have strongly objected to the solution. Having a variable return value is nonsense.