Problem/Motivation

From \Drupal\Core\Entity\Entity::toUrl()

    if (isset($link_templates[$rel])) {
      $route_parameters = $this->urlRouteParameters($rel);
      $route_name = "entity.{$this->entityTypeId}." . str_replace(array('-', 'drupal:'), array('_', ''), $rel);
      $uri = new Url($route_name, $route_parameters);
    }

This assumes that for every link template there is a route available under "entity.ENTITY_TYPE_ID.LINK_TEMPLATE_WITH_UNDERSCORES". However, this requirement is not documented on \Drupal\Core\Entity\EntityType::$links nor on \Drupal\Core\Entity\EntityInterface::toUrl() and will crash your site if not met.

For instance, I had a route called group_type.content and the following link templates:

 *   links = {
 *     "collection" = "/admin/group/types",
 *     "edit-form" = "/admin/group/types/manage/{group_type}",
 *     "delete-form" = "/admin/group/types/manage/{group_type}/delete",
 *     "content-plugins" = "/admin/group/types/manage/{group_type}/content",
 *     "permissions-form" = "/admin/group/types/manage/{group_type}/permissions"
 *   },

When trying to use $entity->toUrl('content-plugins'), my site crashed because I had not defined the route as "entity.group_type.content_plugins"

Steps to reproduce

Proposed resolution

Document the default expected route names on \Drupal\Core\Entity\EntityType::$links or \Drupal\Core\Entity\EntityInterface::toUrl()? See #53

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

kristiaanvandeneynde created an issue. See original summary.

kristiaanvandeneynde’s picture

Issue summary: View changes
kristiaanvandeneynde’s picture

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

Attached is a patch which does the following:

  • The route name is no longer hardcoded into Entity::toUrl() and can be overwritten by custom entity types.
  • There is still no documentation for the assumed route names, but maybe that's no longer required now?
kristiaanvandeneynde’s picture

Title: Entity::toUrl() makes undocumented assumptions » Entity::toUrl() makes undocumented and hardcoded route name assumptions
tstoeckler’s picture

Status: Needs review » Needs work

I think this makes sense, if only to make toUrl() more self-documenting/readable while having overridability and better docs as a side-effect. Thanks for the patch!

Two points.
1. I think the method should be called getRouteName() we generally prefix getter functions with get and this one gets a route name for a specific link relation. It also is not strictly tied to URL objects, so I don't think the URL part is necessary.

2. Per your second point I think it would be great to document the format of the route name either in the method documentation body or as part of the @return documentation. Now that we have a good place to document this behavior (instead of inline in toUrl() or something I think we should.

tstoeckler’s picture

Version: 8.0.x-dev » 8.1.x-dev
Category: Bug report » Task

Also, while this may not be the most intuitive part of Drupal, I can't see how this is a bug, so retargeting for 8.1.x

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
4.15 KB
2.11 KB

I listed this as a bug because it caused my code to break. Having undocumented assumptions in your code is a very gray area that could be classified as a bug, I thought.

Anyways, attached is a patch which:

  1. Renames the method to getRouteName().
  2. Makes the method public, to avoid the same mistake as in #2651974: field_ui_entity_operation() cannot respect route parameters because of incorrectly named routes. People may want to actually use this to find out what the route name for a given link template is.
  3. Therefore specifies the method on the interface.
  4. Documents the default behavior in the method body.

Also: should this be against 8.0.x or 8.1.x? When will 8.1.0 be released because I kind of need this to be fixed ASAP to get my module ready for D8 :)

Status: Needs review » Needs work

The last submitted patch, 7: drupal-entity_route_names-2645136-6.patch, failed testing.

kristiaanvandeneynde’s picture

Some junk snuck into the previous patch. Attached is a rerolled version for 8.1.x.

kristiaanvandeneynde’s picture

Version: 8.1.x-dev » 8.0.x-dev
Category: Task » Bug report
FileSize
2.62 KB

And attached is the correct patch for 8.0.x.

Setting back to bug report vs 8.0.x as:

  • This does not break the API
  • It does break modules whose routes deviate from the expected pattern
  • It's breaking stuff because it is undocumented
  • Setting it to 8.1.x would mean it will only get fixed in 2-3 months

In any case, having a patch for both 8.1.x and 8.0.x will give the entity maintainers a choice :)

The last submitted patch, 9: drupal-entity_route_names-2645136-7.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 10: drupal-entity_route_names-2645136-10-D8.0.x.patch, failed testing.

kristiaanvandeneynde’s picture

ViewUI doesn't implement the base Entity class... :) Updated the patch.

kristiaanvandeneynde’s picture

