Problem/Motivation

In Drupal 7, the path storage API works on arrays. When converting that to a service/methods, this was switched to separate arguments.

This is causing a problem because it was common for contrib modules to use this to pass along additional arguments. The most well-known example is the pathauto/redirect duo. Pathauto is passing along the original alias so that redirect can automatically create a redirect for a changed alias (use case #1). Additionally, redirect also checks a separate flag that allows to control if a redirect should be created or not (use case #2).

See http://cgit.drupalcode.org/redirect/tree/redirect.module?id=refs/heads;i...

Proposed resolution

Multiple solutions have been discussed:

1. Going back to arrays. I think everyone agrees that this would a step backwards, so not likely to happen.
2. Pass along a new $options array that is just used to communicate with hook implementations. This is a currently non-existing pattern that we would introduce here.
3. Conversion of the path storage API to objects, with interfaces. That would allow a module like Pathauto to subclass and provide additional data through that. This is the way forward, but it is also a non-trivial change to do at this point.
4. The last suggested option is to make the storage include the 'original' alias by default, which solves use case #1 but not anything else. That is however the most common use case, and this would be a more generic fix than in Drupal 7, because it works for all aliases, automatically.

While #3 would be the preferrable solution, it is not clear if we can do that at this point. Because of this, the suggestion is to try that in a follow-up (#2336597: Convert path aliases to full featured entities) and deal with the most problematic part here by implementation solution 4. Those two options can be combined, we can incorporate this new functionality into an object/interface in the follow-up issue.

Remaining tasks

User interface changes

API changes

hook_path_update() now receives an additional key 'original' that contains the old alias, source and langcode.

Original report by @Dave Reid

Regression caused by #1269742: Make path lookup code into a pluggable class:

I've also noticed that this removes the flexibility to pass additional information in along with the path aliases. In Drupal 7, we can do something like this:

$alias['source'] = $source;
$alias['alias'] = $alias;
$alias['langcode'] = $langcode;
$alias['pathauto'] = TRUE;
path_save($alias);

But now all the arguments are hard-coded:

drupal_container()->get('path.crud')->save($source, $alias, $langcode);

This seems like a regression which I know will affect a few contrib modules. Can the crud methods accept an array like it did with Drupal 7, rather than making it more of a DX regression?

For example, it's used by Pathauto to indicate that this alias is an automatically generated alias and not a manual user-created alias. We also add in $path['original'] to show what the original alias was if it is being changed or updated, so that the Redirect module can automatically create redirects so that the original alias redirects to the new alias.

http://drupalcode.org/project/pathauto.git/blob/refs/heads/7.x-1.x:/path...
http://drupalcode.org/project/redirect.git/blob/refs/heads/7.x-1.x:/redi...

I'm just not sure why path_save($path) was converted to essentially path_save($source, $alias, $language) when it would have been an easier conversion to leave it as an array. I guess it seemed to originate from:

I know this is a straight-port, but I want to try and avoid anonymous arrays as data objects. If Path is not going to be its own data object, then the CRUD functions should take multiple useful parameters rather than an anonymous data array.

But I would definitely consider url aliases their own data objects since we have path_load(). I'm not sure this change was justified.

Changing passed/accepted arguments sounds like a quick fix though? Let's quickly adjust that.

This regression affects functionality in contrib and DX.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

aspilicious’s picture

Talked with davereid in irc about this and I do like the $source, $alias, $langcode pattern. The best/easiest solution I see at the moment is passing an additional $options array. That array will be passed to the hooks.

moshe weitzman’s picture

Title: [REGRESSION] Path aliases are no longer arrays and cannot pass additional data to path hooks » Path aliases are no longer arrays and cannot pass additional data to path hooks
Issue tags: +Regression

Lets not shout and bugs that recur are only kind of special. Lets use a tag for that. The title is already quite clear that past behavior was different (and better).

Dave Reid’s picture

Ah, thanks, I'll be sure to use the tag in the future now.

sun’s picture

I think I commented along these lines in the original issue already, but here we go again:

So, the obvious quick-fix would be to simply convert this back into an array. I'd +1 that, since the current API is a clear regression in terms of DX (I worked against it already, so this is not unfounded).

However, the second best option would be to go straight to typed Path\Alias class objects instead. That would give properly defined + documented path alias properties on every object.

Off-hand, I cannot see which (contrib) use-case would not be able to work with object properties; i.e., instead of setting an array key, you simply set an object property, and/but like before, any additional property is not persistent and only exists for the lifetime of the path alias variable.

What do you think?

lirantal’s picture

I'd be happy to work on this. So our options are either:
1. Passing in a hashed array (this is compatible with previous versions)
2. Passing in an extra 'options' array to go for module hooks

I'm +1 for option (1) too.

P.S. @sun it seemed like you were also suggesting that instead of passing a hashed array (as specified in (1) ) it should accept an object and use it's properties although that would break compatibility again, wouldn't it?

xjm’s picture

Issue tags: +Needs tests
xjm’s picture

@Crell in #1269742-22: Make path lookup code into a pluggable class where the change away from an array was first suggested:

+++ b/core/lib/Drupal/Core/Path/DrupalPathRegistry.php
@@ -0,0 +1,102 @@
+  public function save(array &$path) {

I know this is a straight-port, but I want to try and avoid anonymous arrays as data objects. If Path is not going to be its own data object, then the CRUD functions should take multiple useful parameters rather than an anonymous data array.

I generally would agree with that assessment and would prefer either explicit arguments or a classed object.

mikeryan’s picture

Status: Active » Needs review
FileSize
6.61 KB

Finally looking to start contributing to D8, the path API seems a relatively familiar place to start;). Here's a shot at adding an options array to Path::save().

dlu’s picture

Component: base system » path.module

Moved to path.module per #2050763-16: Refine "base system" component (notes on refactoring of "base system" category here: https://docs.google.com/a/acquia.com/spreadsheet/ccc?key=0AusehVccVSq2dF...).

xjm’s picture

8: path-extra_data-1854284-8.patch queued for re-testing.

Status: Needs review » Needs work

The last submitted patch, 8: path-extra_data-1854284-8.patch, failed testing.

durgeshs’s picture

Assigned: Unassigned » durgeshs
Berdir’s picture

Assigned: durgeshs » Unassigned
Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
7.19 KB

Re-roll.

We're porting pathauto and redirect to 8.x and been trying find workaround for this, but it's a mess as we have to resort to global state. So this is still a problematic regression.

While I agree that path should probably be a value object, adding a separate $options argument is only an API additional and possibly the easiest option at this point.

Berdir’s picture

Tagging, thanks @xjm.

Status: Needs review » Needs work

The last submitted patch, 13: path-extra_data-1854284-13.patch, failed testing.

Crell’s picture

Honestly, throwing an extra "random crap goes here" array in seems like going backward.

Why not go forward? Go all the way to a classed Path object, which Pathauto can subclass if it wants to pass around extra data?

Berdir’s picture

Status: Needs work » Needs review
FileSize
3.55 KB

Crell: Yeah, that would be nice, but that's quite a change (I found ~20 calls to save/delete/load, new classes, lots of test calls). And it is getting very very late in the cycle.

Discussed this a bit with @dawehner. Is there any other real use case other than the original thing? What if we simply make this a default feature? Just like entity update hooks, we by default load the original data and pass it along to the hook. When a conversion to an Alias (because it would be that, not path ;)) class, we can easily change that to getOriginalAlias() that would return the old alias object.

