Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
There are a lot of places in core where we're using associative arrays with 'route_name' (required) and 'route_parameters' + 'options' (optional).
Also, we need a nice way to turn a path (e.g. 'node/1') into a structure like the associative array described above.
Proposed resolution
Introduce a \Drupal\Core\Url object.
Example code difference (see change record for more):
// Before
$url = some_function_returning_array();
$url = \Drupal::url($url['route_name'], $url['route_parameters'], $url['options']);
// After
$url = some_function_returning_object()->toString();
Remaining tasks
N/A
User interface changes
N/A
API changes
No actual API changes, only additions and internal refactorings.
Origin of the issue
Comes from (1) part of review #2021779-68: Decouple shortcuts from menu links
Comment | File | Size | Author |
---|---|---|---|
#87 | url-2153891-87.patch | 49.34 KB | tim.plunkett |
Comments
Comment #1
amateescu CreditAttribution: amateescu commentedSome initial code to get things started.
Comment #3
dawehnerAt least we could use $this->create
We would need to extend the interface of urlGenerator, as this is something coming from symfony. Not sure whether it is worth to move even farther away fro the original interface.
Comment #4
Crell CreditAttribution: Crell commentedI wouldn't bother. It's hidden away from the user, so who cares?
This should either be __toString(), or a more semantically meaningful method. (Or, a more semantically meaningful method and then __toString() just sub-calls to that.)
Inject this. The constructor in this setup is essentially private, so it's fine to inject it along with the data.
As above: Yes. :-)
This should either catch nothing and let exceptions bubble, or catch everything and wrap it in a local exception, then throw that. I don't have a strong preference at the moment but I'm sure Mark does. ;-)
Shouldn't this be injected?
Comment #5
pwolanin CreditAttribution: pwolanin commentedI don't find Url very intuitive as the class name (I almost like Tim's suggestion of UrlBag better) Something like UrlData?.
Part of the potential for confusion is that this is NOT the output of the UrlGenerator, for example.
in terms of DX when do I use a UrlFactory vs a UrlGenerator?
Comment #6
amateescu CreditAttribution: amateescu commentedAfter another round of debates, dropped the separate factory and moved it to a static method on the same class. Also fixed the other points from the reviews above.
Comment #8
amateescu CreditAttribution: amateescu commentedRenamed
toString()
togetPath()
, removed the dependency on DependencySerialization and worked a bit on moving the unit test. No idea what's wrong with the installation yet.Comment #10
jibran@pwolanin why not RouteData or RouteInfo?
Comment #11
DamienMcKennaWhat's the naming convention that defines that acronyms are treated as normal words and changed to "Url" instead of "URL"? (seriously asking, not being sarcastic)
Comment #12
Crell CreditAttribution: Crell commentedI believe we settled on Url instead of URL, as that's somewhat more common.
Why not serialize/unserialize? (I vaguely recall there being some reason DependencySerialization doesn't use that, but not why.)
Problem: What comes back is a URI. It could, depending on the options, be an absolute URI starting with http. Thus "getPath" is incorrect.
Comment #13
tim.plunkettI was joking.
https://drupal.org/node/608152#naming, number 3. We violate that in hundreds of places, but better to not add more. Also, see http://en.wikipedia.org/wiki/XMLHttpRequest :)
Comment #14
amateescu CreditAttribution: amateescu commentedHere's the issue that explains it: #2074253: Fatal error when using Serializable interface on PHP 5.4
Would you prefer to rename it to getUri() or do you have any other suggestion? :)
Comment #15
amateescu CreditAttribution: amateescu commentedThis is not blocking #2021779: Decouple shortcuts from menu links anymore so demoting to major.
Comment #16
Crell CreditAttribution: Crell commentedLet's link to that issue then, because otherwise people will ask why we're using PHP 4-era methods.
getString? getUri? render()? getUriString()?
getPath() is inaccurate, and toString() is too easily confused with __toString(). Otherwise I'm not wedded to a specific method name.
Comment #17
amateescu CreditAttribution: amateescu commentedI think I like render() best. Any other opinions?
Comment #18
msonnabaum CreditAttribution: msonnabaum commentedtoString() is exactly what it is. That's what it should be called. There's no room for confusion…because it doesn't have a double underscore.
Every other name proposed makes the method's intent less clear.
Comment #19
amateescu CreditAttribution: amateescu commentedSo.. how does one who doesn't care about the name of this method proceed with this issue? :)
Comment #20
amateescu CreditAttribution: amateescu commentedComment #21
andypostComment #22
yched CreditAttribution: yched commentedDon't want to derail the issue further, but a static createFromRequest(Request $request) would be quite handy next to createFromPath($path).
(Spent an hour yesterday figuring out how to do that - and LinkGenerator::getActive() is a weird place for it)
Comment #23
Crell CreditAttribution: Crell commentedEh, call it toString(). Better to have something than to go nowhere with this issue.
Comment #24
neclimdulComments on IRC but figured I should share it here.
Seem like its a bad idea to make the assumption that URL's are only internal and defined by routes. If we're going to do something like this it seems like we should define a interface for a URL's and then describe how to internal and external url's implement that. Some interfaces may end up only working with internal URLs but that's on them to enforce.
Comment #25
webchickComing from #2141329: Replace getAdminPath() with getAdminRouteInfo() in Field UI.
In that issue, there was this piece of code:
which then got changed to this piece of code:
Which hurts my soul, as well as my eyeballs.
I'm told this issue could change that to:
(This is not clear to me from the patch.)
If that's the case, however, I think the DX improvement here is significant enough to call this at least a beta target.
Comment #26
star-szrThere has been quite a bit of discussion since #8 but I'm tagging this for reroll so we can have something to work from at least.
Comment #27
amateescu CreditAttribution: amateescu commentedRerolled and renamed
getPath()
back totoString()
. Didn't address #24 or #22 yet..Comment #28
YesCT CreditAttribution: YesCT commentedNot sure why this didn't go to the testbot (or maybe it did but the result didn't report back?)
Might be related to #2130059: Issue status change not consistently saved in node revisions or #2166935: Testbot not running. Should we just resubmit the same patch?
Comment #29
star-szrStatus should trigger it hopefully.
Comment #31
tim.plunkettTechnically, '<front>' is the route name, and '/' is the link path. But we allow people to enter that, and need to handle it.
Previously, UrlMatcher caught all exceptions to prevent this from breaking, I think this special casing is better.
Also returning in toString() :)
Comment #33
tim.plunkettI considered moving the exception handling into the static factory method, but @msonnabaum and @Crell counseled against it.
Also cleaned up the unit test.
Comment #34
tim.plunkettFor later: https://privatepaste.com/bf23059ff4
Comment #35
tim.plunkettOkay, switched to using $router->match(), which means we don't have to create a new Request, or mess with _system_path.
I also removed the urlGenerator from the constructor, that would prevent people from making new Url objects very easily...
The change to UrlMatcher is copied from RouteProvider, and was pointed out by @Crell. That allows $router->match() to work.
Comment #37
tim.plunkettI'm not sure why this change to menu_item_route_access() wasn't needed already...
The exact same pattern is in PathBasedBreadcrumbBuilder::getRequestForPath()
I also added some setters that I know we need, and added Url support to $form_state['redirect_route'] and did one sample conversion.
Comment #39
tim.plunkettWell, there is an explicit check for that exception NOT being caught, so I'm not sure which way to go here.
Comment #40
tim.plunkettLet's go this way.
Comment #41
tim.plunkettDoing
\Drupal::l($text, $url->getRouteName(), $url->getRouteParameters(), $url->getRouteOptions());
constantly is not fun, so let's add a code path to support\Drupal::l($text, $url)
I think this is as far as the patch should go, let's get it in!
Comment #43
tim.plunkettDoh!
Comment #44
amateescu CreditAttribution: amateescu commentedShouldn't this be saved for a followup where we will get rid of those three params for l() and make it accept only a Url object?
This is quite a nasty naming overlap, even if they're in different namespaces. I think we should rename the Url utility class as it's bound to get very confusing..
Comment #45
tim.plunkett1) I'm not really sure *what* we want to do here, I assume we'd want to remove the alternate approach, but I think we should make this available right away.
2) We talked about this more in IRC and came up with UrlHelper. I renamed the alias in FormBuilder for now, and added a todo for renaming Url, if we want. I don't think this is in scope, that change would be big, and an extra API change.
This also hopefully addresses #22 (yched) and #24 (neclimdul).
While writing ExternalUrl, I decided to move the class down a namespace, and to rename getRouteOptions to getOptions, to match the named part of toArray() as well as making it applicable for ExternalUrl.
If no one likes the ExternalUrl approach, let's just push it to a follow-up.
Comment #46
tim.plunkettNeeded a reroll after #2047633: Move definition of menu links to hook_menu_link_defaults(), decouple key name from path, and make 'parent' explicit. No changes.
Comment #48
yched CreditAttribution: yched commented#22 / createFromRequest() : awesome :-)
Comment #49
yched CreditAttribution: yched commentedThe current logic around $this->active in LinkGenerator::setRequest() could probably be refactored to build on the new createFromRequest() rather than duplicating it ?
Comment #51
amateescu CreditAttribution: amateescu commentedOpened a separate issue for this: #2184653: Rename Drupal\Component\Utility\Url to UrlHelper
Why?! :) This unnecessarily ties the class to the routing system and I tried to avoid that...
Sure, that makes sense.
Comment #52
tim.plunkett#49, that's actually dead code. I opened #2184649: Remove LinkGenerator::getActive() and LinkGenerator::setRequest() after talking to Wim.
#2184653: Rename Drupal\Component\Utility\Url to UrlHelper actually fixed the broken test, so I've included it for now.
Per @amateescu's suggestions in IRC, I folded the ExternalUrl class into Url, and moved it back out of the Routing namespace.
Comment #53
tim.plunkettChanged our usage of Exceptions per an IRC discussion.
Also removed the UrlHelper changes, that can just happen in the other issue.
Comment #54
dawehnerReally great work!
I would have guessed that we could just do this functionality inside the link generator. Note: The providerBasedGenerator we use, does the same for SymfonyRoute objects, so it is kind of similar.
Let's explain under which circumstances this exception is thrown.
It is sad that we actually need to make a workaround. I would have guessed that PathProcessorFront::processOutbound could have solved it for us automatically.
Can't we use \Drupal::service('router')->matchRequest directly?
I would have expected this to be public.
Have we considered using "toPath" instead?
Can't we just extend DependencySerialization ?
It seems more clean to do it the other way around. Use generate as the basic method (given its similarity to the url generator) and use the generateFromUrl the special case.
Is there a reason we previously did not had to catch the exception?
Comment #55
amateescu CreditAttribution: amateescu commentedWe need to update this because now we handle external urls as well.
"The URL options."
It doesn't have to be a system path anymore..
This method could use a couple of empty lines inside to be more readable.
Why do we have to go through the url generator for external urls?
Can't we just use the class variables instead of getting them through the helper methods?
Comment #56
tim.plunkett#54
1) I wanted to have typehinting and a dedicated method on LinkGenerator, while still letting \Drupal::l() serve both.
2) Done
3) We're moving this workaround out of MenuLink::preSave, which is a good thing. I don't really want to mess with path processing as part of this patch...
4) Good idea!
5) This is handled internally (no pun intended), and no one should be able to change the externality of the URL.
6) Yes, see #4, #8, #12, #16, and finally #18.
7) Yes
8) I disagree, mostly because I think the changes to hook_link_alter are an improvement.
9) Not sure, I'm going to remove it and see what fails. I think that the refactoring I've done since should obsolete this.
#55
1) Fixed
2) Fixed
3) Fixed
4) Fixed
5)
This should work (not actually using this test because its just testing UrlGenerator)
6) I don't really care either way, and I know in other issues @damiankloip has explicitly asked for using methods. Take it up with him I guess :)
Comment #57
amateescu CreditAttribution: amateescu commentedRegarding #54.7 (DependencySerialization) The initial patches had this but someone complained about it in IRC and we chose to implement just what we needed in this class, see the interdiff from #8. I can't remember though why I was asked to do that :/
Comment #59
tim.plunkettIf we tried to write this test from the UI, it would fail. The API should fail as well. So let's fix the bug, and make the paths used by the test exist.
Comment #60
Crell CreditAttribution: Crell commentedA few small comments, none of them terminal:
A parameter that could be a string or could be a full object that results in 2 different actions feels wrong to me. (I don't know if that was discussed above, I've not kept a close eye on the last several patches.) I don't want to hold up the entire patch on it, but if we can sort that out somehow that would be preferred.
Scope creep! (Kidding!)
This is a "foreign" exception (ie, from a different package). We ought to catch it, wrap it with one in our namespace, and rethrow it.
Comment #61
tim.plunkett#60.1 and #60.2:
I removed a lot of changes I introduced while writing the patch. This has seen a lot of refactoring, and many of them turned out to be unnecessary.
#60.3, after discussion in IRC, I added \Drupal\Core\Routing\MatchingRouteNotFoundException.
Comment #63
tim.plunkettI don't know how to better manage the scope of this patch. If we had just added the class and a test, it wouldn't have all of the methods it has now, because I didn't know we needed them.
I can rip out all of the actual usage of this if necessary... And put that in a follow-up.
Comment #64
aheimlichAny particular reason why
Drupal\Core\Url
doesn't implement__toString()
to complement thetoString()
method?Comment #66
tim.plunkettI don't think we should make __toString call the url generator, which I assume is what you suggest. If anything, it would be
And that's not what you're suggesting.
Anyway, I removed all of the entity conversions. That needs to be its own issue, we just need this object available ASAP. No changes, no interdiff.
Comment #67
tim.plunkettThis change is needed here, but is really a part of #2185831: Split up ParamConverterManager and stop throwing NotFoundHttpException.
Comment #70
tim.plunkettadmin/structure/taxonomy/manage/tags/fields is not an actual path anymore...
Comment #71
amateescu CreditAttribution: amateescu commentedThe last patch looks good to me. @Crell, @dawehner, @msonnabaum, @neclimdul, do you have any other concerns? :)
Comment #72
dawehnerGreat idea, seriously!
This looks really nice now.
Comment #73
tim.plunkettThis is blocked by #2185831: Split up ParamConverterManager and stop throwing NotFoundHttpException, because it also makes the same change to UrlMatcher, but includes test coverage for it. I'll reroll after that goes in.
Comment #74
tim.plunkettLeaving postponed, here's the reroll.
Comment #75
Crell CreditAttribution: Crell commentedWe've got the generator wrapped in a utility method. If we have to live with a global service call out for the router, too, it should also be wrapped (with a corresponding setter as well).
The anal-retentive jerk in me would ask that be shortened to a ternary, because it's so easy to do. Not a commit blocker, though, just me being an anal-retentive jerk. :-)
Comment #76
tim.plunkettReroll! Should be quick re-RTBC.
Can someone help with the change record?
Comment #77
dawehnerNote: a ternary might cost more and an URL object might be used in a lot of places.
Comment #78
tim.plunkettI crossposted with Crell's review, let me address that:
1) These are static factory methods. There cannot be a setter, and it's not worth having a protected static function to wrap them.
2) Ternaries copy the values needlessly, we don't want to do that if we can help it.
Comment #79
tim.plunkettAdded a draft change record: https://drupal.org/node/2189619
Comment #80
tim.plunkettComment #81
tim.plunkettComment #82
jibranThanks for the change notice. Couple of minor issues.
followup issue please.
Why we need this?
Comment #83
tim.plunkett82.1) Opened #2189661: Replace $form_state['redirect_route'] with setRedirect()
82.2) If you call Url::createFromPath('http://drupal.org'), it will determine that it is an external URL, and this is how we handle it. The code you referred to is in the protected function setExternal. You cannot reach this code path via the constructor directly.
Comment #84
jibranThanks for the explanation. RTBC++
Comment #85
aheimlichShould there be a
getRouteParameter()
method to complementsetRouteParameter()
?Comment #86
tim.plunkettThere is no use case for needing the value for a single route parameter. You either need the whole set to pass along to something else, or you don't need them at all.
I purposefully left that out.
Comment #87
tim.plunkett#2184649: Remove LinkGenerator::getActive() and LinkGenerator::setRequest() removed code right next to where we were adding it. Rerolled, no changes.
Comment #89
tim.plunkett87: url-2153891-87.patch queued for re-testing.
Comment #90
tim.plunkettBot fluke, fixing status.
Comment #91
xjmA beta blocker is blockered on this.
Comment #92
alexpottCommitted 65c808b and pushed to 8.x. Thanks!
Let's publish the change record!
Comment #93
xjmNot both.
And wow, that d.o bug is nasty.
Comment #95
ParisLiakos CreditAttribution: ParisLiakos commentedshouldnt the change record be published now? or there are other issues required for that?
Comment #96
tim.plunkettFinally opened the follow-up I mentioned in #66 #2215961: Entity::urlInfo() should return \Drupal\Core\Url