Updated: Comment #24

Problem/Motivation

In general we want to get rid of using paths but switch to route names and route parameters to generate URLs. The url generator
defined by symfony defines a method called generate which does not deals with extra options like query, fragment, language or manually set https.

Proposed resolution

  • Define a new interface called \Drupal\Core\Routing\UrlGeneratorInterface which extends the one from symfony
  • Add a new method called generateFromRoute which accepts $options
  • Merge the current PathBasedGeneratorInterface into the new interface
  • Make the new Drupal UrlGeneratorInterface extend the symfony VersatileGeneratorInterface so we can insure all expected methods are present

Remaining tasks

Potentially a benchmark.

API changes

New interface etc., see Proposed solution.

TODO

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Damien Tournoud’s picture

A different solution would have been to hack around the generate method to support a options array, but this will cause issues with understanding it.

I would rather see that happening.

pwolanin’s picture

Have been having a long conversation about this w/ dawehner and before him with alexpott in IRC.

For me, hacking the 3rd param to generate() would be ok. alexpott didn't like it.

dawehner points out that in Symfony\Cmf they are already hacking/overriding the meaning of the params to generate() in ContentAwareGenerator, so we would be in less violation of the interface than that code.

pwolanin’s picture

Title: Provide a generateWithOptions method on the url generator » Update the generate() method on the url generator to accept options like url
Issue tags: +WSCCI

changing title

pwolanin’s picture

Status: Active » Needs review
FileSize
8.02 KB

This adds an overriding interface, marks some things deprecated, and tweaks url() to take path as an array

dawehner’s picture