Title: Entity::toUrl() makes undocumented and hardcoded route name assumptions » Entity::toUrl() makes undocumented and hard-coded route name assumptions
kristiaanvandeneynde’s picture

kristiaanvandeneynde’s picture

Version: 8.0.x-dev » 8.1.x-dev

Seems like most of this type of work gets targeted for 8.1.x so setting back to that as per tstoeckler in #6.

kristiaanvandeneynde’s picture

Status: Needs review » Reviewed & tested by the community

8.1.x will be closed in little over a week (https://groups.drupal.org/node/508968), let's not make this have to wait another couple of months please.

The work being done here is exactly the same as in #2651974: field_ui_entity_operation() cannot respect route parameters because of incorrectly named routes, which has passed several reviewers. Albeit on a different method in the same class.

I addressed all the remarks made by tstoeckler in #5, so RTBC?

tstoeckler’s picture

Status: Reviewed & tested by the community » Needs review

Please don't mark your own patch RTBC. FWIW, I disagree with making the method public. You can do $entity->toUrl()->getRouteName() if you need that information from the outside.

kristiaanvandeneynde’s picture

You can do $entity->toUrl()->getRouteName() if you need that information from the outside.

Which will lead to a crash as per the issue summary. That's the whole issue: If a module does not name its routes entity.MY_ENTITY.LINK_TEMPLATE, then $entity->toUrl() crashes the website.

Unless you're talking about moving forward with this patch, but making the newly introduced method getRouteName() protected?

I marked this issue as RTBC because a D8 module I'm about to release depends on this bug being resolved and I'd hate to have to tell everyone to wait until D8.2.0 comes out before they can properly use it.

The ugly fix I'm using now is this:
http://cgit.drupalcode.org/group/tree/src/Entity/GroupContent.php?h=8.x-...

Berdir’s picture

Status: Needs review » Needs work

Agreed that this doesn't need to be a public API. We have more than enough url related public methods already. A new protected method in the base class is also way less problematic for BC.

Just because you want a feature doesn't mean you can RTBC it yourself.

tstoeckler’s picture

You can do $entity->toUrl()->getRouteName() if you need that information from the outside.

Which will lead to a crash as per the issue summary.

Ahh, yes, sorry. I had forgotten about that. Still agree with #20, though.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
1.98 KB
2.19 KB

I just tried to change the getRouteName() method to be protected, but then ViewsUI crashes. Basically, any new protected method will crash ViewsUI because it wraps all Entity methods as shown below.

+++ b/core/modules/views_ui/src/ViewUI.php
@@ -1034,6 +1034,13 @@ public function toLink($text = NULL, $rel = 'edit-form', array $options = []) {
+  public function getRouteName($rel) {
+    return $this->storage->getRouteName($rel);
+  }

So I fixed that by not putting the method on the interface at all and marking it protected as requested above. Attached is a patch for that.

P.S.:
I'm sorry for marking it as RTBC myself, but I've spent a lot of hours debugging and writing patches for D8 lately and most of the issues have just been sitting there for days or even weeks. With the news of 8.1.x becoming frozen soon, I was getting a bit anxious (or even frustrated) and marked them RTBC to get them moving.

I don't mean to be rude by this or comment on the obviously large amount of time the core maintainers put into the D8 issue queue. It's just that I am close to finishing a very 'powerful' module that pushes D8 to its limits and thus tends to uncover the more obscure bugs. I realize this causes the issue to not pick up as much attention as a very common WSOD bug would.

It's just demotivating to not see your issues moving forward.

kristiaanvandeneynde’s picture

Also, seeing as this one won't be a public method, I'm removing and closing the related issue.

Berdir’s picture

Status: Needs review » Reviewed & tested by the community

Ok, this seems fine to me.

I'm not sure about test coverage. Theoretically, all changes have to be test covered. But the code that's actually being called is just as test-covered as it was before, a unit test that would test this is IMHO too much about the internals and a kernel test or so that would require a new test entity type just to override that methods seems to be a bit overkill to me.

Setting to RTBC, so that a committer can decide on that.

Berdir’s picture

For the record, there is also the argument to be made that this is by design and instead of adding a method to change it, should just be documented properly. I doubt that someone would actually find that documentation if running into that problem, but we could then at least point to it.

There might be use cases for doing the reverse, trying to map a route name to a link template for example. Then it would have to match that pattern for that to work.

Not sure, personally.

dawehner’s picture

Well, we should clearly document that this is simply not supposed to be changed. There are assumptions about the route name in all kind of places, so you really cannot and should not change it

tim.plunkett’s picture

Can we get a better issue title before commit, please?

dawehner’s picture

So yeah IMHO we should document it, but not add the method. Maybe a public static method, that is not replaceable.

tstoeckler’s picture

Just my two cents:
1. I find the method more clear in terms of code organization. To me it is more readable and clearer than any documentation.
2. Pick any methon on (Content)EntityBase and there's about a 50% chance that things massively break if you change even the slightest implementation detail. So I think it would completely overblown to document it on this specific method. One could even argue that that indicates that I can overwrite everything else without things breaking (although that's maybe going a bit too far.)

So I support the RTBC as it stands, but that's just me.

Berdir’s picture

Status: Reviewed & tested by the community » Needs review

K, taking back my RTBC then for now. Before doing this, we'd have to prove that this actually works. @dawehner, can you share some other cases that depend on this assumption?

dawehner’s picture

I find the method more clear in terms of code organization.

I totally agree with that point, naming things properly by putting it into a method really helps.

+    // By default, we expect entity link relationships to have a corresponding
+    // route name in the form of entity.{entity_type_id}.{template_name}

This though indicates that you could override it, but really, this would not work. Examples where this causes issues: DefaultHtmlRouteProvider, entity://node/1 schema inside the \Drupal\Core\Url object.

kristiaanvandeneynde’s picture

A good example of where this would need to be overridden is the Group module: It defines plugins that will get routes defined for them automatically on a GroupContent entity. Because there can be multiple plugins, the routes need to be namespaced to avoid collision.

So it really needs route names to be different from the default hard-coded pattern. For example:
entity.group_content.plugin_a.edit_form
entity.group_content.plugin_b.edit_form

If I were to simply use entity.group_content.{whatever the plugin specifies}, I'd obviously get name collisions real quick. I hope this example makes sense and clarifies why I really think this functionality needs to be in a separate, overridable method instead of being hard-coded in toUrl().

Also, thanks for moving this issue forward!

P.S.: The use case I have in Group is really a great example of how hard-coding the route names can severely limit a module. If you would like me to go into detail, I'll gladly write out a proper summary of why this change is needed.

kristiaanvandeneynde’s picture

I see what dawehner is getting at and actually, it is a clear case why the method I introduced should be public after all.

For example, he states it would break in DefaultHtmlRouteProvider:

  /**
   * {@inheritdoc}
   */
  public function getRoutes(EntityTypeInterface $entity_type) {
    $collection = new RouteCollection();

    $entity_type_id = $entity_type->id();

    if ($edit_route = $this->getEditFormRoute($entity_type)) {
      $collection->add("entity.{$entity_type_id}.edit_form", $edit_route);
    }

    if ($canonical_route = $this->getCanonicalRoute($entity_type)) {
      $collection->add("entity.{$entity_type_id}.canonical", $canonical_route);
    }

    if ($delete_route = $this->getDeleteFormRoute($entity_type)) {
      $collection->add("entity.{$entity_type_id}.delete_form", $delete_route);
    }

    return $collection;
  }

But if we introduced the method as a public function, the above could easily be rewritten along these lines:

  /**
   * {@inheritdoc}
   */
  public function getRoutes(EntityTypeInterface $entity_type) {
    $collection = new RouteCollection();

    $entity = /* create empty entity from $entity_type here */;

    if ($edit_route = $this->getEditFormRoute($entity_type)) {
      $collection->add($entity->getRouteName('edit-form'), $edit_route);
    }

    if ($canonical_route = $this->getCanonicalRoute($entity_type)) {
      $collection->add($entity->getRouteName('canonical'), $canonical_route);
    }

    if ($delete_route = $this->getDeleteFormRoute($entity_type)) {
      $collection->add($entity->getRouteName('delete-form'), $delete_route);
    }

    return $collection;
  }

Keep in mind that the above is off the top of my head. There's probably a case to be made to move the method to the entity type, so the calls would look like $collection->add($entity_type->getRouteName('delete-form'), $delete_route);

Then the Entity base class would call that from within a protected function, much like it does with ::linkTemplates():

  /**
   * Gets an array link templates.
   *
   * @return array
   *   An array of link templates containing paths.
   */
  protected function linkTemplates() {
    return $this->getEntityType()->getLinkTemplates();
  }
Berdir’s picture

DefaultHtmlRouteProvider wouldn't work for you anyway with those routes. And you don't have an object there, so you can't return varying routes.

However, as far as I understand, what you're doing seems to be too complicated. I'd really try to avoid different routes per plugin. Add a a single standardized route per relation, and forward from there to a plugin specific method/form/whatever. You might need a bit more complexity but then all the routing stuff is standard.

kristiaanvandeneynde’s picture

@Berdir, I was just mentioning that code to show that the patch won't necessarily break the code dawehner pointed out.

The use case I have doesn't really allow for "canonical" routes. Let me explain:

Group defines bundle entities called GroupType. On each GroupType, you can enable one or more plugins which allow you to add content to that group type's groups. Each enabled plugin generates a bundle for a GroupContent entity.

Now each of those plugins can define routes for the GroupContent entity. Most of them have the standard 'edit-form', 'canonical', etc. but each of them would preferably get their own path. So instead of group_content/{group_content}/edit, the paths would be group/{group}/member/{group_content}/edit, group/{group}/node/{group_content}/edit, etc.

One of them could also actively choose to opt out of 'delete-form' for instance. If we had "canonical routes" such as entity.group_content.delete_form, that would increase the complexity. But I guess it can be done with an extra access check _has_link_template or something.

Finally, they can each add additional link templates. So suppose two plugins define a link template called "special-stuff", but each plugin has a completely different implementation for that template, we would also get issues if the route name was just entity.group_content.special_stuff.

As you can see, it would be a big improvement to have more flexibility on the entity route names. In my case, giving the plugins their own route names would also increase the flexibility for other modules to alter just the one plugin's routes.

kristiaanvandeneynde’s picture

Just looked into this and I currently have this code in Group:

  protected function getRouteName($rel) {
    $route_prefix = 'entity.group_content.' . str_replace(':', '__', $this->getContentPlugin()->getPluginId());
    return $route_prefix . '.' . str_replace(['-', 'drupal:'], ['_', ''], $rel);
  }

Meaning that if the method got moved to the entity type, it would break my code as well because I can no longer ask an entity what plugin it's using (the entity type doesn't know this without an entity instance).

