Detailed analysis of the existing usages
\Drupal::url() — 461 occurrences
- t('foo bar baz', \Drupal::url()) (e.g. /ViewsBlockBase.php:1)
- test assertions (e.g. core/modules/user/src/Tests/UserPasswordResetTest.php:224)
- hook_help() (e.g. \user_help())
- redirects (e.g. new RedirectResponse(\Drupal::url('', [], ['absolute' => TRUE])); -> solveable
- twig variables (e.g. $variables['book_url'] = \Drupal::url('entity.node.canonical', array('node' => $book_link['bid']));)
Drupal::l() — 98 occurrences
- #theme => item_list's #items should support Url (e.g. core/authorize.php:136)
- hook_help() (e.g. block_help())
- test assertions (e.g. core/modules/book/src/Tests/BookTest.php:231)
- twig variables (e.g. $variables['title'] = \Drupal::l($comment->getSubject(), new Url(''));, core/modules/comment/comment.module:670)
- links printing the URL or path as link text (e.g. core/modules/views_ui/src/ViewListBuilder.php:268)
UrlGenerator::generate() — only test occurrences
Should be deprecated, implicitly already is, we just need to make it explicit.
UrlGenerator::generateFromRoute() — 33 occurrences
Should be internal.
UrlGenerator::generateFromPath() — 27 occurrences
Is already deprecated.
Url::toString() — 194 occurrences
- test assertions (e.g. \Drupal\system\Tests\Common\fully_qualified_local_url) -> add $this->url() & $this->link() to TestBase, those helpers would NOT take cacheability metadata into account
- t(…) -> should NOT do the manual ->toString() call, that should be taken care of by t() automatically
- redirects (e.g. return new RedirectResponse($url->toString());); -> solveable, see the point in the conclusion about RedirectResponse
- HTML HEAD link/feed (e.g. core/modules/content_translation/content_translation.module:561, \Drupal\views\Plugin\views\style\build) -> special case the 'href' attribute in _drupal_add_html_head_link() to only accept Url objects
- links printing the URL or path as link text (e.g. core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php:188)
- entity API validation (e.g. core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraintValidator.php:51) -> does not need any cacheability metadata
- Any #theme callback/template that accepts a URL (e.g. #theme => feed_icon's #url, concrete example: core/modules/views/src/Plugin/views/style/Opml.php:46) should pass the Url object, and not the string; TwigExtension::renderVar() already automatically casts that to a string and therefore bubbles cacheability metadata
- Advanced use cases, for example in Views, where we assign a Url to an object that is passed to a template, whose preprocess function extracts the object property into a seprate variable, causing Twig to render it individually, and hence it is safe: core/modules/views/src/Plugin/views/row/RssFields.php:149, another example of this can be found in Search: \Drupal\search_extra_type\Plugin\Search\SearchExtraTypeSearch::execute()
- statistics_node_view() and views_views_pre_render() are passing a URL to JavaScript code via drupalSettings; hence it technically needs to associate cacheability metadata, but *actually* they should not be sent at all; the needed URLs can be generated on the client side already using Drupal.url()
- Views' custom "active link" handling in template_preprocess_views_view_summary()
- $form['#action'] should support Url objects (e.g. core/modules/views/src/Form/ViewsExposedForm.php:119)
Url::toRenderArray() — 1 occurrence
Remove the sole usage & mark as deprecated.
LinkGenerator::generate() — 28 occurrences
Should be internal.
LinkGenerator::generateFromLink() — 3 occurrences
Should be internal.
LinkGeneratorTrait $this->l() -- 26 occurrences
- t(…)
- $this->l() that should actually be a render array (#type => link …), e.g. core/modules/book/src/Controller/BookController.php:94, \Drupal\dblog\Controller\DbLogController::eventDetails, etc.
- #theme => item_list's #items should support Url (e.g. \Drupal\help\Controller\HelpController::helpLinksAsList)
UrlGeneratorTrait $this->url() -- 50 occurrences
- t(…)
- $this->url() called to generate a destination query string (e.g. core/modules/comment/src/CommentManager.php:170, core/modules/block_content/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php:31)
- $form['#action'] (e.g. \Drupal\comment\CommentForm::form)
- redirect
Entity::url() and Entity::link() loose cacheability metadata
They should either be deprecated and trigger an error
……………
The original reason for introducing \Drupal::url()
See https://www.drupal.org/node/2078285#comment-7819847
- * t('Visit the content types page', array('url' => \Drupal::urlGenerator()->generate('node_overview_types')));
+ * t('Visit the content types page', array('@url' => \Drupal::url('node_overview_types')));
Proposed:
- * t('Visit the content types page', array('@url' => \Drupal::url('node_overview_types')));
+ * t('Visit the content types page', array('@url' => Url::fromRoute('node_overview_types'))); 1
Conclusions/notes
-> we can provide our own RedirectResponse class that does accept Url objects
-> It's either painful to fix now, or painful for the entire D8 cycle.
Key problems:
- far too many ways to generate URLs and links
- all of them need to support the gathering/bubbling of cacheability metadata (see #2335661: Outbound path & route processors must specify cacheability metadata)
Proposed resolution
- Use URL objects as much as possible
- Ensure that all our APIs support URL objects
- Mark the following bits as internal:
UrlGenerator->generateFromRoute()
LinkGenerator->generate(), LinkGenerator->generateFromUrl() - Mark the following bits as deprecated:
UrlGenerator::generate()
Entity->url()
Entity->link()
and throw notices - We throw an exception in ::generate() in case we return a string in a inside a render context.
- For link objects add toString() so its renderable in twig
- Add Link::toRenderArray() to support using Link objects
Remaining tasks
TBD
User interface changes
None.
API changes
Less APIs!
Comments
Comment #1
wim leersMy rough POV for an ideal solution:
Link,LinkGeneratorand\Drupal::l(). Links are markup. Yes, very little markup, but still markup. Markup is the realm of render arrays. Render arrays have the built-in ability to bubble cacheability metadata. So, always use#type => link.\Drupal::url(), it only exists for legacy reasons, and 80–90% of the callers in core are 1) tests, 2)hook_help()implementations.Url::toString()andUrl::toRenderArray()i.e. make it a pure value object again.UrlGenerator::generate()without complying with the Symfony interface, because it doesn't make sense for Drupal to only have one option (absolute or relative). And have that always return an object that includes the cacheability metadataThen we are left with one way to generate a URL, and one way only.
This is probably not realistic, but would make things infinitely easier to understand. Because for the sake of BC and simplicity, we have actually been making things more and more complex.
Comment #2
dawehnerWell, Url::toString() was chosen in order to make it possible to not have to worry about the url generator or the unrouted url assembler.
The link value object is valueable, I would not remove that. Links are not just markup, things like breadcrumb builders should return a list of links, in
which case a value object works perfectly here.
+1!
Well, getting rid of the parent interface will be tricky, given that RouterInterface forces you to inject one.
Comment #3
fabianx commentedEven if we keep \Drupal::l() it could just call \Drupal::service('renderer')->render($render_array_with_type_link). Together with the rest of the plan, that would also keep sanity.
For Urls I think the best is - as Crell said - to have a UrlRenderer as the single mechanism that has both the UrlGenerator and the Renderer injected - and use that to 'render' urls to a String.
Comment #4
webchickPlease do not make me write "\Drupal::service('renderer')->render($render_array_with_type_link)" to print a simple link. :)
Comment #5
wim leersWhen would you even need to do "print a link"? Either you do it in Twig templates, using
{{ link('Title', 'route:foobar') }}, or you do it in a render array, and you write['#type' => 'link', '#title' => 'Title', '#url' => Url::fromUri('route:foobar')]. You don't even need to call the renderer manually.Comment #6
webchickWell, except we obviously do:
versus:
But fair point that best practice in D8 is definitely to use render arrays. But core is apparently setting a very bad example of that currently.
Comment #7
webchickAlso cross-linking a few things.
Comment #8
webchickAnd another.
Comment #9
wim leersThe picture changes significantly if you exclude test code:
Plus, I don't know how to grep for it, but the majority of
\Drupal::url()calls are inhook_help(). Imagine that allhook_help()implementations actually lived in Twig files. Then not only would we remove a fair chunk of markup from our PHP files, we'd also be able to use Twig'surl()orlink(), both of which don't suffer from these problems.Comment #10
wim leersComment #11
dawehnerComment #12
tim.plunkett"Url::to" will only include documentation, it is ->toString() on a Url object, which isn't always constructed on that line...
So that's only a subset of them.
Comment #13
Crell commentedQuote the webchick:
Yes, which is a large part of the problem. :-) That is largely due to the url-generating process evolving over the past 4 years, and full metadata actually having significant value only coming in rather late. (Also, the eternal "I don't know how far we'll be able to push it" problem.) Given the current state of things, though, I would agree with Wim that the primary way of dealing with links SHOULD be via value objects at this point, as those can encapsulate metadata better than any of the other options. Those value objects can be tossed into render arrays fairly easily, which is what you should do in the majority case.
The Generator et al still exist, and will continue to exist, but become internal implementation details of rendering. You'll rarely touch them yourself, except perhaps for creating redirect responses or other places where you're interacting with HTTP directly rather than through HTML. (If you're doing HTML, you want to use our mechanisms.)
Getting rid of hook_help() will make this problem space FAR easier, as well as the dozen or so other advantages of killing it. (One of which being that we can use the line "the last remnants of Drupal 3 have been swept away.")
Comment #14
jibranIsn't that ship sailed already? #1918856: Put each module's help into a separate Twig file, #2188753: Get rid of hard-coded markup in hook_help() implementations and #2351991: Add a config entity for a configurable, topic-based help system
Comment #15
bill richardson commentedInstead of hook_help() topic pages at all within the site, why not just have an external link to the information on the module that already exists on drupal.org ? --- This would make the information more easily updated and available in other translated languages.
Comment #16
xanoIn which contexts? I did a search through my D8 contrib code to look for potential use cases and only once did I use
Url::toString(), which is done to return a SymfonyRedirectResponse. As Wim said, in most cases code should build a render array rather than render a URL on the spot.Comment #17
dawehnerThere are gazillions of generic code in Drupal 8 which don't want to have to deal with the difference between being an internal URL vs. an external URL, so its encoded as being a Url object.
Comment #18
xanoThere are other ways to abstract out that logic than to add logic and dependencies to
Url. A generic link/URL renderer can internally dispatch calls to the correct URL handlers, so most code just uses Url objects and inserts them into render arrays.Comment #19
fabianx commentedMy perfect URL interface:
- \Drupal::url() (cfr. Drupal 7's url())
- \Drupal::l() (cfr. Drupal 7's l())
using e.g. twig inline templates via
- Url::render() - returns render array, for twig
- Link::render() - returns #type => link render array, for twig
( need to extend TwigExtension::renderVar to check for RenderableInterface )
Both should be used primarily within twig.
Leave:
- Url::__toString()
- Link::__toString()
but push any metadata this would generate to the current render context.
Deprecate:
- UrlGenerator::generate() (Symfony's interface, only able to opt in to absolute links, all other options lost)
- UrlGenerator::generateFromRoute() (the recommended way)
- UrlGenerator::generateFromPath() (deprecated)
- LinkGenerator::generate()
- LinkGenerator::generateFromLink() (to pass in Link objects)
- Url::toString()
- Url::toRenderArray()
- Link::toString()
Comment #20
Crell commentedFabian: No, using Drupal:: is not the preferred interface for anything. I'm hoping we can remove it entirely in Drupal 9. It's a BC shim for procedural code only.
The Url and Link objects are, I would argue, the right way to approach it. Value objects for complex values.
Comment #21
wim leers@dawehner pointed out there's two more.
Comment #22
dawehnerThis is the result of some research of @plach, @Wim Leers and @dawehner
Comment #23
tim.plunkettYeah not okay with the phrasing of the "remaining tasks".
Comment #24
timmillwoodI came across this issue after in this comment @Crell steered me towards not using
Drupal::l()and using a render array instead.Thus replacing:
with something like:
I'm not sure we should be removing ways to do things, but better documenting stuff, especially with
@deprecatedwould be good.Comment #25
wim leers#24: related to *exactly* that: #2529560: Expand support for link objects.
(Just opened yesterday, so wasn't yet in the related issues.)
Comment #26
jepster_So what's now the "right" and "not deprecated" way to create a link?
Comment #27
wim leers#26: see the change record associated with this issue: https://www.drupal.org/node/2614344
Comment #28
wim leersI think we can now actually close this. @dawehner, @tim.plunkett et al, what do you think?
Comment #29
bojanz commentedThat change record is incomplete, it says other methods have been deprecated, but doesn't give the new recommendation (other than the entity example).
We should expand it to give an example of the Link and Url object usage.
Comment #30
jepster_+1 for #29
Comment #31
ujin commentedI was need to create html to put in #suffix so I did next
Comment #33
ajay.singh commented1) Can i use summary field as meta description and call call on the head of .twig file??
2) If yes, what will be the proper syntax to call summary field in .twig file?
Since i have page.twig and html.twig with me..
3) Also how can we make block call in internal basic pages in drupal 8?
Please reply me as earliest..
Comment #34
roderik#29 yes, the change record is incomplete. Inspiration can be gotten from https://dev.acquia.com/blog/coding-drupal-8/generating-urls-and-redirect... .
I would try to draft it myself if I had a full picture but there's at least one thing I find confusing:
* On one hand, I (Drupal dev who's dabbling in core contribution but never keeps up with everything that's happening) have been told by a zillion articles to Inject All The Things. So, great. As far as I can see, the url_generator service / UrlGeneratorInterface is what I should be injecting into all my services/plugins/components.
* but wait! UrlGeneratorInterface::generateFromRoute() is marked as internal so apparently I shouldn't use it in my components. Apparently Url::fromRoute(ROUTE)->toString() is the replacement to get a link in string form. (And AFAICT there is no alternative to 'string form' for at least doing redirects.)
* but but but wasn't I supposed to Inject All The Things and isn't Url::someStaticMethod() just a glorified 'global function' that is 'bad' to call in my components? Confusion!
What gives?
Comment #35
cilefen commentedComment #36
pwolanin commented@roderik The Url class is basically a value object, so in that sense doesn't need to be injected. It's only when you render it to a string that the UrlGenerator dependency is pulled in. But you basically shouldn't be rendering it yourself - use a link render array.
Comment #37
roderik@pwolanin thanks for the explanation and @cilefen for the link - it sounds perfectly reasonable now I'm tracing the code and the discussion in the other issue. But it's not a thought pattern I had internalized yet, so couldn't get there myself.
(So now to find time for that change record...)
Comment #39
dawehnerHere is one subissue which cleans up URL generating for entities: #2818625: Add a Url::fromEntity() method
Comment #41
anavarreComment #46
krzysztof domańskiComment #48
wim leers#3069696: Remove BC layers from the entity system will help reduce the pain here a fair amount :)
Comment #50
dpiDrupal 8 and 9 now have too many ways to generate URLs and links.
Comment #51
catchOne variant we can remove without too much trouble: #3151019: Only allow route names, deprecate support for route objects in UrlGenerator methods
Comment #52
catch#3151017: Deprecate UrlGenerator::supports() and UrlGenerator::getRouteDebugMessage() and #3151019: Only allow route names, deprecate support for route objects in UrlGenerator methods both ready for review - simplifies UrlGenerator a bit.
Comment #58
andypostAll child issues and related ones are closed, can we close it?
Comment #59
andypostLooks one of related #2646744: \Drupal\Core\Url does not accept root-relative (file) URLs, making it impossible to let LinkGenerator create root-relative file URL links
Comment #60
andypostAnd one more #3087434: [meta] Use FileUrlGenerator::generate() everywhere, then deprecate generateString() and generateAbsoluteString()
Comment #61
catchI think we still might need issues opening for these:
Comment #62
andypostThank @catch
Filed for #63.1 - it needs opinions about
Url::renderAccess()which is used in contrib but not in code after #3342975: Deprecate Url::toRenderArray() and Url::renderAccess()and for #63.3 is internal already and only 1 usage #3342991: Deprecate LinkGeneratorInterface::generateFromLink() but it replacable with
generate()Comment #63
andypostThe
generate()is already internal via #2606390: mark \Drupal\Core\Utility\LinkGeneratorInterface::generate as internalSo we can only deprecate
generateFromLink()via #3342991: Deprecate LinkGeneratorInterface::generateFromLink() as it also internal since #2606392: mark \Drupal\Core\Utility\LinkGeneratorInterface::generatFromLink as internalComment #64
andypostAlso linked child issue #2819197: Generated url cache context is not correct for relative urls from the UrlGenerator
Comment #65
catchLooking through the issue summary again, we might need one more issue (if it doesn't already exist):
And maybe for this bit:
Comment #66
andypostBoth
::url()and::link()on entities are already replaced by::toUrl()andtoLink()in #2606398: Add EntityInterface::toUrl() and EntityInterface::toLink() and mark urlInfo(), url() and link() as deprecatedAs I see
generate()always returnsGeneratedLinkso it should always have cachability metadataThere's already
toString()and\Drupal\Core\Link::toRenderable()so not sureLink::toRenderArray()is neededThe remaining is #3342975: Deprecate Url::toRenderArray() and Url::renderAccess()
Comment #67
catchThanks @andypost! Given #3342975: Deprecate Url::toRenderArray() and Url::renderAccess() is RTBC I think we can close this now!
Tried to add issue credit for triage, but a lot of comments here and it's hard for meta/policy issues, so might not be perfect.
Comment #68
wim leersAwesome! 🤩
Thank you, @andypost! 😊
Comment #69
andypostAwesome! Now API looks polished, would be great to start add types to arguments and clean-up routing leftovers
@Wim Leers please comment/help with remaining related bug #2819197: Generated url cache context is not correct for relative urls from the UrlGenerator
Comment #71
andypostThe last one commited! #3342975: Deprecate Url::toRenderArray() and Url::renderAccess()
Comment #72
catchLate contender #3271570: Rendering the URL part of a link field in a Twig template should work equally for external, internal relative paths, and internal system paths.