If so, then this is much easier to do.

dawehner’s picture

I agree that passing along some options is kinda of a bad idea. I mean people could come up with examples for that in pretty much every other
part of core. I think we should try to avoid that.

On the other hand to convert it to an object is a non-trivial amount of effort, especially because it is an API change, which would need another
big amount of time, to finish it properly. If someone though is motivated to do so, it can be still done after this patch.

The change from berdir makes sense, given that there are many update implementation which return some context of the previous value, like
for example \Drupal\Core\Database\Query\Update::execute() with the amount of updated rows.

+++ b/core/lib/Drupal/Core/Path/AliasStorage.php
@@ -63,11 +63,19 @@ public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NO
+      $original = $this->connection->select('url_alias')
+        ->fields('url_alias', array_keys($fields))
+        ->condition('pid', $pid)
+        ->execute()
+        ->fetchAssoc();

Let's just use a static query.

Berdir’s picture

Changed it to a static query.

Will update the issue if others agree with this as a solution.

Dave Reid’s picture

Hrm, this still doesn't satisfy the original request at all. Right now redirect_path_update() looks at $path['redirect'] to see if it a FALSE value, and if so, will not create an automatic redirect. This would be impossible with the current approach.

Berdir’s picture

Yes, it doesn't, but on the plus side, that will make it work for all kinds of updated aliases. We can also move that to a separate issue.