This sucks, I'm stuck :) I need varying routes because of different paths and permissions per "route clone", but I can't implement it without a crash if it comes to calling Entity::toUrl(). Even with my current implementation, it would still break in the places mentioned by dawehner.

What would you guys do in this case? Try a PathSubscriber to at least make the URLs look pretty?

On topic:
So maybe this issue needs repurposing as mentioned earlier: Just documenting the fact that every link template is expected to have an associated route name "entity.ENTITY_TYPE_ID.TEMPLATE_WITH_UNDERSCORES"? The annotation class for entity types may be a good place to document that?

On the other hand: Still putting the route name logic into its own method would mean I don't have to copy the entire Entity::toUrl() method to change 1 line. I can choose not to use the DefaultHtmlRouteProvider class and Url::fromEntityUri() method for group content entities.

kristiaanvandeneynde’s picture

After further inspection, the places where dawehner mentioned it would break are not really an issue:

  • DefaultHtmlRouteProvider is optional and even mentions in its name that it's the "default". Meaning you can't expect it to cover edge cases like mine and would then have to provide your own routes.
  • \Drupal\Core\Url::fromUri() is arguably bugged, actually, because it expects a "canonical" route to exist without even checking. Any module that would have a reason not to allow one of its entities to be viewed will get a crash if someone calls Url::fromUri('entity:my_unviewable_entity/1')