+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.phpundefined
@@ -156,19 +156,41 @@ protected function getInternalPathFromRoute(SymfonyRoute $route, $parameters = a
+    // Symfony adds any parameters that are not path slugs as query strings.
+    if (isset($options['query']) && is_array($options['query'])) {
+      $parameters = (array) $parameters + $options['query'];
+    }

So we should not support $options['query'] on the longrun, but tell people to use parameters instead?

pwolanin’s picture

@dawehner - yes, probably so unless we want to handle the query string independently to avoid the Symfony feature(?) that seems to prevent you from having a path slug and query parameter with the same name.

tstoeckler’s picture

As @pwolanin pointed out in IRC (per @dawehner) the 'query' stuff is just insane. There's no way to generate the URL http:example.com/node/1?node=foo, for example.

On a more general note: If we're adding a separate interface anyway, what exactly is the benefit of hacking the existing function instead of providing a new one. I might be missing something but I literally see no benefit of that. Avoiding the 'query' madness would be only a nice side-effect of that.

pwolanin’s picture

Title: Update the generate() method on the url generator to accept options like url » Add a generateFromRoute() method on the url generator to accept options like url()
Issue tags: +Stalking Crell
FileSize
37.61 KB
36.88 KB

Patch changes direction a little based on converstation with Crell & msonnabaum in IRC. Let's not blow up the exisintg method signature, but add a new method.

From Crell, we are trying to meet the basic requirements:

  1. Given a route name, generate a domain-relative URL. Must include query parameters and fragments, and include outbound path manipulation (eg, for aliases and language prefixing).
  2. Given a route name, generate an absolute URI. Must include query parameters and fragments, and include outbound path manipulation (eg, for aliases and language prefixing)
  3. Given a URI (absolute or otherwise) generate an <a> element for it (containing various sytem-controlled attributes in the element).

The last would be handled by using this new code with a link generator for routes.

dawehner’s picture

+1 for having a proper method to support options!

+++ b/core/includes/common.incundefined
@@ -1268,12 +1267,14 @@ function drupal_http_header_attributes(array $attributes = array()) {
+ * @param array|string $path
+ *   An array containing a route name and an array of parameters or an external
+ *   URL being linked to such as "http://example.com/foo". System paths such as
+ *   "node/34" are still supported, but their use is deprecated. After the url()
+ *   function is called to construct the URL from $path and $options, the
+ *   resulting URL is passed through check_plain() before it is inserted into
+ *   the HTML anchor tag, to ensure well-formed HTML. See url() for more
+ *   information and notes.

+++ b/core/lib/Drupal/Core/Routing/GeneratorWithOptionsInterface.phpundefined
@@ -0,0 +1,154 @@
+   * @param $path
+   *   (optional) The internal path or external URL being linked to, such as
+   *   "node/34" or "http://example.com/foo".

These changes should kind of be synced. Additional we nee d it on both l and url, right?

+++ b/core/lib/Drupal/Core/Routing/GeneratorWithOptionsInterface.phpundefined
@@ -0,0 +1,154 @@
+ * Contains Drupal\Core\Routing\GeneratorWithOptionsInterface.

It feels like UrlGeneratorWithOptionsInterface would describe it better, what this interface is about. "Generator" in the context of routing has the potential for misunderstanding.

+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.phpundefined
@@ -87,7 +87,7 @@ public function __construct(RouteProviderInterface $provider, OutboundPathProces
-   * Implements \Drupal\Core\Routing\PathBasedGeneratorInterface::setRequest().
+   * Implements \Drupal\Core\Routing\GeneratorWithOptionsInterface::setRequest().

@@ -179,11 +203,11 @@ public function generate($name, $parameters = array(), $absolute = FALSE) {
-   * Implements \Drupal\Core\Routing\PathBasedGeneratorInterface::generateFromPath().
+   * Implements \Drupal\Core\Routing\GeneratorWithOptionsInterface::generateFromPath().

@@ -329,21 +356,21 @@ public function generateFromPath($path = NULL, $options = array()) {
-   * Implements \Drupal\Core\Routing\PathBasedGeneratorInterface::setBaseUrl().
+   * Implements \Drupal\Core\Routing\GeneratorWithOptionsInterface::setBaseUrl().
...
-   * Implements \Drupal\Core\Routing\PathBasedGeneratorInterface::setBasePath().
+   * Implements \Drupal\Core\Routing\GeneratorWithOptionsInterface::setBasePath().
...
-   * Implements \Drupal\Core\Routing\PathBasedGeneratorInterface::setScriptPath().
+   * Implements \Drupal\Core\Routing\GeneratorWithOptionsInterface::setScriptPath().

I guess we should start to use @inheritdoc directly?

+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.phpundefined
@@ -157,18 +157,42 @@ protected function getInternalPathFromRoute(SymfonyRoute $route, $parameters = a
+    if (isset($options['query']) && is_array($options['query'])) {
+      $parameters = (array) $parameters + $options['query'];
+    }
...
+    $fragment = '';
+    if (isset($options['fragment'])) {
+      if (($fragment = trim($options['fragment'])) != '') {
+        $fragment = '#' . $fragment;
+      }

We certainly have to test this new functionality. Peter, are you okay with helping on the tests?

pwolanin’s picture

The doxygen change to l() will be reverted in the next patch - that was left-over from the rejected changed to url()

pwolanin’s picture

Adds tests and moves the interface name to UrlGeneratorWithOptionsInterface

Also, per discussion in IRC with dawhener and Crell, removes $parameters from being passed to getRouteByName() since the neither the Drupal implementation nor any symfony implementation look at them, they make no sesne being there in the interface (see getRoutesByName()) and they make the test mocking harder.

dawehner’s picture

I prefer the new name, as it is more descriptive.

+++ b/core/includes/common.incundefined
@@ -447,7 +447,6 @@ function drupal_get_query_parameters(array $query = NULL, array $exclude = array
- * @see \Drupal\Core\Routing\PathBasedGeneratorInterface::httpBuildQuery()

oh right, this method was removed some time ago.

+++ b/core/lib/Drupal/Core/Routing/UrlGeneratorWithOptionsInterface.phpundefined
@@ -0,0 +1,154 @@
+ * Contains Drupal\Core\Routing\UrlGeneratorWithOptionsInterface.

Sorry, but this needs an starting "\"

+++ b/core/lib/Drupal/Core/Routing/UrlGeneratorWithOptionsInterface.phpundefined
@@ -0,0 +1,154 @@
+   * @param $path
+   *   (optional) The internal path or external URL being linked to, such as
+   *   "node/34" or "http://example.com/foo".
...
+   * @param $options
+   *   (optional) An associative array of additional options.
...
+   * @return
+   *   A string containing a URL to the given path.
...
+   * @deprecated since version 8.0
+   *   System paths should not be used - use route names and parameters.

We could just fix the types here

+++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.phpundefined
@@ -156,6 +168,9 @@ public function testAliasGeneration() {
     $url = $this->generator->generate('test_1');
     $this->assertEquals('/hello/world', $url);
 
+    $url = $this->generator->generateFromRoute('test_1');
+    $this->assertEquals('/hello/world', $url);

This is just a copy and paste for no real reason ...

+++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.phpundefined
@@ -177,6 +192,17 @@ public function testAliasGenerationWithParameters() {
+    $this->assertEquals('/hello/world?zoo=5#top', $url, 'Correct URL generated including alias and options.');
+    $options = array('query' => array('page' => '1'), 'fragment' => 'bottom');
...
+    $this->assertEquals('/goodbye/cruel/world?page=1#bottom', $url, 'Correct URL generated including alias and options.');
+    // Changing the parameters, the route still matches but there is no alias.
...
+    $this->assertEquals('/test/two/7?page=1#bottom', $url, 'Correct URL generated including no alias and options.');

Lets add some readability by more empty lines. Note (even the other tests are wrong), the message is just shown, when something fails. Please revert the logic of the messages.

One potential follow up is to extend the test coverage to external URLs, which is not covered yet.

pwolanin’s picture

@dawehner - the generator doesn't handle external URLs, only absolute. the link generator would do that.

Other fixes made as suggested.

katbailey’s picture

This looks pretty reasonable to me - can we not just call the interface UrlGeneratorInterface though? I mean, it's a different namespace, who cares that it supports options?

Also noticed some things about the test changes...

+++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.phpundefined
@@ -65,7 +72,7 @@ function setUp() {
     // getRoutesByNames() method calls on the route provider. The parameters
-    // passed in will be slightly different but based on the same information.
+    // are not passed in and default to an empty array.

This sentence was referring to the fact that the params for getRouteByName and getRoutesByNames are slightly different (i.e. one takes a single name and the other takes an array of names) - it was not referring to the "parameters" param. And if we don't need the "parameters" param any more, let's not include it in the $return_map_values array and just pass an empty array in the foreach loop.

As for the "Incorrect URL generated" messages, it's not clear to me that they're adding any value at all - let's just leave them out?

dawehner’s picture

The parameters will be marked as deprecated in CMF 1.2, see https://github.com/symfony-cmf/Routing/issues/78#issuecomment-22094967

As for the "Incorrect URL generated" messages, it's not clear to me that they're adding any value at all - let's just leave them out?

+1 for not adding messages all over the place.

dawehner’s picture

This looks pretty reasonable to me - can we not just call the interface UrlGeneratorInterface though? I mean, it's a different namespace, who cares that it supports options?

We seem to do that in quite some other places in core already.

pwolanin’s picture

Fine for me to use the same interface name if it's not confusing?

dawehner’s picture

I don't think that anyone will be confused when there are namespaces involved.

pwolanin’s picture

updated per suggestions

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Thank you!

dawehner’s picture

Maybe this also needs some profiling.

pwolanin’s picture

It does need a simple DB query if the route has not been previously loaded - to the extent that we are using this in combination with access checks for rendering, I think it will be net faster.

pwolanin’s picture

FileSize
37.13 KB

re-roll for conflicts due to a change in ShortcutSetController.php

alexpott’s picture

Tagging since this issue is changing an API and for the fact that this needs to be documented in the issue summary

dawehner’s picture

Done.

dawehner’s picture

FileSize
9.35 KB
48.13 KB

Alex wondered why @inheritdoc is not used in some places, so let's inheritdoc in the places where we change things

dawehner’s picture

Issue summary: View changes

add issue summary.

pwolanin’s picture

Priority: Normal » Major
Issue tags: -Needs issue summary update

The summary has been updated, and this change would make it a lot easier to finish #2047619: Add a link generator service for route-based links , so bumping to major

andypost’s picture

Suppose a bit more flexibility is cheap

+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -23,7 +23,7 @@
+class UrlGenerator extends ProviderBasedGenerator implements UrlGeneratorInterface {

@@ -157,18 +157,41 @@ protected function getInternalPathFromRoute(SymfonyRoute $route, $parameters = a
+  public function generateFromRoute($name, $parameters = array(), $options = array()) {
...
+    $path = $this->processPath($path, $options);

suppose better to make this optional to allow apply only static route list

dawehner’s picture

@andypost
This is a good idea for a followup: #2070177: Allow to skip the the path processing in the url generator

dawehner’s picture

pwolanin’s picture

Issue tags: -API change, -WSCCI, -Stalking Crell

#26: routing-2057155-26.patch queued for re-testing.

Status: Reviewed & tested by the community » Needs work
Issue tags: +API change, +WSCCI, +Stalking Crell

The last submitted patch, routing-2057155-26.patch, failed testing.

pwolanin’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
47.78 KB

Re-roll for conflicts in 2 form classes where use statements and constructors changed.

pwolanin’s picture

only "Stalking Crell" for a +1 - I don't think we need any technical input.

webchick’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: +Approved API change

I'll admit I don't really understand the "bigger picture" that this is all leading to yet (despite pwolanin patiently trying to explain it to me; I think I might get it the third or fourth time :)), and I'm extremely concerned that it's going to push an initiative already wildly behind schedule even further behind — we need to be *very* critical about what we deem actual release-blocking issues at this point when we are adding far more of them per week than we are fixing, as is the current state of things.

However, this change itself is fairly straight-forward, so I don't think there's much harm in allowing it in; it simply makes this existing code consistent with the url() function. Tagging as an approved API change.

Here's my review:

+++ b/core/includes/common.inc
@@ -1192,7 +1191,7 @@ function datetime_default_format_type() {
- * @see \Drupal\Core\Routing\PathBasedGeneratorInterface::generateFromPath().
+ * @see \Drupal\Core\Routing\UrlGeneratorInterface::generateFromPath().

Most of the patch is this.

I asked Peter about the nature of this change; basically, because the inheritance hierarchy shifts to extending from Symfony CMF's "VersatileGeneratorInterface" (silliest. name. ever.) which itself descends from *their* version of UrlGeneratorInterface, it makes sense to rename this, because it's effectively our own version of theirs. (I also think it's confusing to have two classes named the same thing that do different things, but as has been pointed out in other issues, namespaces are the differentiator there.)

It shouldn't affect module developers too badly, although I see it does require changes on some entity listings.

+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -108,7 +108,7 @@ public function setRequest(Request $request) {
-    $route = $this->getRoute($name, $parameters);
+    $route = $this->getRoute($name);

I inquired about this too; $parameters isn't used and is deprecated upstream, so we're just bringing our code inline.

+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -157,18 +157,41 @@ protected function getInternalPathFromRoute(SymfonyRoute $route, $parameters = a
+  public function generateFromRoute($name, $parameters = array(), $options = array()) {

+++ b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
@@ -0,0 +1,201 @@
+   * @param array  $parameters
+   *   An associative array of parameter names and values.
+   * @param array $options
+   *   (optional) An associative array of additional options, with the following
+   *   elements:
+   *   - 'query': An array of query key/value-pairs (without any URL-encoding)

$parameters and $options are very close synonyms, so initially I pushed back on this. But $parameters comes from Symfony CMF parlance (meaning either path slugs or query string arguments, as described below), and $options is the standard thing we use in Drupal to say "optional things". So I think this is ok.

+++ b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
@@ -0,0 +1,201 @@
+/**
+ * Defines an interface for generating a url from a route (or path) with
+ * additional options not present in the base generator.
+ */

(minor) This needs to be only one line long. Fixed in commit.

+++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php
@@ -65,33 +72,29 @@ function setUp() {
-      $route_name_return_map[] = array($values['route_name'], $values['parameters'], $values['return']);
-      $routes_names_return_map[] = array(array($values['route_name']), $values['parameters'], $values['return']);
+      $route_name_return_map[] = array($values['route_name'], array(), $values['return']);
+      $routes_names_return_map[] = array(array($values['route_name']), array(), $values['return']);

This change seems kinda weird to me. It seems like if we're getting rid of $parameters, we shouldn't need it in tests anymore ether. I don't really understand this part of the code though, but maybe this needs a follow-up?

Apart from that, not much to complain about.

Committed and pushed to 8.x. Thanks!

pwolanin’s picture

dawehner’s picture

+1 for the change record.

Crell’s picture

Status: Fixed » Reviewed & tested by the community
Issue tags: -Stalking Crell, -Approved API change

/me inserts a +1 based on a read-through of #33.

Crell’s picture

Status: Reviewed & tested by the community » Fixed

Belatedly, it would seem... :-)

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

Anonymous’s picture

Issue summary: View changes

add an item to the solution