The problem here I see is that an additional $options array doesn't seem likely to get accepted, going back to an array even less, and going forward to an object would be quite the change and I don't know if it would be accepted and if someone has time to work on that.

slashrsm’s picture

I agree with @Crell. Adding options array where where throw everything that we don't know where to put isn't the best solution. Current patch is IMO a step in right direction.

It would be great if we could proceed with conversion to actual path object. Could we target 8.1 with that?

Crell’s picture

I'm fairly certain converting to an Alias object would be a breaking API change, so not allowed in 8.1. It would have to get in before that. We won't know how hard it is unless we try, though. :-)

slashrsm’s picture

Status: Needs review » Reviewed & tested by the community

Let's RTBC this and open another issue for object conversion and see if we're capable of doing it. How does that sound?

slashrsm’s picture

Berdir’s picture

Issue summary: View changes

Updated the issue summary with all discussed options, also created a change record draft: http://drupal.org/node/2336611

Crell’s picture

That seems like a reasonable plan, Berdir.

Dave Reid’s picture

Stating for the record that I disagree and that this feels like this is another small place core is squeezing out things that contrib can and will possibly do.

Crell’s picture

Dave: "Place to dump random arbitrary data for whatever" is not a pattern that can work in modern high-end software (or low-end, for that matter). We need alternatives to that. Providing a "previous version" object is a structured and consistent-with-other-subsystems way of addressing some use cases. That's a step forward. If you have structured (ie, non-"big array of extra stuff") recommendations for how else to improve the API here to make it more flexible please share them.

Dave Reid’s picture

My perspective is "something I could easily do in Drupal 7, I can no longer do in Drupal 8." I'm not disagreeing with you that arbitrary data is bad, but this is simply a regression between core versions. We definitely have a use case that allowing for arbitrary third-party data should be allowed and respected. For example: #2224761: Add a generic way to add third party configuration on configuration entities and implement for field configuration and #2324121: NodeType's settings array was meant to be able to store information from mutliple modules. My original proposal was just to add an extra $data = array() to the existing methods that could be passed through to the hooks and other methods and would have easily solved this use case until we could convert aliases to a more appropriate object.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed c78a5c2 and pushed to 8.0.x. Thanks!

This seems a good place to start. Yes #2224761: Add a generic way to add third party configuration on configuration entities and implement for field configuration added a way to store third party data but it was not arbitrary. It's using a interface to control how it is stored on the object and made it play nice with config schema. Hopefully #2336597: Convert path aliases to full featured entities will create a nice way for modules to add additional data to paths.

  • alexpott committed c78a5c2 on 8.0.x
    Issue #1854284 by Berdir, mikeryan | Dave Reid: Fixed Path aliases are...

Status: Fixed » Closed (fixed)

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