There could, of course, be other places where the undocumented route naming pattern is used. But right now, I don't see why we couldn't split it off into a protected method that can be overridden by custom entities.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

kristiaanvandeneynde’s picture

Right, I've given this some thought and came to the following conclusion, asking to move forward with the patch from #22.

Fact: Entity types may specify a route provider and the default one core provides is DefaultHtmlRouteProvider
This means both Entity (the base class) and DefaultHtmlRouteProvider may make assumptions regarding the structure of their route names following the entity.ENTITY_TYPE.LINK_TEMPLATE pattern.

Fact: Entity types may choose to write their own route provider and -for whatever reason- could choose not to follow the aforementioned naming pattern. Especially as it isn't documented anywhere.
This should be possible, as a module may have a good reason to not follow said pattern. The patch from #22 would allow said module to be able to use Entity::toUrl() without crashing the site. I could provide several examples of why one may choose to use a different pattern.

Logical conclusion: Any non-route-providing code should not make assumptions on route names but instead ask the entity for a URL and then read the route name from that.
First mentioned by tstoeckler in #18. This means \Drupal\Core\Url::fromEntityUri() is currently bugged and should instead load the entity and read the route name from there. This is possible because it knows both the entity type and the entity ID.

Could this still go into 8.1.0 if I change the patch in #22 to:

  • Add in the change to Url::fromEntityUri()
  • Adjust the new method's documentation to mention:
    1. The route name assumptions are also found in DefaultHtmlRouteProvider
    2. Modules should always use $entity->toUrl()->getRouteName() to figure out the route name when an entity is available

I will update the issue summary if people agree with the above statements.

Crell’s picture

