Problem/Motivation

Part of #2339219: [meta] Finalize URL generation API (naming, docs, deprecation). url() needs to be deprecated and removed from Drupal 8, because it circumvents the routing system for internal URLs.

Proposed resolution

Convert as many straightforward l() calls as possible.

  • For entities use $entity->urlInfo() to get an url object to this entity. Then use \Drupal::linkGenerator()->generateFromUrl($text, $url); to generate the link
  • For internal links, \Drupal::l() (which takes a route name) is usually the correct replacement in procedural code.
  • Uses of l() for external URLs won't be converted until #2343759: Provide an API function to replace url()/l() for external urls is completed.

Remaining tasks

The patch converts all but 49 uses of l() to the appropriate replacements. Many of the remaining conversions are blocked by one of these issues:
#2345725: Query parameters are not decoded the same as the path portion of a URL
#2344487: Provide special route names for <current> and <none>
#2343759: Provide an API function to replace url()/l() for external urls

In followup issue #2343661: Rename l() to _l() and url() to _url(), and document replacements, we will rename l() to _l() to make the BC break explicit, and mark it deprecated. After that, the remaining uses of _l() will be removed before 8.0.0, in #2343669: Remove _l() and _url().

User interface changes

None.

API changes

None yet (in other issues).

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

xjm’s picture

Issue tags: +Pre-AMS beta sprint
xjm’s picture

Issue tags: +beta blocker
xjm’s picture

Issue tags: +Novice
dawehner’s picture

Issue summary: View changes
Status: Active » Needs review
FileSize
1.44 KB

Adapted the info to include the proper way for entities. Here is a patch which JUST converts 2 instances given an example for both.

dinarcon’s picture

FileSize
13.27 KB
10.86 KB

Some additional changes.

Shall links to 'javascript:void()' use Drupal::l?

dinarcon’s picture

FileSize
13.29 KB
10.87 KB
dinarcon’s picture

Status: Needs work » Needs review
dawehner’s picture

