Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
$url->toString(TRUE)->getGeneratedUrl()
Seriously, what does TRUE even mean.
Proposed resolution
- Add
$url->generate()
- Deprecate \Drupal\Core\GeneratedUrl::getGeneratedUrl() in favor of \Drupal\Core\GeneratedUrl::toString().
- disputed: Deprecate \Drupal\Core\Url::toString() in favor of \Drupal\Core\GeneratedUrl::toString(). This will lead to longer constructs ($url->toString() becomes $url->generate()->toString()), but enables us to also deprecate MetadataBubblingUrlGenerator / the behind-the-scenes bubbling of metadata that has caused many LogicExceptions
Remaining tasks
User interface changes
API changes
Data model changes
Comment | File | Size | Author |
---|---|---|---|
#76 | 2735575-nr-bot.txt | 157 bytes | needs-review-queue-bot |
#66 | 2735575-64-interdiff.txt | 4.87 KB | roderik |
#64 | 2735575-generatedUrl-64.patch | 91.79 KB | roderik |
#45 | 2735575-generatedUrl-45.patch | 284.32 KB | tim.plunkett |
#31 | 2735575-generatedUrl-31.patch | 36.33 KB | tim.plunkett |
Comments
Comment #2
dawehnerComment #3
Nikhilesh Gupta CreditAttribution: Nikhilesh Gupta as a volunteer and at Melity commentedComment #5
tim.plunkettShould it return the generated URL (string) or the GeneratedUrl (object)? I think
->toGeneratedUrl()->getGeneratedUrl()
makes very little senseComment #6
mayurjadhav CreditAttribution: mayurjadhav commentedYep, tim.plunkett make sense,
function toGeneratedUrl() returns "->toString(TRUE)", updated patch with "->getGeneratedUrl()".
Comment #7
dawehnerWell, the entire point of this method IMHO is to get back the object with the metadata attached, because, well,
$url->toString()
does the same already. This issue is about the problem of$url->toString(TRUE)
being impossible to understand.Comment #8
tim.plunkettYep, that makes sense. I just think that GeneratedUrl::getGeneratedUrl() is a terrible method name, but that's out of scope here.
Comment #9
dawehner@tim.plunkett
Agreed! Cool that we agreed here :)
Comment #10
Wim LeersI think this is only slightly better. It's still very confusing/bizarre.
If everybody thinks this is a step forward, then I'm fine with it. I'm fine with anything, as long as we continue to have cacheability metadata for generated URLs. No matter what we do, it's always going to be lipstick on a pig.
I agree all of this is ugly, confusing, bizarre, suboptimal, and so on. Let's not forget the root cause though: our URL generator, and Symfony's, is infested with globals.
Comment #11
dawehnerYou know that this is not the only reason we have to do that. Also CSRF token URLs need cacheability metadata. I really think this is a problem made by Drupal in a way.
Of course this is not a huge improvement, but at least it tells readers of this function something ...
Comment #13
tim.plunkettOkay so I'm fine with the addition of toGeneratedUrl()
But is there a reason to even call
->toGeneratedUrl()->getGeneratedUrl()
in any of these cases in the patch? How is it different than just->toString()
?Or does it just indicate a place where the metadata isn't properly handled, and should never exist?
Regardless,
<code>->toGeneratedUrl()->getGeneratedUrl()
is just too confusing to read. Proposing deprecating getGeneratedUrl() for toString().Comment #14
tim.plunkettBetter summing up my reasoning for that change:
Also, whenever we have a call to Url::toString(), it probably means that metadata isn't handled properly, and is likely a bug. But thats out of scope here.
Fixing a bug, bad merge added the method twice.
Comment #15
Wim LeersWhy not
generate()
instead?Url::generate()
then results in aGeneratedUrl()
object.That seems more logical than
Url::getGeneratedUrl()
.Comment #16
Wim LeersThis is not in the IS nor title.
Comment #17
tim.plunkettAgreed with the rename
Comment #20
dawehnerI really like this change, and well, stopping here is also cool. Changing too much in one issue is bad anyway.
I added two change records in the meantime.
Comment #21
tim.plunkettWas that supposed to be an RTBC?
Rerolled.
Comment #22
dawehnerWe could throw
@trigger_error
, can't we?Should we document on Url::toString that generate() is the more appropriate method to use?
Comment #23
Dinesh18 CreditAttribution: Dinesh18 as a volunteer commentedI think for the 1st point we can use @trigger_error('Deprecation message', E_USER_DEPRECATED) which will be more meaningful.
Comment #24
dawehnerComment #28
tim.plunkettAddressed #22 and fixed more usages.
Comment #30
Wim Leers🎉
Comment #31
tim.plunkettThere's a lot of weirdness here. But just trying to get it passing first...
Comment #33
tim.plunkettTestbot issue (selenium didn't load).
Retest passes
Comment #34
andypostLooks ready except
8.7.x I guess...
Comment #35
tim.plunkettGiven a \Drupal\Core\Url object, there are now two non-deprecated methods to call.
toString()
, if you just want the string and don't care about cacheable metadatagenerate()
, if you do need the cacheable metadata, and later you can calltoString()
on this object instead of the original Url object.IMO, everyone should always be calling
generate()
and using that result to include cacheable metadata, and then go on to get the string.I'm not 100% sure what the use case is for bypassing that, barring laziness. (perhaps too harsh of a word, but meaning that the concept of caching is bypassed here).
Furthermore, there are two instances in this patch where the code does
$url->generate()->toString()
, which tricks the system into thinking the dev has respected the cacheable metadata, when they haven't. This avoids the dreaded exception that saysvia
\Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber::wrapControllerExecutionInRenderContext()
.I left those hacks in there to get the tests to pass. But is that okay?
Comment #36
Wim LeersAFAIK there's two reasons:
toString()
because you hate/don't care for cacheabilityThat's also why we did that automatic bubbling in the background in D8, to ensure people actually did think about it.
Comment #37
Wim LeersIf I understand your proposal, then I think you're saying that going forward:
Url::generate()
if you care about cacheability (you get a value object with the generated URL and its cacheability)Url::toString()
if you don't care about cacheability (you get a string), cacheability is still bubbled automatically in the background, and could still result in that dreaded exceptionIOW: this keeps the same automatic bubbling. Do we want to? After years in D8, can we drop
\Drupal\Core\Render\MetadataBubblingUrlGenerator
and leave that responsibility with the developer? So that if they useUrl::toString()
, no cacheability is bubbled in the background. That'd remove magic. But it'd also increase reponsibility.Perhaps this is out-of-scope for this particular issue, but it made me think of this.
Comment #38
tim.plunkettPerhaps it would be overly prescriptive or result in even more
->generate()->toString()
hacks, but why not deprecateUrl::toString()
as well to encourage handling of metadata?Comment #39
Wim LeersDefinitely works for me. Now that you mention it … I now recall that that was the whole reason we did it this way: we couldn't break existing code.
Doing what you describe there would also allow us to deprecate
\Drupal\Core\Render\MetadataBubblingUrlGenerator
. And there's be fewer code paths. And less magic.Comment #40
tim.plunkettI think this might be too much. But let's see how bad it is.
Comment #42
tim.plunkettSloppy attempt at find and replace, let's see.
Comment #44
tim.plunkettComment #45
tim.plunkettcore/modules/image/templates/image-formatter.html.twig giving some trouble (and Twig in general, thanks to
\Drupal\Core\Template\TwigExtension::escapeFilter()
having toString calls)Comment #48
dawehnerI'm wondering whether for easier reviewing the patch could be split up into the
Url:: toString -> generate
and theGeneratedUrl:: getGeneratedUrl -> toString ()
change.Comment #49
tim.plunkettIt would certainly be easier to review. My worry is that if we actually split it up, we'd be forcing people to change from
$url->toString(TRUE)->getGeneratedUrl()
to
$url->generate()->getGeneratedUrl()
to
$url->generate()->toString()
That feels rather odd (I especially dislike the
generate()->getGeneratedUrl()
part).But honestly this patch is completely unreviewable as-is, so we probably have to do something.
My interest for now is trying to get it passing first, before splitting up.
Comment #50
tim.plunkettI guess that's better than doing them in the other order, where the intermediate API would call for
$url->toString(TRUE)->toString()
;)Comment #51
roderikAfter some LogicException pain and various tries to understand what is happening inside the Url code... I wanted to open an issue to add extra method documentation to Url::toString() to ease some developer pain, but then came across this one. I assume this issue is still current / not superseded by anything.
This change feels sensible. I'll try to look at it with fresh eyes when everything has sunk in but as far as I can see now:
* People may see it as a DrupalWTF that they have to do $url->generate()->toString() to get a simple string
* ...but they won't have to debug any LogicExceptions
* ...and we're likely able to explain the reasons. (I can't totally get my head around the concepts of Url vs UrlGenerator vs GeneratedUrl yet, but I'll try again later.)
So: picking this up.
First a 'straight' reroll. It wasn't trivial but I'm fairly sure I didn't miss anything.
Tomorrow I'll
* add more replacements. (Even in the context of the patch I see some lines that used to be $entity->url(), were changed to $entity->toUrl()->toString(), and now need to be changed to $entity->toUrl()->generate()->toString().)
* hopefully look at what the testbot says.
* maybe later look at "deprecate \Drupal\Core\Render\MetadataBubblingUrlGenerator". It would be an honour.
Comment #53
roderikComment #54
roderikInterdiff is boring; it's only
I've seen 'something' is needed in TwigExtension::escapeFilter() / theme_render_and_autoescape(), but will first see what the bot says.
Comment #56
roderikOf course this failed to apply against #1883744 - rebasing.
Comment #57
roderikHere's one with the TwigExtension::escapeFilter() stuff - the first 'interesting' interdiff I've uploaded.
I will have questions, because I'm not so sure anymore that just doing a sweeping replacement by ->generate()->toString is really... adding anything useful.
But those questions will possibly wait until the patch is green, and split out in two, as per #49. (Because the questions are about 'phase 2' anyway.)
Comment #62
roderikWhile gathering thoughts, I've picked apart the patch into 4 parts. I'm hoping the first 3 will succeed, in which case I'll have a more solid basis for writing up thoughts.
Comment #64
roderikWell... gathering thoughts took a while. I have them drafted now, but let's see if this goes green first, before I comment on it.
Comment #65
roderikComment #66
roderikI'm rebasing this patch in several separate commits on my local system, so I can re-add things if needed (and it's a little easier for me to keep chasing HEAD if I have to). I can also upload separate patches for s/toString(TRUE)/generate() and s/getGeneratedUrl()/toString()/ for easier review if you want.
The one uploaded as #64 is basically equal to #31, except:
s/toString(TRUE)/generate()
ands/getGeneratedUrl()/toString()/
That is: I backed out the deprecation of Url::toString(FALSE), because I don't 'get' it. (I'm still keeping a rebased copy of that code locally, though.)
This is now a fairly straightforward replace of one function by another, plus a docblock addition (and I'm going to argue for moving discussion about other things into a followup issue).
And...
I'm fully aware that me barging into a Daniel/Tim/Wim issue, holds a risk of just creating diversion, but... I don't have another option if I want to help out on the Url related issues. (I tried to do my homework and read up.)
...and to me it feels like the discussion went sideways from #35 - #39, or I'm just not getting things.
---
Before I get to talking about things I don't get, let's just first enumerate where Core itself doesn't do this, but calls plain Url::toString() - which can be found by searching the '1.2.3.4-62' patch (mostly equal to Tim's latest patch) that replaced them with ->generate()->toString():
Entity\Form\DeleteMultipleForm
FormSubmitter::redirectForm()
CommentController::commentApprove()
layout's AddSectionController::build()
MediaDeleteMultipleConfirmForm::getConfirmText
user module's AccessDeniedSubscriber::onException() and MaintenanceModeSubscriber::onKernelRequestMaintenance()
That's... a fairly long list. (It's longer than when Tim last looked at it because Entity::Url() was deprecated, which unhides a lot of these cases where generate() was not called.)
Also: there's the ~400 lines of \Drupal::url() which I haven't fully looked at yet but will soon also be toString() methods, as per #2731817: Replace all calls to the deprecated Drupal::url() function in Core.
I'm not sure if I 'get' verything involved in the above code. From what I can gather, just doing toString() without calling generate() may be OK:
...but 1) I have a hard time believing that's the case in all the above code, and 2) then the 'bubbling in the background' can't be removed, right? (Or do we not always care about handling metadata, because e.g. we completely trust that other code in the same request adds the same metadata? I don't believe that's our stance.)
---
Here's where I was losing the discussion.
1)
I was first assuming that we were going to just replace toString() calls by straight generate()->toString() calls, since patch #42 and further were doing that. But AFAICT we cannot do this because it introduces a risk of cache pollution. And it encourages bad practice. (Because we're not handling the cacheability metadata. Or -same question as above- are there instances where we don't care about handling these, because e.g. we completely trust that other code in the same request adds the same metadata?)
So for now I am going to assume that we don't want to do a straight replace, and we should fix the majority of code in the above list, to properly handle metadata. But that seems like a separate followup (or even meta issue?) because it's too much to just fix alongside the original intent of this issue.
Feel free to tell me whether I should approach this differently... or if separate issues should indeed be opened to fix up Url::toString() code in Core. I can do some work on this if needed.
2)
Personally, yes I am indeed afraid that this is going to result in even more ->generate()->toString() hacks. I am afraid that deprecating Url::toString() isn't going to encourage handling of metadata.
Not only because Core itself isn't always giving the right example and I don't think there is explicit wording in source code / drupal.org telling people to do the right thing yet... but in my experience, for some years the prevalent advice linked from Google has been to do the wrong thing: "just use ->generate()->toString()".
I'm not saying we shouldn't eventually deprecate toString() and/or remove automatic bubbling... but I doubt we're ready yet (see above) / I'm fairly convinced we should move the discussion about that to a followup issue. It's too big/hairy to decide halfway the comments on this issue.
IMHO the goal of this issue is to improve DX - in two ways:
1) renaming the two methods to be more intuitive, as per patch #31
2) document toString() to tell people about its restrictions and proper usage, which I added a proposal for (and which was my original reason for bumping into this issue). Because AFAICT that has not really been done before - and is a necessary step on the path toward perhaps deprecating toString(). (See also 'soapbox rant' below if you want.)
Similar answer: AFAICT we are not ready for dropping MetadataBubblingUrlGenerator and putting the responsibility on all developers to do the right thing, because from my viewpoint, we have not properly started telling people that they have that responsibility. (Again see 'soapbox rant' below if you want.)
I'm sure many contrib developers who call Url::toString() don't know what it does in the background and what they should be thinking of. Telling people more explicitly would be the first/next step on a path toward deprecating automatic bubbling. Or... we can start telling them and deprecating toString() right now, but IMHO at least
Again - I can do at least some work on this if we have agreement / you tell me what is the way forward.
---
'Soapbox rant':
Just "generating a URL (string) that represents a route in Drupal" seems like a simple thing for developers; noone naturally understands or assumes they heed to read the caching API and know what a cache context and bubbleable metadata is, just to be able to do this.
I get that we can't (always) make "just outputting a simple URL" easy. Because caching issues. But then we should at least tell them "this is not easy, because caching issues".
I imagine an average developer is like me in 2017 (#3032255: Extend Cacheable/RedirectResponse constructor to accept GeneratedUrl): if they want to create a URL for something, are going to look at the Url object. And then find a method toString(). Well,
toString(true)->getGeneratedUrl()
. Because that's the only information they found. (For years, the highest ranking informative google pages in a google search were https://drupal.stackexchange.com/questions/187086/trustedresponseredirec... and #2630808: Response::getResponse() does not collect cacheability metadata from Url object. The former leads to the latter, where an experienced Drupal developer is saying to usetoString(true)->getGeneratedUrl()
.)I'm not blaming the code for being complex, but on the way from "I just want to output a URL" to "hey there's a URL object" to "WTF is this early rendering exception I'm getting" to (hopefully not but somewhat likely) "just use toString(true)->getGeneratedUrl() and be done with it"... at least one road sign is missing.
Which explains the positively monstrous addition to the method docs of Url::toString(), that I'm putting up for review / in the interdiff. For people who have not been exposed that they need to think about cacheability, we IMHO need to have some practical pointers specific to Url generation, without them being absolutely required to read cache API docs. And I don't think these are anywhere else.
I'm almost sure this size of comment breaks at least the spirit of code comment standards, but... it's just a sign of the complexity / interdependencies we've introduced. And I think these kinds of pointers are suitable for a method docblock.
I'm happy to be told that this is the wrong way to go about things, or that the description is wrong, or whatever. I'm also happy to split this out into another issue if this is considered separate from this issue. But in my mind the order of things to happen in this area is:
1) this issue renaming Url::toString(TRUE) and UrlGenerator::getGeneratedUrl(), which I'm happy about as a simple developer.
2) at least one more road sign for devs trying to "do URL strings" (possibly in this same issue)
3) creating followup issue(s) about deprecating toString() and/or the automatic metadata bubbling and/or cleaning up Core code that needs to be done first.
Comment #67
roderikComment #68
joachim CreditAttribution: joachim as a volunteer commentedComment #76
needs-review-queue-bot CreditAttribution: needs-review-queue-bot as a volunteer commentedThe Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.