The switch to entity.$entity.$link format was deliberate to ensure consistency so that automation could be built on top of it. That it was not better documented was an oversight, but IMO entities not following that pattern are broken and should be fixed. Allowing entities to do any-old-thing and then trying to support it consistently with other entities is how systems become overly complex and buggy. Standards are your friend, if properly documented and enforced. Let's just get better at documenting and enforcing.

(aka, I do not agree with #39.)

dawehner’s picture

Yeah, I totally am a fan on convention over configuration or convention over an additional level of API. Even if we fix core, contrib/custom code could already easily rely on that intended naming scheme.

kristiaanvandeneynde’s picture

While I can follow the reasoning in #40, I disagree with the reasons stated to not make any changes. I just based a significant part of the rather huge Group module on the idea that we were free to implement custom entity route names. And in a way that makes total sense, too. I'll gladly explain it over IRC/hangouts/skype/slack/whatever if you want.

The thing with enforcing "standard" patterns is that if the pattern appears to be flawed afterwards, you have to fix it in a million places and end up not doing it at all because the task is too daunting. I mean, look at hooks: we still don't know how to fix naming collisions with those but we just ignore it because it only happens once in a blue moon. Yet when it does happen, it completely breaks a site.

So why don't we go ahead and fix this? There is no impact whatsoever for existing code. Old code will still work just fine and people using entity.$entity.$link can keep doing so until the end of times. Just those modules (like Group) that do have valid reasons to deviate from the default pattern should be able to do so.

If any contrib module, or core for that matter, does hardcode the route name pattern in a way that breaks those few modules that deviate from the pattern, we can still file a bug report then asking them to use $entity->toUrl()->getRouteName() instead.

I just spent months working on a solid D8 Organic Groups alternative that was really well received just last Saturday at Drupalcamp London. "Fixing" this issue would really help me out with removing a very ugly Entity::toUrl() overwrite that only exists to allow me to overrule the default naming pattern.

I mean, isn't that what contrib developers are beneficial to the system for? Finding bugs, flaws or DX issues in core while working on a module and then addressing those issues so Drupal can kick even more ass in the next iteration?

tstoeckler’s picture

This means \Drupal\Core\Url::fromEntityUri() is currently bugged and should instead load the entity and read the route name from there. This is possible because it knows both the entity type and the entity ID.

While this is technically possible, this is practically undoable for performance reasons. Even with a menu of a couple hundred entity links this would mean 100 separate load() calls (i.e. not loadMultiple()), which we currently avoid. And even if we had some way to do multiple loading, there are also use-cases of tens of thousands of entity links in a menu, so even then it would be problematic.

See #2648996: Optimize menu link migration for some background information. While the actual symptom there is migration-related, the cause is precisely the huge performance difference between internal:/node/1 and entity:node/1 (because for reasons that are not relevant to this issue the former actually does load the respective entity).

Regarding your actual use-case, I looked at the current code in http://cgit.drupalcode.org/group and if I understand it correctly, the use-case is to have a single relation map to different route-names based on the plugin ID of the group content enabler plugin that is associated to the group content (via its group content type and group type). So while it surely isn't super nice, I think you can do the following:

class GroupContent ... {

  ...

  public function getRelation($pseudo_relation) {
    $plugin_id = str_replace(':', '__', $this->getContentPlugin()->getPluginId());
    // Although I wouldn't advise it, you could try actually using a period ('.') as the
    // separator to keep the exact same route names as currently.
    $separator = '___';
    return $plugin_id . $separator . $pseudo_relation;
  }

  ...

}

// When generating a URL, instead of doing
$group_content->toUrl($rel);
// you do
$group_content->toUrl($group_content->getRelation($url));

That would allow you to work within the existing system. In any case, even if there is some reason you do not want to use that specific approach, this is just to serve as an example that I think you can actually achieve what you're trying to do, without either overriding toUrl() or having it broken.

kristiaanvandeneynde’s picture

@tstoeckler in #43: Thanks for referring to the menu system issue. Even though it doesn't help my cause, it makes total sense to try and gain performance there.

if I understand it correctly, the use-case is to have a single relation map to different route-names based on the plugin ID

That is entirely correct, thanks for taking the time to investigate my use case!

The whole system behind my varying route names allows plugin authors to really customize "their" routes (tabs, local actions, access checks, ...) without having to worry about breaking the route for other plugins. It also helps them get "clean" URLs such as group/1/members and group/1/node/add/article instead of group/1/content/group_membership and group/1/content/add/group_node:article.

So while it surely isn't super nice, I think you can do the following:

Well, that would work for my code. But the standard entity list builder, for instance, uses edit-form and delete-form templates. The moment I "fixed" my local GroupContent::toUrl() was the moment links magically started appearing all over the place.

Still, thanks for the use case review. I really appreciate it.

Edit: I could look into the possibility of using canonical routes after all, but that would require me to set up path aliases for several routes every time a GroupContent entity is saved if I still want to keep the "clean" URLs. So that may actually make my module extremely complex for others to extend.

Crell’s picture

Path aliases are a fairly standard Drupal tool used for clean everything. Why is that an insufficient solution to "I want user-friendly URLs?" If anything, I could see plenty of cases where someone would NOT want group/1/node/add/article, because they don't want the boring group/1/ in there; they'd want /las/history-department/new-post, or somesuch, in which case they need to use a path alias anyway.

I've found that trying to make "programmatic URLs" too user-friendly is a losing battle. :-) Make them REST-friendly, and layer aliases on top of them site-by-site as needed.

kristiaanvandeneynde’s picture

Okay so path aliases may work for creating clean URLs, but what about other route properties?

Right now, a group_membership and group_node overview both use the GroupContentListBuilder. If they share the same route and group_membership wants a different controller, it would also swap out the list builder for group_node. If they have separate routes, however, it would allow a plugin to swap out the controller without affecting the other plugins. Same goes for access checks, provided defaults, etc.

By the way, I'm really liking the feedback. It's got me thinking about revisiting the system I put in place, but I really need to know whether it will be as flexible using canonical routes. Having multiple routes for one link template is what currently gives Group's plugin system so much developer freedom. I'd hate to take that away from them.

kristiaanvandeneynde’s picture

I'm also contemplating something like below, but that would be a "Groupism" on top of a "Drupalism" so will probably break something somehow at some point in the future. It's basically the same idea tstoeckler had in #43.

public function toUrl($rel = 'canonical', array $options = []) {
  return parent::toUrl($this->mapLinkTemplate($rel), $options);
}

FWIW: I'm still not convinced having a hardcoded pattern all over core is the right way to move forward, but I can certainly understand the performance implications if we don't do it. The problem is that if we need to change such patterns down the line, it's already too widespread to be able to effectively do so.

I'd rather have it come from one single point, even if that were something ridiculous like EntityRouteNameProvider::getRouteName('node', 'canonical')

Crell’s picture

I've not used the Group module at all, so I'm having a hard time following the use case in #46. Can you elaborate? What exactly are you trying to do?

A few key things to note:

1) Multiple routes with the same path is totally legit, as long as they differentiate on some other attribute (which could include a custom route filter that filters based on a plugin ID specified in the options section of the route; that's what Page Manager is doing now. Just be mindful of the performance of the filter since it's critical-path.)