+++ b/core/modules/user/src/RegisterForm.php
@@ -115,7 +115,7 @@ public function save(array $form, FormStateInterface $form_state) {
-    $this->logger('user')->notice('New user: %name %email.', array('%name' => $form_state->getValue('name'), '%email' => '<' . $form_state->getValue('mail') . '>', 'type' => l($this->t('Edit'), 'user/' . $account->id() . '/edit')));
+    $this->logger('user')->notice('New user: %name %email.', array('%name' => $form_state->getValue('name'), '%email' => '<' . $form_state->getValue('mail') . '>', 'type' => \Drupal::l($this->t('Edit'), 'entity.user.edit_form', array('user' => $account->id()))));

+++ b/core/modules/user/src/Tests/UserAdminTest.php
@@ -52,7 +52,7 @@ function testUserAdmin() {
-    $link = l(t('Edit'), "user/" . $user_a->id() . "/edit", array('query' => array('destination' => 'admin/people')));
+    $link = \Drupal::l(t('Edit'), 'entity.user.edit_form', array('user' => $user_a->id()), array('query' => array('destination' => 'admin/people')));

You could use $user->urlInfo('edit-form') again (see initial patch as example

Shall links to 'javascript:void()' use Drupal::l?

Let's keep them for now.

dawehner’s picture

Assigned: Unassigned » dawehner

Working on also making it possible to get a url object from views.

dawehner’s picture

Status: Needs work » Needs review
FileSize
37.79 KB
24.71 KB

Some work.

Wim Leers’s picture

Assigned: dawehner » Wim Leers

Pushing this one further.

Wim Leers’s picture

(While debugging the test failure in Drupal\user\Tests\UserAdminTest.)

Apparently, UrlGenerator is broken, and its test coverage is incomplete, because:

  $user_a = entity_load('user', 1);
  $link = l(t('Edit'), 'user/1/edit', array('query' => array('destination' => 'admin/people')));
  var_dump($link);
  $link = \Drupal::l(t('Edit'), 'entity.user.edit_form', array('user' => $user_a->id()), array('query' => array('destination' => 'admin/people')));
  var_dump($link);
  exit;

prints:

string '<a href="/user/1/edit?destination=admin/people">Edit</a>' (length=56)
string '<a href="/user/1/edit?destination=admin%2Fpeople">Edit</a>' (length=58)

l() calls url() which calls UrlGenerator::generateFromPath(), which uses UrlHelper::buildQuery() to build the query string.
\Drupal::l() calls UrlGenerate::generateFromRoute(), which calls getInternalPathFromRoute(), which calls \Symfony\Component\Routing\Generator\UrlGenerator::doGenerate(), which … of course doesn't call UrlHelper::buildQuery() and therefore explains the difference.

That's a pretty clear regression. But the only ways to fix it are: 1) fix upstream (no time to do that before beta), 2) don't use Symfony's UrlGenerator::doGenerate(), 3) allow the regression for now.

effulgentsia’s picture

Crell’s picture

Wim, can you explain why those are functionally different? Symfony seems to work with links, so is it an actual regression or just "different"?

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Status: Needs work » Needs review
FileSize
40.07 KB
2.13 KB

I have to call it a day now, will continue tomorrow. Didn't get much done, sadly, had other obligations, and debugging these is pretty painful.

Progress report:

  1. Fixed Drupal\toolbar\Tests\ToolbarHookToolbarTest failure.
  2. Drupal\user\Tests\UserAdminTest root cause explained in #15. Indirectly caused by Drupal\user\Plugin\views\field\LinkEdit still rendering using l(), converting that to \Drupal::l() will fix the fail, but the way those links are rendered is … non-trivial … so I will need to step through it with a debugger to find where l() is being called :)
  3. Drupal\taxonomy\Tests\TermTest fails because in TermForm::save(), $term->id() is NULL, i.e. the term didn't get created at all.
Wim Leers’s picture

Crell: because this was done intentionally, for legibility:

// For better readability of paths in query strings, we decode slashes.
$params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
Wim Leers’s picture

Assigned: Unassigned » Wim Leers

Have some unexpected time to push this forward further.

Upchuk’s picture

I did some more conversions. Would have done more but there were some external links + some more complex cases I'd be more comfortable if you took a look at the Wim.

What about the l() with current_path() in them? Can we use the request object to get the current route?

Let me know what else I can do to help.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Status: Needs work » Needs review
FileSize
3.55 KB
50.95 KB

This should bring it down to 1 fail and 1 exception.

(Cross-posted with Upchuk, so applied my interdiff to #22, the numbers above apply to #18.)

dawehner’s picture

FileSize
53.43 KB
15.63 KB

@Wim Leers
Mh actually the UrlGenerator already takes into account that. See UrlGenerator::$decodedChars. Will be worth to debug.

20:2 failures, this is nothing.

Let me work on the converting some of the view ones.

Also haven't done the views bit we talked about because this is kind of a mindfuck.

dawehner’s picture

MEH! :(

dawehner’s picture

FileSize
53.75 KB
10.03 KB

Started with #23 and merged mine in.

dawehner’s picture

Adding a related issue.

Wim Leers’s picture

Assigned: Unassigned » Wim Leers

I'll continue with this one.

Wim Leers’s picture

#24: no, UrlGenerator::decodedChars is used for the path segment only! :(

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
2.28 KB
58.29 KB

This should fix at least 48 of the 107 fails.

Wim Leers’s picture

FileSize
58.32 KB
1.02 KB

This should fix another 35 fails, bringing it down to 24 hopefully.

All the test failures in Shortcut module were related to a single wrong invocation of \Drupal::l() (which is an alias for LinkGenerator::generate()): it was being called with a Url object where a string $route_name was expected. Because PHP doesn't have a string typehint and we don't do explicit checking, this is what happened.
I haven't added a manual type check plus exception yet, because I know that's a bit controversial, but we might want to do that. It took me 25 minutes to find this. With such an exception, it would only have taken a second.

The last submitted patch, 33: l-2343651-33.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 34: l-2343651-34.patch, failed testing.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
FileSize
61.66 KB
3.4 KB

Removed 3 more l() invocations (one was in dead code). Number of failed tests should remain the same.

There are only 3 remaining l() invocations to be converted:

  1. Link::preRenderLink(). That uses l(), but to fix that, we have to fix all code that uses #type => link. Grepping for #href will reveal those.
  2. The "views bit" mentioned in #24: for (operation) links rendered by Views
  3. ViewListBuilder::getDisplayPaths() and ViewUI::renderPreview(), which likely needs deep Views knowledge to be fixed

After that, I think we have no remaining l() calls that can be converted, they all:

  1. use external URLs (most of them in tests, which we could arguably convert to not use any function at all), or
  2. use file URLs, or
  3. use current_path() and hence need #2344487: Provide special route names for <current> and <none>, or
  4. are for listing path aliases, which are impossible to convert to routes

These represent 45 l() invocations. These would have to use the renamed _l() function IMO, as per #2343661: Rename l() to _l() and url() to _url(), and document replacements.

Wim Leers’s picture

Status: Needs work » Needs review

d.o--

Status: Needs review » Needs work

The last submitted patch, 37: l-2343651-37.patch, failed testing.

divesh.kumar’s picture

I tested it and worked fine however its getting failed due to other changes.

tim.plunkett’s picture

tim.plunkett’s picture

Status: Needs work » Needs review
Issue tags: -Novice
FileSize
58.38 KB
2.72 KB

Opened #2345725: Query parameters are not decoded the same as the path portion of a URL and linked in this patch, since we can't convert anything using / in query parameters.

Status: Needs review » Needs work

The last submitted patch, 42: 2343651-l-42.patch, failed testing.

xjm’s picture

A few DX questions/observations:

  1. +++ b/core/modules/views/src/Tests/DefaultViewsTest.php
    @@ -94,7 +94,7 @@ protected function setUp() {
    -      $values['body'][]['value'] = l('Node ' . 1, 'node/' . 1);
    +      $values['body'][]['value'] = \Drupal::l('Node ' . 1, 'entity.node.canonical', ['node' => 1]);
    

    Out of scope here, but I wanted to bring it up: So this is the right change for the API we have. Replacing 'node/1' with 'entity.node.canonical', ['node' => 1] is definitely a bummer, though. It's the sort of little thing that incites mindless rage at Drupal 8.

    How would #2277103: Switch Drupal::l() and LinkGenerator to expect a Url object affect code like this? (Trying to get some insight on whether we should try for or give up on that beta deadline API change.)

  2. +++ b/core/modules/node/node.module
    @@ -197,12 +198,14 @@ function node_entity_view_display_alter(EntityViewDisplayInterface $display, $co
    -function node_title_list($result, $title = NULL) {
    +function node_title_list(StatementInterface $result, $title = NULL) {
    

    Q: What on earth is this used for nowadays? A: Two blocks we still need to convert to views. #2020387: Convert "Active forum topics" block to a View and we don't even seem to have an issue for the statistics block that I could find.

  3. +++ b/core/modules/statistics/statistics.module
    @@ -147,41 +147,6 @@ function statistics_get($nid) {
    -function _statistics_link($path, $width = 35) {
    ...
    -function _statistics_format_item($title, $path) {
    

    Nice.

  4. +++ b/core/modules/tracker/tracker.pages.inc
    @@ -85,7 +85,7 @@ function tracker_page($account = NULL) {
    -        'title' => array('data' => l($node->getTitle(), 'node/' . $node->id()) . ' ' . drupal_render($mark_build)),
    +        'title' => array('data' => \Drupal::linkGenerator()->generateFromUrl($node->getTitle(), $node->urlInfo()) . ' ' . drupal_render($mark_build)),
    

    Why does this use \Drupal::linkGenerator()->generateFromUrl() while the example in the Views test uses \Drupal::l('Node ' . 1, 'entity.node.canonical', ['node' => 1])? What's the reason to use the route name vs. the entity URL object?

    For reference: Entity::urlInfo()

  5. +++ b/core/modules/taxonomy/src/Plugin/views/field/LinkEdit.php
    @@ -68,7 +68,7 @@ public function render(ResultRow $values) {
    -        return l($text, 'taxonomy/term/'. $tid . '/edit', array('query' => drupal_get_destination()));
    +        return \Drupal::l($text, 'entity.taxonomy.edit_form', ['taxonomy_term' => $tid], array('query' => drupal_get_destination()));
    
    +++ b/core/modules/taxonomy/src/TermForm.php
    @@ -134,14 +134,17 @@ public function buildEntity(array $form, FormStateInterface $form_state) {
    +    $link = \Drupal::linkGenerator()->generateFromUrl($this->t('Edit'), $term->urlInfo('edit-form'));
    ...
    -        $this->logger('taxonomy')->notice('Created new term %term.', array('%term' => $term->getName(), 'link' => l($this->t('Edit'), 'taxonomy/term/' . $term->id() . '/edit')));
    +        $this->logger('taxonomy')->notice('Created new term %term.', array('%term' => $term->getName(), 'link' => $link));
    

    Another case where I'm unclear as to why the route vs. entity URL object is used.

So I guess the only thing I'm unclear of is for entities, when it's best practice to use the route name vs. the entity URL method, and why half the patch is one way and half the other. If it were always route names with an $entity->id(), the pattern would be more learnable.

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned
Status: Needs work » Needs review
FileSize
59.06 KB
704 bytes

Here's the fix for that fail.

I think this is enough for one patch, we'll need #2344487: Provide special route names for <current> and <none> and dedicated follow-ups for more complex conversions.

#44.1 This is really only common in tests. Note that it doesn't have a fully loaded node.

4,5 The difference is some places don't have a fully loaded entity. #2277103: Switch Drupal::l() and LinkGenerator to expect a Url object would definitely help the ones that do.

xjm’s picture

@tim.plunkett discussed 4 and 5 in IRC, and contemplated the API addition of an Entity::l() method (or something with a better name) that would return the 90% case of a link to the entity with the entity label as the link text.

I've left additional comments on #2277103: Switch Drupal::l() and LinkGenerator to expect a Url object about that change WRT the above.

I'm comfortable with this going in as it stands, since the meh DX is not introduced by this patch, just surfaced, and l() to entities generally are only a small subset of all the l() calls that we need to worry about. Going to do a local review of #45 and then update the summary here.

xjm’s picture

Assigned: Unassigned » xjm
Related issues: +#2259445: Entity Resource unification
+++ b/core/modules/comment/comment.module
--- a/core/modules/comment/src/CommentForm.php
+++ b/core/modules/comment/src/CommentForm.php

+++ b/core/modules/comment/src/CommentForm.php
@@ -370,7 +370,7 @@ public function save(array $form, FormStateInterface $form_state) {
-      $logger->notice('Comment posted: %subject.', array('%subject' => $comment->getSubject(), 'link' => l(t('View'), 'comment/' . $comment->id(), array('fragment' => 'comment-' . $comment->id()))));
+      $logger->notice('Comment posted: %subject.', array('%subject' => $comment->getSubject(), 'link' => \Drupal::l(t('View'), 'entity.comment.canonical', array('comment' => $comment->id()), array('fragment' => 'comment-' . $comment->id()))));

Per discussion with tim.plunkett, this should use the same entity link generation pattern as above, not the static route name. I asked Tim why using the route name was "wrong" and he said:

xjm: when it goes through the outbound processor, the entity method ensures that ->setOption('entity_type', $this->getEntityTypeId()) ->setOption('entity', $this);
17:51 timplunkett: well it does it internally, so it is available in the outbound processor
17:51 timplunkett: also
17:52 timplunkett: the internal method uses the link templates
17:52 timplunkett: which could be altered to point at other routes
17:52 timplunkett: which is a whole other ball of wax
17:52 timplunkett: in theory, you should never use a route name for an entity manually

I'll reroll with that change.

Also referencing related issue for entity URI templates.

Wim Leers’s picture

17:52 timplunkett: in theory, you should never use a route name for an entity manually

Woah. That implies that we have to fix this all over core, in theory.

It makes sense, but it's a sad realization. 2 levels of abstraction to get an entity path: EntityInterface::urlInfo() to get the route name, the route name to get the path.

tim.plunkett’s picture

xjm’s picture

Attached addresses #47. The log entry has no test coverage so I tested manually and found #2345779: Fix double-escaping due to Twig autoescape in dblog event "operations" (so apparently there's no test coverage for dblog "operations" links whatsoever). However, the double-escaped link is the same in HEAD as the patch, so out of scope. :)

xjm’s picture

+++ b/core/modules/aggregator/src/Controller/AggregatorController.php
@@ -121,7 +121,7 @@ public function adminOverview() {
-      $row[] = l($feed->label(), "aggregator/sources/" . $feed->id());
+      $row[] = \Drupal::linkGenerator()->generateFromUrl($feed->label(), $feed->urlInfo());

+++ b/core/modules/book/src/Controller/BookController.php
@@ -71,8 +71,13 @@ public function adminOverview() {
-        l($book['title'], $book['link_path'], isset($book['options']) ? $book['options'] : array()),
+        \Drupal::linkGenerator()->generateFromUrl($book['title'], $url),

These (and elsewhere) could use $this->getLinkGenerator() instead.

xjm’s picture

Issue summary: View changes
xjm’s picture

Issue summary: View changes
xjm’s picture

FileSize
59.1 KB
5.98 KB

Attached uses the link generator trait in page and form controllers (as per #51).

xjm’s picture

Assigned: xjm » Unassigned

Reviewed locally with git diff --color-words; noticed a couple things:

  1. +++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeFieldItemList.php
    @@ -51,7 +51,7 @@ public function defaultValuesForm(array &$form, FormStateInterface $form_state)
    -          '#description' => t("Describe a time by reference to the current day, like '+90 days' (90 days from the day the field is created) or '+1 Saturday' (the next Saturday). See !strtotime for more details.", array('!strtotime' => l('strtotime', 'http://www.php.net/manual/en/function.strtotime.php'))),
    +          '#description' => t("Describe a time by reference to the current day, like '+90 days' (90 days from the day the field is created) or '+1 Saturday' (the next Saturday). See <a href=\"@url\">@strtotime</a> for more details.", array('@strtotime' => 'strtotime', '@url' => 'http://www.php.net/manual/en/function.strtotime.php')),
    

    This was a misuse of l() to begin with; however, shouldn't the external URL go through whatever we come up with for https://www.drupal.org/node/2343759 instead, or, for now, UrlGenerator::generateFromPath() instead?

  2. +++ b/core/modules/dblog/src/Tests/Views/ViewsIntegrationTest.php
    @@ -40,6 +40,9 @@ class ViewsIntegrationTest extends ViewUnitTestBase {
    @@ -57,12 +60,12 @@ public function testIntegration() {
    
    @@ -57,12 +60,12 @@ public function testIntegration() {
         // Setup a watchdog entry without tokens.
         $entries[] = array(
           'message' => $this->randomMachineName(),
    -      'variables' => array('link' => l('Link', 'node/1')),
    +      'variables' => array('link' => \Drupal::l('Link', '<front>')),
         );
         // Setup a watchdog entry with one token.
         $entries[] = array(
           'message' => '@token1',
    -      'variables' => array('@token1' => $this->randomMachineName(), 'link' => l('Link', 'node/2')),
    +      'variables' => array('@token1' => $this->randomMachineName(), 'link' => \Drupal::l('Link', '<front>')),
         );
         // Setup a watchdog entry with two tokens.
         $entries[] = array(
    @@ -72,7 +75,7 @@ public function testIntegration() {
    
    @@ -72,7 +75,7 @@ public function testIntegration() {
           'variables' => array(
             '@token1' => $this->randomMachineName(),
             '!token2' => $this->randomMachineName(),
    -        'link' => l('<object>Link</object>', 'node/2', array('html' => TRUE)),
    +        'link' => \Drupal::l('<object>Link</object>', '<front>'),
    

    Not sure why we're changing this test to use the front page instead of two particular nodes?

  3. +++ b/core/modules/filter/src/Element/TextFormat.php
    @@ -179,7 +179,7 @@ public static function processFormat(&$element, FormStateInterface $form_state,
    -      '#markup' => l(t('About text formats'), 'filter/tips', array('attributes' => array('target' => '_blank'))),
    +      '#markup' => \Drupal::l(t('About text formats'), 'filter.tips_all', array(), array('attributes' => array('target' => '_blank'))),
    

    Out of scope, but have we considered converting this to D8 modals? :) Or maybe there's too much content for a modal to be usable. (I remember how long it took to get this fix in in the first place...)

Status: Needs review » Needs work

The last submitted patch, 54: 2343651-l-54.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
59.64 KB
822 bytes

Just fixing that, not addressing #55

Wim Leers’s picture

Not sure why we're changing this test to use the front page instead of two particular nodes?

I think I might have done those. Those tests just need some links, it doesn't care links to what. In the new routing system, it's impossible to generate links to nodes without enabling the node module. This test doesn't yet enable the node module, so that'd be slowing the tests down for no reason. Hence I just switched them to other links.

Wim Leers’s picture

Assigned: Unassigned » Wim Leers

Pushing this one further.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
59.65 KB

This comment is an attempt to go over ALL outstanding problems/questions/remarks and answer them, to determine what we still have to do in this issue before it can be RTBC.


In #37, I wrote:

There are only 3 remaining l() invocations to be converted:

  1. Link::preRenderLink(). That uses l(), but to fix that, we have to fix all code that uses #type => link. Grepping for #href will reveal those.
  2. The "views bit" mentioned in #24: for (operation) links rendered by Views
  3. ViewListBuilder::getDisplayPaths() and ViewUI::renderPreview(), which likely needs deep Views knowledge to be fixed
  1. The first of those really can't be converted; all (or at the very least nearly all) occurrences of #href are for external URLs, and hence we need #2343759: Provide an API function to replace url()/l() for external urls to fix those.
  2. The second bit, tim.plunkett "fixed" in #42: it cannot be solved because Symfony's URL generator handles a slash in a query string differently than we did, hence we need #2345725: Query parameters are not decoded the same as the path portion of a URL to fix those.
  3. The third bit can only really be done by a Views maintainer (I think), and the first is a protected function so is totally okay to change after beta, the second is public but is unlikely to ever be used outside of Views, and it only uses l() because ViewExecutable::getUrl() returns a path. So both of these are safe to do at a later point; they won't affect an important public API.

#44.1/#44.4/#44.5 are answered by #45.
#44.2: I wondered the same. And indeed, that code can be deleted altogether once they're views. But for now, we have to keep it around.
#44.3: :)


I'm comfortable with this going in as it stands, since the meh DX is not introduced by this patch, just surfaced

+1


#55.1: I thought this also at first, but… you only need to pass it through some sort of "external URL builder" or "external URL manipulator" or "external URL tweaker", if you're actually manipulating the external URL. If we're really using an external URL verbatim, then there's no point in passing it through a function.
#55.2: answered in #59.


CONCLUSION: This is RTBC.

… except that one of the blocking issues (#2344487: Provide special route names for <current> and <none>) landed in the mean time, so now we can convert those. But to do that, I opened #2346103: Convert url() and l() invocations using current_path() and '', since we have to do it not only for l(), but also url(), which has many more of those cases anyway.

Let's get this committed, just like we got #2340251: Remove most remaining url() calls, which also didn't convert everything, but represented massive progress, just like this patch does.

Attached reroll is only for chasing HEAD, no changes.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
webchick’s picture

Status: Reviewed & tested by the community » Fixed

Nice work! Onward to beta. :)

Committed and pushed to 8.x. Thanks!

  • webchick committed a6492ce on 8.0.x
    Issue #2343651 by xjm, tim.plunkett, Wim Leers, dinarcon, dawehner:...
martin107’s picture

Found a broken $this->l() in core. I guess as they arise they can be Zapped on a case by case basis

Status: Fixed » Closed (fixed)

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