2) As a curious side effect of the way the routing system works (I am not 100% happy with this myself, but it's the nature of HTTP), if you have 2 routes with the same path and placeholder rules, generating a link to either one will result in the same output markup; they'll get differentiated again on the incoming request. That is, if you have entity.node.canonical: /node/{node}, and my.custom.event.node.route: /node/{node}, and a custom route filter that checks the bundle of {node} and filters out one or the other route, that works. And you can always generate links to entity.node.canonical, and then event nodes will end up at the second route while other nodes use entity.node.canonical. The REST module relies on exactly this fact. So on the *link* side, you don't need to care which one you're linking to. That split only matters on the inbound side.

3) You absolutely can make your own routes, either via code or YAML, rather than relying on the auto-generated ones. They just need to have the right route name, which will automatically disable to route provider for those routes. (It doesn't override routes that are already defined.) I usually recommend using the auto-generated ones by default for consistency, but if you have a use case to make your own that's fine.

kristiaanvandeneynde’s picture

Wow, that's really useful information Crell. Thanks. Before I explain my use case, what would such a route filter look like? Any class I can look at specifically?

The use case of Group is this:
A Group entity is a fieldable entity with GroupType bundle entities. Then there is the fieldable GroupContent entity which allows you to add content entities into a group. Every GroupContent has two entity references among its base fields: one to reference a single group, the other to reference a single content entity.

The set up is that any content entity can be added to any group under a specific ruleset and with specific metadata (the fields on the GroupContent), e.g.:

  • Add users as members, with the option of giving them roles and permissions in a group
  • Add nodes with the option of making them private

Which content you can add to what groups is configured by enabling plugins on the group type. For instance, the plugin group_membership allows you to add User entities to Group entities as members.

Seeing as GroupContent entities are fieldable, you can add different fields to the "content relationship" between a group and a content entity per group type. So groups of type "Redaction" may ask their members about their writing skills, while groups of type "Playground" may want to know if their members can resist eating stuff from the sandbox.

To achieve this in a user-friendly way, a GroupContentType bundle entity is automatically generated for every plugin that is enabled. It then configures the non-group-referencing entity reference field to only allow the right content entities to be added. Furthermore, routes are generated for all GroupContent entities for that plugin.

In group_membership's case, it's:

  • /group/{group}/member (collection)
  • /group/{group}/member/{group_content} (canonical)
  • etc.

In group_node's case, it's:

  • /group/{group}/node (collection)
  • /group/{group}/node/{group_content} (canonical)
  • etc.

Now the key point is that every plugin generated route should be totally customizable by the plugin. They could add extra access checks, swap out the default controller, ... without affecting the route of another plugin.

Use case 1: The base plugin provides a general collection route, but the group_membership plugin may want to swap out the controller to allow for a bulk actions block at the top of the list.

Use case 2: The base plugin generates routes with a _group_permission sort of check where the site checks whether the user has the specific permissions to view GroupContent of the route configured plugin.

I've managed to achieve all this in a way that has great UX and DX: Site builders can choose exactly which content is available to which group types and what fields they want on the relationships. Developers do not need to know about the complex data structure under the hood, because they can simply write a very small plugin that will generate all of the basic structure for them.

I just gave a very well received session on this at Drupalcamp London, but sadly that recording won't be online for another week or two. The slides are here though: http://www.slideshare.net/KristiaanVandenEynde/group-drupalcamp-london-2016

kristiaanvandeneynde’s picture

The way I fixed it now, by the way is by overwriting toUrl() to match the most recent patch and then use this:

  protected function getRouteName($rel) {
    return $this->getContentPlugin()->getRouteName($rel);
  }

Which leads to this on the plugin:

  public function getRouteName($name) {
    $route_prefix = 'entity.group_content.' . str_replace(':', '__', $this->getPluginId());
    return $route_prefix . '.' . str_replace(['-', 'drupal:'], ['_', ''], $name);
  }

So entity.group_content.canonical becomes entity.group_content.group_membership.canonical, entity.group_content.group_node.canonical, etc. depending on what GroupContent::getContentPlugin() returns. And it works beautifully so far, yet requires an ugly toUrl() overwrite to do so.

I could do something like the code in #47, but that would require me to create route names like so: entity.group_content.group_node__canonical and would break massively if any plugin implementation decides to deviate from that pattern. (Because the default toUrl would then crash again because of the hardcoded pattern in there.)

Crell’s picture

Hm. So first off, it sounds like your architecture is somewhat similar to OG v1 in Drupal 7, with the interstitial entity. OG dropped that for v2 because it ended up being a massive pain in the butt. Making good use of Views and Panels with that model was extremely difficult bordering on impossible, whereas in v2 (without the interstitial) it was really easy and powerful. (I had a site I tried to build in late-model OG v1, that failed, then we tried again with OG v2 and it worked great.) A bit off topic to this thread, but fair warning that there be dragons.

To the point of this thread, it sounds like what you're ultimately trying to do is:

1) Make the path (and other route information) of an entity configurable rather than static in the source code.
2) Allow it to vary by bundle.

Is that an accurate generalization of the problem space?

Assuming so, the way entity/link/route integration works the entity really really really should have defined links on it, from which many other things are built. That many include standard routes, with standard stuff on them like access control, admin forms, metadata for entity upcasting, etc. Those links are also then available to build into the output automatically, as is being hotly debated in #2406533: edit-form, delete-form etc. <link> tags added on /node/{node} are invalid according to W3C Validator. :-) (Bottom line: Moar links good, ignore the themer calls to have control over the header, they're wrong. :-) )

If you want it to be more dynamic than the static annotation, you can provide your own route providers that generate routes off of the link definitions your way. You can also use hook_entity_type_info_alter() to set links dynamically, including their paths and adding more route providers. And all kinds of other stuff. That way, you can use code to define links with known standard names, then use a route provider (code) to turn those into routes based on whatever rules you decide, which could be driven by plugins. That said, in many cases I'd recommend hooking into the entity access system rather than the routing system; that way you will catch more than just routes, but anything else that relies on entity access, not just routing. That would allow you to use a fairly normal set of routes (maybe with the default route provider, maybe not), which gets you a lot of stuff built out for free out of lower-level parts.

The caveat is that part 2, varying the route by bundle... is just not a thing. The Entity API doesn't work that way, and I would discourage you from trying to make it do so. All nodes, regardless of bundle, live at /node/{node}. Trying to make that fork to different routes is going to bite you, badly, as this issue shows.

However! You can certainly tuck the bundle information into the route definition under options, and then define multiple alternate routes at the same path. Assuming your goal is not that you want the bundles to live at different paths but to have different controllers, this is quite straightforward now in D8. For core's route filters, have a look at:

http://cgit.drupalcode.org/drupal/tree/core/lib/Drupal/Core/Routing/Cont...

Which is misnamed and should be called ContentTypeHeaderFilter. :-) That gets a RouteCollection of routes that all match the same path, then returns a new RouteCollection containing some subset of those routes based on excluding some of them based on the _content_type_format requirement and the request's Content-Type header. Page Manager is doing something a bit more elaborate with its own data:

http://cgit.drupalcode.org/page_manager/tree/src/Routing/VariantRouteFil...

See also, this recent blog post on this topic:

https://www.palantir.net/blog/d8ftw-rest-aware-routing

That way, you can have one "default" route with standard route names, generated off of the links. However, you can have an unlimited number of other routes at the same path (one per bundle if you are so inclined), with whatever name you want, that have some extra data on them, and then a route filter to differentiate the routes on incoming requests. Those different routes could have the same controller with different "default" parameter values, or different controllers, or the same controller with different access callbacks, or... Whatever. The REST module does exactly this; the entity.node.canonical link is the HTML page, but REST adds other routes as well at the same path, and then relies on route filters that look at the _format specified on the route and the _format GET parameter, and filters accordingly.

As long as there is *A* route with the standard name that you can link to, everything should be fine.

If your goal is to actually have different user-facing paths for different bundles... don't. Drupal doesn't like that. It really doesn't like that. And users will likely want to override those anyway. The "internal" path should be optimized for code, not for users. Just use path aliases to make the paths pretty.

Does that help simplify things for you?

kristiaanvandeneynde’s picture

Thanks for the warning about the interstitial entity. In D8 it seems to work great with views using relationships, but discussing that further may go too much off topic. If you wish to elaborate on how exactly that broke OG1, I'd gladly hear about it in a PM, over IRC, or whatever. Alternatively, I'll happily demo how it seems to work fine in D8.

To the point of this thread, it sounds like what you're ultimately trying to do is:
1) Make the path (and other route information) of an entity configurable rather than static in the source code.
2) Allow it to vary by bundle.

Well, almost. The route varies by plugin, whereas several bundles may use the same plugin. But ultimately, it boils down to the same thing: An entity having routes vary by a property on them (plugin ID instead of bundle in my case). In any case, I'm really starting to get convinced by what you've been telling me about filtering routes. Just a few more questions.

1. What about collection routes? For any route containing a GroupContent ID, I can find out the plugin ID from the loaded GroupContent. But collection routes don't have that, so how would I know how to filter /group/1/content to the right route for that path?

2. What about performance? You mention it's critical-path, so does loading an entity to read the Plugin ID from it count as "too expensive"?

Having an answer to question 1 would help me rewrite the entire routing logic in Group. Thanks a lot for the contribution so far Crell, I am learning a lot here :)

About this issue:
Should we repurpose this as "Clearly document the expected route name pattern for entities" then?

Crell’s picture

Title: Entity::toUrl() makes undocumented and hard-coded route name assumptions » Clearly document the expected route name pattern for entities

Yes, let's.

On performance, it depends in a large part on whether it's work you'd be doing later anyway (eg, loading the node in /node/{node} is going to happen sooner or later, so it happening sooner doesn't hurt much). There's more context there than I can go into in this comment. I'd suggest talking to someone from the Panels team about what they did, and having Kris Vanderwater or Tim Plunkett talk you through the details.

As for collection routes, I don't completely follow but you can stick extra metadata onto the route when creating it (or in a route alter event) to include stuff in the options section to filter on. If you're able to precompile in that fashion, do so. It's going to be much more performant than doing it at runtime.

Track me down in IRC if you want more details; it's probably easier than going back and forth here. Let's let this thread get back to documentation. :-)

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

joachim’s picture

Status: Needs review » Needs work

Needs work based on previous 2 comments.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

quietone’s picture

Issue summary: View changes
Status: Needs work » Active
Issue tags: +Bug Smash Initiative

Added standard template to the IS, updated proposed resolution to be just documentation.

Setting to active because there is no patch to review.

joachim’s picture

Status: Active » Needs work

There is a patch, but it needs work.

> Document the default expected route names on \Drupal\Core\Entity\EntityType::$links or \Drupal\Core\Entity\EntityInterface::toUrl()? See #53

The patches so far add a method getRouteName() as well -- I don't see where it was decided to drop that.

Are I don't think either of those is the right place for documentation.

The expectation is that if you define the 'links' property for your entity, then you need to back that up with routes. So there should be documentation in the Entity annotation $links property, at least, to say 'Hey if you set this, you have obligations.'