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
Proposed resolution
This patch implements a blend of options #2 and #3 described in that issue's summary. It:
- Introduces a $route_match object that can be injected independently from $request. Code that receives that object can and should treat it as an independent object, and make no assumptions about how or whether it's related to what's in $request->attributes. This patch updates all theme negotiators to demonstrate the pattern of $route_match as an injected object.
- Introduces a \Drupal::routeMatch() service wrapper for non-DI code to use, and updates all such non-DI code that I could find to use it.
- Implements the service with a CurrentRouteMatch class that proxies the getter methods to the corresponding data in
$this->requestStack->getCurrentRequest()->attributes
. In this way, it's similar to the adapter approach tried in #2103301: Add a helper class to introspect a given request, but a key difference is that that's an implementation detail that all consuming code is shielded from. For example, per #2124749-32: [meta] Stop using $request->attributes->get(MAGIC_KEY) as a public API, contrib could explore overriding this service along with RouterListener, ControllerResolver, and maybe a couple other classes, with an implementation that avoids using $request->attributes entirely without breaking any code that uses the RouteMatch API.
Remaining tasks
- Decide if we like this approach.
- Figure out how to inject $route_match into controllers. Via the controller's factory method,
create()
? Via allowing a $route_match parameter on the controller method's signature (would require updating ControllerResolver to support that)? Or via a trait? - Figure out what to do about event subscribers (e.g., AccessSubscriber): they get their request object via $event->getRequest(). Is it ok for them to use $request->attributes directly, or do we need to inject them with $route_match somehow.
- Decide which of our interface methods that currently receive $request (e.g., TitleResolverInterface::getTitle(), Local(Action|Task)Interface::getRouteParameters(), Local(Action|Task)Interface::getOptions()) need to be changed to receive $route_match instead / in addition to.
- Add constructor or setter injection of $route_match into all services that need it and aren't covered by above.
Note that other than the first item above, we can decide to punt the rest to followup issues.
Comment | File | Size | Author |
---|---|---|---|
#134 | interdiff.txt | 20.2 KB | effulgentsia |
#134 | routematch-2238217-134.patch | 62.29 KB | effulgentsia |
#133 | routematch-2238217-133.patch | 59.29 KB | effulgentsia |
#132 | interdiff.txt | 2.47 KB | effulgentsia |
#132 | routematch-2238217-132.patch | 59.28 KB | effulgentsia |
Comments
Comment #1
effulgentsia CreditAttribution: effulgentsia commentedComment #3
dawehneri don't see the scope of the request object.
Just in general it seems wrong to have global state for the matched route. We should at least have an easy way to reconstruct it. if the request changes etc.
Comment #4
znerol CreditAttribution: znerol commentedIn Szeged @dawehner and me thought about extending the
Symfony\Component\Routing\RequestContext
and use it in therouter.request_context
service. This was in the context of decouplingUrlGenerator
from therequest
service. Maybe this could help in this case also. Although a problem with that approach might be thatRequestContext::fromRequest
is called before routing. Thus the route related request attributes are not populated at this point in time, I guess.Comment #5
znerol CreditAttribution: znerol commentedA possible solution to that problem could be to also extend
Symfony\Component\HttpKernel\EventListener\RouterListener
and update the route match service or whatever at the end ofonKernelRequest
.Comment #6
effulgentsia CreditAttribution: effulgentsia commentedLet's see if this cuts down the failures to a manageable amount.
Comment #8
effulgentsia CreditAttribution: effulgentsia commentedWhat do you mean by "scope"? If you mean visibility, then it's declared as protected at the top of the class. If you mean, logically, why is it included as part of what a "route match" knows about, then it's because routing is based on matching a request to a route. So, the concept of a route match should include the matched route, the values of that route's parameters (i.e., arguments), and also, in my opinion, the request object, in case code that receives a $route_match object wants to consider other HTTP info like query parameters or POST data (e.g., see AjaxBasePageNegotiator::determineActiveTheme()). I suppose alternatively, we could require use cases like AjaxBasePageNegotiator to supply route enhancers to map the stuff they care about into route arguments.
The current patch doesn't retain it as state. \Drupal::routeMatch() reconstructs it each time. However, once the instantiation is put into RouterListener, per #5, then the only way to change it in response to a $request change will be to invoke $router->matchRequest(), which I think would be a desirable restriction: a route match shouldn't change arbitrarily, it should be created as part of routing, and then be immutable.
Comment #9
dawehnerSorry for my last review, that one had a really bad quality.
In the case that someone actually wants request specific data why not just let him deal with the request object directly. I don't see why we
need an additional layer for that. I hope we won't disable to get the request if you want it.
Mh, wouldn't that require everyone who wants to use anything from the request to write another route enhancer which will then be fired on every request and potentially in many more places. This seems to also be no real win, because it is getting harder to follow what is going on.
Right, good point. For now living on
\Drupal
is probably okay, but why don't we actually move that into some routing bit?\Drupal\Core\Routing\UrlMatcher
seems to be one potentially good fitting place.\Drupal
just feels like a class which should be deprecated kind of by definition.Do you think it would be also possible to have the request be real optional? a request object could point to something which actually does not exists in reality.
Just in general: it would be better to not execute some crazy logic inside the constructor but yeah in this case it is not really a problem.
I really hate that we now no longer depend on http foundation but on actual our own bit untop of CMF and component routing. Could we maybe at least move the route match into a component? There is nothing 100% specific about that, as it is a nice way to access these data.
oh yeah I sucked totally when writing that code. This should really be on the interface for example. On the other hand I think it is a good idea for now to not recalculate the active theme, everytime this is called.
Comment #10
larowlanI was expecting to see a RouteMatchStack here. Every time a new request object is pushed into the request stack, KernelEvents::REQUEST fires. We should either be subscribing to that event and pushing a new routematch object into our stack, or we should be just requiring the request_stack as a dependency.
Every time we call this we get a new route-match object, regardless of whether the request has changed or not. That surely leads to memory leaks.
Shouldn't this use the request stack instead?
Missing docblocks
In my opinion this is worse DX. We are replacing a domain object (param bag) with an arbitrary array. Anyone who gets all the arguments back then needs to do the isset() dance. The Param Bag is far superior
This illustrates my point, this code wouldn't need the isset() dance if we kept the properties as param bags.
This looks much better
This is a step backwards. We are interested in a property of the request here, i.e. something formerly accessed with $_GET. I don't want to have to go via RouteMatch for that, because I'm genuinely interested in a property of the Request, not of the routing system.
Just so it is explicity clear every one of these calls creates a new RouteMatch object in memory. We can't do that. We have to have a RouteMatch stack service in or before this patch.
This looks good. Whilst the _something attributes are obvious overloading, the 'node' attributes one could argue are valid - but only in the context of routing - so moving them to RouteMatch makes sense.
This is a step backwards - I assume there will be methods on RouteMatch to get the system path in one of the associated issues?
Comment #11
larowlanI think this will interfere with the work being done in #2016629: Refactor bootstrap to better utilize the kernel. We should stop pretending the request is optional. In that issue, we make it clear that a request is needed to create a DrupalKernel. It is required before we even decide what settings.php to use. I think the $request parameter has to be required here.
Comment #12
sunI 100% agree. And that's a fundamental part I don't get about all these issues:
Request
as a scope + service was a bad idea, and introducedRequestStack
.ContextStack
a dozen of times by now, whereas eachContext
in the stack would offer access to (1) the route, (2) the request, (3) the current user, (4) the current language, and (5) *whichever* additional context contrib comes up with (cf. Organic groups, Domain Access, etc.pp.).Request
into some other one-off, routing-specific, state-ful objects, which are guaranteed to not resolve the overall problem at all...?Can someone explain to me why we do not just simply do the following?
RequestStack
intoContextStack
.Context
domain object that pretty much works likeParameterBag
, but only operates with values that are objects implementing aContextInterface
.Request
is subclassed to implement that interface.$context->get('request')->...
$context->get('route')->...
$context->get('user')->...
$context->get('language')->...
→ Clean context injection for the entire application stack.
→ Secondary: Fixing ESI, sub-requests,
HttpKernelTestBase
, etc.pp.→ Secondary: Epic milestone for SCOTCH, layouts, and "Panels" in D8.
The
RequestStack
concept is proven to work and resolves a whole bunch of problems (for Drupal).By now, my impression is that we have a communication break down somewhere. That is, unless the above was "blocked" by some insane purism of "Let's not subclass Symfony, contribute upstream instead", which would be hilarious, because (1) Symfony-based PHP frameworks may not share our requirements, (2) that's an epic effort, and (3) we're trying hard to release.
Comment #13
znerol CreditAttribution: znerol commentedI do not want to get OT here, but please note that
RequestStack
is a special case and we do not need more*Stack
classes in order to implement context properly.There is consensus that the synchronized
request
service was a bad idea (and also container scopes which were introduced to make synchronized services actually work). And it certainly is true that introducing therequest_stack
will solve many problems. I'd like to stress that the actual win for services consuming the request stack instead of the synchronized request service is that they may store a reference at construction time and never need to update it subsequently. Whether this is a stack or not is not relevant at all (it is only relevant to the symfony HttpKernel).Please note that there is already a pattern in Symfony demonstrating a solution on how to decouple context from request and we are using it already: This is what the Symfony\Component\Routing\RequestContext is all about. Services consuming the
router.request_context
service always can access up-to-date information about the request which is relevant in the service context without actually accessing the request itself (or the request-stack). Starting from Symfony 2.4, therouter.request_context
is updated from within the Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelEvent handler.Anyone please make sure to try understanding the context-concepts in Symfony and only after that consider introducing new ones. I very much believe that new
*_stack
services beside therequest_stack
are not necessary.Comment #14
effulgentsia CreditAttribution: effulgentsia commentedThanks for the feedback, guys. I'll work on addressing what I can.
Comment #15
effulgentsia CreditAttribution: effulgentsia commentedThis one still has all sorts of failure, but wondering if this addresses all of the architectural feedback. Not attaching interdiff, as almost every hunk has changed.
Symfony's RequestContext is a different pattern, because it has a meaningful fromRequest() method, as all of its info comes from something legitimate about $request. However, RouteMatch needs to be instantiated completely independently of $request. I hope this patch helps illustrate that.
Per above, I don't know how to do it other than with a RouteMatchStack, so that's what this patch does. I'm open to other ideas though.
I'd like us to have at least one more implementation of a stack besides Request and RouteMatch before trying to abstract all the stacks into a unified ContextStack.
Comment #17
effulgentsia CreditAttribution: effulgentsia commentedComment #19
znerol CreditAttribution: znerol commentedI'm opposed to swapping out the
RouterListener
completely. If we do that, chances are that inner workings of corresponding Drupal and Symfony components begin to diverge. See #2223189: Use regular upstream HttpKernel instead of Drupal's custom for an example. Also note that this would get in the way of #2223593: Decouple the router.request_context service from the request service..The way
router.request_context
works in Symfony 2.4 is like follows:We could do exactly the same with the router matcher, except that the data is updated from within the on-requests-event handler after routing obviously. The symfony router listener does not care to remove the request context in a finish-request-event, therefore it is unclear to me why our router listener should pop a router matcher stack there.
We do not need any new stack-services.
Comment #20
effulgentsia CreditAttribution: effulgentsia commentedJust trying to get bot to run properly for now.
I'll reroll once that's in.
Yeah it does. It does it by invoking
$this->setRequest($this->requestStack->getParentRequest());
which makes sense if it can reconstruct the request context from $request. Which we can not do for a route match, unless we store everything the route match needs on the request, but that's what we're trying to move away from.Comment #22
martin107 CreditAttribution: martin107 commented#20 patch no longer applies - reroll - this patch is just Auto-merging fixes.
So not expect anything to run/pass.
Comment #24
martin107 CreditAttribution: martin107 commentedOk so now the error messages are up to date I will reroll...
Comment #25
dawehnerAdds some tags.
Comment #26
martin107 CreditAttribution: martin107 commentedChanges to comments
So the way forward is far from clear .. but writing docs to gain perspective..
Comment #27
effulgentsia CreditAttribution: effulgentsia commentedAccording to the testbot log in #22, bot is failing on
drush en -y simpletest
. I can enable it without errors from the admin/modules UI, so something with the Drush integration. I'll look into it when I get a chance if no one beats me to it, but probably won't be until tomorrow.I also realize there's feedback in earlier reviews, especially #19, that hasn't really been addressed yet. I'm hoping to do that once #2223593: Decouple the router.request_context service from the request service. lands, and once the patch passes bot.
Comment #28
martin107 CreditAttribution: martin107 commentedOn my local machine it is still possible to push this patch into error by browser testing of
Drupal\simpletest\Tests\OtherInstallationProfileTestsTest
Comment #29
znerol CreditAttribution: znerol commented#2223593: Decouple the router.request_context service from the request service. went in.
Comment #30
effulgentsia CreditAttribution: effulgentsia commentedJust a straight reroll for now. Will post another patch once I'm able to get installation and enabling of simpletest from Drush to work, in order to get a real testbot run.
Comment #31
effulgentsia CreditAttribution: effulgentsia commentedComment #33
effulgentsia CreditAttribution: effulgentsia commentedJust a reroll. Still need to fix tests, but CNR to see if bot finds any new failures.
Comment #34
effulgentsia CreditAttribution: effulgentsia commentedComment #36
effulgentsia CreditAttribution: effulgentsia commentedJust a reroll. There's one more thing I want to do here before unassigning myself.
Comment #37
effulgentsia CreditAttribution: effulgentsia commentedThis removes the docs from #26, which weren't entirely correct. We will need some docs in here, which I can help with if there's some consensus on this approach even being worth pursuing.
This also integrates a Controller example (using the REST module one), which requires ControllerResolver integration and some new todos as a result.
Next, I'll open a separate meta issue to discuss the relative merits of this approach vs. the earlier/alternate attempts in #2103301: Add a helper class to introspect a given request.
Comment #39
effulgentsia CreditAttribution: effulgentsia commentedPostponing on #2256669: [meta] Finalize module-facing API of getting the matched route name and a matched route argument for a request
Comment #40
xjmUntagging in favor of that one.
Comment #41
effulgentsia CreditAttribution: effulgentsia commentedNew direction based on last Thursday's IRC meeting; not attaching interdiff as I don't think it would be helpful. Need to update summary still, but seeing what bot thinks first.
Comment #43
effulgentsia CreditAttribution: effulgentsia commentedComment #47
catchWhy storing the object itself on the route - is this for performance?
Comment #48
effulgentsia CreditAttribution: effulgentsia commentedRe #47: yes, but that was me being stupid and not realizing that Route::compile() already maintains its own static cache. So, here's a better implementation of CurrentRouteMatch.
Comment #49
dawehnerI wonder whether getArgument should fallback to the raw argument in case there is no upcasted value?
Given that we will call getParametersNames for each get...Argument call I wonder whether we should a a little bit of caching here.
Comment #51
effulgentsia CreditAttribution: effulgentsia commentedPicked up a few other easy conversions and updated the summary. Unassigning myself pending reviews.
What's an example where that's desirable?
I think we should hold off doing that until we have an example of it being called enough times in a request to matter. I'm not yet clear whether/how menu link rendering will use $route_match, which is the only use case, I think, where those numbers start going up.
Comment #52
catchI like the look of this so far.
Minor, but shouldn't be also be losing the use statement at the top of the file for these conversions?
Comment #53
effulgentsia CreditAttribution: effulgentsia commentedRerolled for #2236167: Use request stack in cache contexts and fixed #52.
Comment #55
Crell CreditAttribution: Crell commentedIn broad strokes I approve of this plan. Thanks, Alex! Some comments below:
This class needs a docblock to explain why it's different than RouteMatch, because it's not at all obvious.
That there's three *RouteMatch classes suggests it needs an interface.
CurrentRouteMatch has a lot of @inheritdoc statements, but this parent has no docs to inherit.
Needs docblocks.
To the follow-up questions:
- I don't think we should inject RouteMatch into controllers automatically. Most of the data there that the typical use case for a controller would need is already mapped by ControllerResolver. The second most common request-specific thing a controller would want is GET parameters, but those aren't on RouteMatch anyway. (You need the Request for that.)
Comment #56
effulgentsia CreditAttribution: effulgentsia commentedI agree that in principle, an interface would be good. I initially refrained in order to keep symmetry with Request, which doesn't have an interface, since both Request and RouteMatch will be passed to interface methods like in the theme negotiators. But perhaps that's not a good enough reason. Anyone have objections to breaking the symmetry and changing the typehint to RouteMatchInterface?
Comment #57
catchThis wasn't tagged beta blocker however I think it should be, do we still need #2256669: [meta] Finalize module-facing API of getting the matched route name and a matched route argument for a request open though?
There's details to work out here but it doesn't look like we're going to do anything radically different requiring a different issue to figure out.
Comment #58
xjmWe left the tag on #2256669: [meta] Finalize module-facing API of getting the matched route name and a matched route argument for a request because we weren't sure which of the two proposals would be the correct solution and @effulgentsia wanted to give contributors a chance to evaluate both. So I'm okay with marking it fixed/duplicate/etc.
The feedback we gave on the recent Drupal 8 call was that the name RouteMatch was confusing for both Dries and myself to understand the object's purpose; any further thoughts around that?
Comment #59
catchRouteFinder? RouteLocator?
Comment #60
effulgentsia CreditAttribution: effulgentsia commentedIgnoring the contents of $request->attributes, which part of the point of this issue is to make Drupal code never deal with, then Request is a concept that is unaware of routes. And Route is a concept that is unaware of requests. The process of routing is taking a Request as input, finding a matching Route, and returning an object (at least conceptually, if we ignore that Symfony's router returns an array rather than an object) whose meaning is: everything you want to know about the intersection between a Request and a Route. So, for example, a Route knows that its path pattern is /node/{node}, while a Request knows that the path being requested is /node/1, but it is the intersection of those two objects that provides the API for knowing that the "node" parameter = node_load("1").
Zend Framework 2 calls that intersection object a RouteMatch. I don't know what other frameworks name it. Symfony doesn't have a name for it, since it's based on returning an array that gets merged into $request->attributes. I don't think the suggestions in #59 are preferable to RouteMatch.
In addition to naming issues, when I discussed this patch with Dries, he asked why RouteMatch doesn't provide a getRequest() method: since it's the intersection of a Request and Route, and has a method for getting the Route, it stood out as a lack of symmetry to not also have a method for getting the Request. Adding that would allow theme negotiators and other similar interfaces to just get a RouteMatch rather than needing both a RouteMatch and a Request as in #53. My initial patch on this issue actually had that, but I took it out based on dawehner's feedback in #9. One way to think about the lack of symmetry is that a RouteMatch represents the output of a process (routing). The process has an input, a Request, but why require the output to keep a pointer to the input? Anyway, I think it's mostly a stylistic question, and Dries didn't feel too strongly about it either way, but I'm curious what others think.
Comment #61
xjmRerolled for #2247991: [May 27] Move all module code from …/lib/Drupal/… to …/src/… for PSR-4.
What about something like RouteRequestMatcher or RouteRequestInfo or something? Something that indicates that it gets routing information related to a given request. If a getter were returning this object, I'd call it something like getRequestRouteInfo(), I think.
Comment #62
Crell CreditAttribution: Crell commentedThe class name should not be a verb, because it's not an object that does anything. It is (from the POV of someone using it) a read-only data object with useful data. RouteMatch seems weird to me too but I won't lose sleep if we go with that. That ZF2 uses that name means it will be familiar to at least somebody. If Symfony didn't already use the name for something else I'd suggest "RequestContext".
I would prefer to keep Request a separate service/object. I could probably throw in something like the Law of Demeter here, but I'll just say "what effulgentsia said". :-) Input object and output object are different things doing different things.
Comment #63
effulgentsia CreditAttribution: effulgentsia commentedThis adds the interface and removes NullRouteMatch, which wasn't used. I wonder whether to leave RouteMatch in or not: it's only used by tests, so could be replaced with a mock; however, is it useful to leave in for any reason (e.g., to demonstrate an alternate implementation than CurrentRouteMatch)?
I spent a few hours googling around for what other frameworks do, and seems like they all are quite different. Some mutate $request, as Symfony does. Others mutate or construct a new Route instance that contains a getParameter() method. Others only pass the parameters to the controller and don't make them available in any other way. I couldn't find anything other than ZF2 that defines a dedicated object (though that could just be due to my poor searching skills). Unless someone can find some other framework with a similar object and a different name, I suggest we stick with what ZF2 names it.
Ok, so until someone comments with a compelling reason for adding getRequest() to RouteMatchInterface, I'm leaving it out.
Yep, RouteMatchInterface, RouteMatch, and CurrentRouteMatch all need proper docs still.
Other than the docs, per above, what's left to work out here? My suggestion is to do each of the issue summary's remaining tasks in follow ups. Any objections to that?
Comment #64
znerol CreditAttribution: znerol commentedThank you @effulgentsia for working so hard on this. I very much like the new approach.
In my opinion the name very well describes the purpose of the class. In some programming languages the result of applying a regular expression to a string is an object providing easy access to substrings matched by the pattern (e.g.
Match
in Python). It seems very natural to me that the process of routing (i.e. applying a set of route patterns to an URL) results in aRouteMatch
object. Routing is closely related to pattern matching. This is also very apparent when looking at the corresponding interfaces of the Symfony routing component.I very much agree. I suspect that most of the theme negotiators do not really require the request-object at all, so maybe it would be possible to remove that from the
ThemeNegotiatorInterface
in a follow-up.How about
current_route_match
? Similar tocurrent_user
.Yeah! This is so much better, as well as all the other conversions in this patch.
Use interface instead of class name here.
I think we should use
Parameter
orParameterValue
instead ofArgument
here. In the Symfony documentation parameter is used in the context of routing and argument when discussing controller related things.Comment #65
effulgentsia CreditAttribution: effulgentsia commentedThanks znerol! I agree with all the suggestions in #64. Will reroll when I get a chance if no one beats me to it.
Comment #66
cweagansRerolled with changes from #64.
Comment #68
cweagansWhoops. Wrong patch. Use this one instead. Same interdiff.
Comment #71
znerol CreditAttribution: znerol commentedCurrentRouteMatch
still uses the old method names,RouteMatchInterface
has the new ones.Comment #72
martin107 CreditAttribution: martin107 commented1) I looked at #68 and saw the invalid service definition warning .... fixed
2) Applied Znerol suggested fixes.
limited local testing DRUPAL MATCH PATH is green
Comment #73
jibrandoc block missing.
Comment #74
martin107 CreditAttribution: martin107 commentedMissing doc blocks supplied.
Comment #75
znerol CreditAttribution: znerol commentedDid go through this again and noted the following things:
There are no PHPunit tests for
CurrentRouteMatch
. Not sure whether it would make sense to assert a couple of things, especially becauseCurrentRouteMatch::getParameterNames()
seems to do a substantial amount of work.No documentation for classes / interfaces. Out of those three the
RouteMatchInterface
is the most important to document accurately.How about something like this:
Let's improve consistency and also rename the instance variables from
$arguments
to$parameters
.Comment #76
martin107 CreditAttribution: martin107 commentedReferring to #75 :-
I agree CurrentRouteMatch::getParameterNames() needs testing ..
Can reviewers comment ...
This patch is set to alter some core functionality which will be set until then next API change .. in ahem Drupal 9.0 ( fingers crossed )
but I don't feel qualified to add the appropriate @api tags.
So here is my changes :-
1) RouteMatchInterface I have added the suggested text because it looks good to me ( minor tweek - upcasted has become upcast - because it reads better to me )
2) I have added placeholder comments for the other missing class and interfaces ... they feel a little bare, so please feel free to augment/correct.
3) $arguments thas become $parameters.
Comment #78
martin107 CreditAttribution: martin107 commented76: 2238217_76.patch queued for re-testing.
In the 20 minutes between #76 and #77 I get 6 commits !!!
This is in the middle of the sprint mayhem that is Druplicon Austin 2014.
But as then as NOW this patch applies and passes my minimal match path test I used in #72.
The test report in #76 is too vague and not what I am finding locally so I am retesting.
Comment #80
neclimdulI was going to reroll this since the bootstrap patch broke it but I feel like there is a problem in the current approach. Going to try and grab some people at the con and see if I can get a better grasp on this and better enumerate my concerns on here.
Comment #81
kim.pepperI thought I'd re-roll just to see if we are still passing with the current implementation.
Comment #82
effulgentsia CreditAttribution: effulgentsia commentedClosed #2239009-12: Remove public direct usage of the '_system_path' request attribute as a duplicate. See that comment for details.
Comment #83
effulgentsia CreditAttribution: effulgentsia commentedFiled #2281619: Convert most direct usages within module code of routing related request attributes to use RouteMatchInterface instead as a follow up.
Comment #84
neclimdulSummary of my concerns and the discussion.
I have some... academic concerns I will skip but the practical problem was that currently we may need to call a method in a Request event listener that wants a RouteMatch object as an argument because of how closely these classes are connected. The happy medium we came up with is to create a
RouteMatch::createFromRequest()
method and use that in event listeners and other things that may only have a Request available. Being as that will contain the logic for getting route information from request data, the CurrentRouteMatch would call into that method, probably with some for of static cache of RouteMatch data objects.Comment #85
znerol CreditAttribution: znerol commentedThe route match is the result of the routing process (see #75). Introducing
RouteMatch::createFromRequest()
does not make much sense. A request event subscriber relying on the route match just needs to run after therouter_listener
subscriber.Comment #86
neclimdulThis is correct, it does have to run after the route_listener because the information would not exist on the request. However, the need is real and came up in trying to merge this into the bootstrap where the theme initialization is moved into an event subscriber.
The fact is that we've made the this tight grouping of data between RouteMatch and Request and we are requiring the RouteMatchInterface for accessing it. Event listeners however only have access to request information directly through $e->getRequest(). The fact that we see this here, illustrates how we could run into this in Symfony libraries, external libraries, or possibly in our own internal libraries. As a matter of DX, we need to provide this ability if we're going to implement this concept.
Comment #87
znerol CreditAttribution: znerol commentedThe theme initialization needs to run after routing (see #2016629-29: Refactor bootstrap to better utilize the kernel). There is nothing wrong with injecting the
current_route_match
into thetheme.negotiator.request_subscriber
service.Comment #88
msonnabaum CreditAttribution: msonnabaum commentedJust resolving a recent conflict.
Comment #89
msonnabaum CreditAttribution: msonnabaum commentedCleaned up CurrentRouteMatch to be more readable. No functional changes.
Comment #90
xjmHere's an interdiff from #88 to #89 (@msonnabaum's readability improvements).
Comment #91
xjmNeeds a docblock. :)
Comment #92
xjmAdding the docblock for the new protected method.
Comment #93
znerol CreditAttribution: znerol commentedI think
_raw_variables
also lives on request-attributes. This is attempting to fetch them from the query string.We badly need unit tests here.
Comment #94
xjmAgreed.
Comment #95
znerol CreditAttribution: znerol commentedSuggestion for the next iteration:
CurrentRouteMatch::getCurrentRequestAttributes()
, even less demeter for the rest of the class.Comment #96
neclimdulWorking on merging the patch I made with my understanding of the agreement effulgentsia, crell, msonnabaum, and I had during the code sprint.
Comment #97
neclimdulWhy you got to be a jerk d.o? Patches aren't really deleted... but they got taken out of the list because of a submit conflict. Sorry.
Ok, patch. Diff 1 vs 81, Diff 2 pulls in readability fixes. SplObjectStorage is weird class but it seems like a good method for the internal cache of RouteMatch objects.
Comment #99
neclimdulWoops. Better patch.
Comment #101
xjmSo still to do:
_raw_variables
getCurrentRequest()
in the cleanups.CurrentRouteMatch::getCurrentRequestAttributes()
(#93)Case typo here.
These members need a one-line description. Also for anyone else who went "huh?" http://www.php.net//manual/en/class.splobjectstorage.php
Comment #102
neclimdul1) Current bug is drush not having raw variables. @related 3) fixed.
2) Sure
4) Didn't seem to help much in this patch so left it off.
5) 6) fixed.
Comment #104
neclimdulI'm off this for the evening. Problem currently seems to be that our error pages(403,404) build requests and then kicks off additional renders but those new requests don't get routing information. This breaks when we get to initializing the theme system which in this patch requires a RouteMatch. Seems we could either make a RouteMatch not require routing... or not really sure.
Comment #105
znerol CreditAttribution: znerol commentedThanks @neclimdul for your hard work.
We might be able to break this circular dependency by adding an optional
$force_theme
parameter todrupal_theme_initialize()
. Anything which tries to render stuff before routing is then required to initialize the theme layer manually and supply an appropriate theme name. If$force_theme
is set, the negotiation step will be skipped.It is debatable whether it is legit in the first place to render HTML fragments before routing. ExceptionController::on403Html() does this at the moment if no custom 403 page has been configured. Also a lightweight Ajax callback churning out HTML fragments from within a request-event subscriber which runs as early as possible (i.e. before routing) seems a legit use-case. I think that it is justifiable to assign the responsibility of enforcing an appropriate theme in those cases.
Update: stack trace
Comment #106
znerol CreditAttribution: znerol commentedThere is another option if we agree that rendering of HTML fragments should be possible independently of whether a route was found and without actually having to initialize the theming layer manually beforehand.
CurrentRouteMatch::getRouteMatch()
would have to detect whether a given request already has been routed. If this is not the case, it could return anEmptyRouteMatch
(orNoRouteMatch
) which just implements stub methods. This actually would reproduces the current behavior.Instances of
EmptyRouteMatch
should not be recorded in the$routeMatches
map ofCurrentRouteMatch
. First there is no need for caching an empty implementation, second this allows for code which accesses current route match before or after routing to always get an accurate result.This might look something like:
The
isRouted()
method would e.g. check for the presence of_raw_variables
and/or route name/object.Note that we also have a third option: go back to #92 and leave out the caching and the coupling from
CurrentRouteMatch
toRouteMatch
for the moment.Comment #107
catchWhat about adding routes for the 403/404 pages? If there's a good reason against that then fine, but those pages are already very strange and the general approach to how they get rendered hasn't changed since i can remember.
Comment #108
effulgentsia CreditAttribution: effulgentsia commented#107 seems out of scope for this issue to me, but perhaps worth exploring elsewhere.
This implements #106.
Comment #109
neclimdulYeah @catch, that seems like the ideal solution but I'm concerned there's a use case hidden in there that would drag this issue into oblivion. Also, from my quick skim, those methods look really complicated which makes things worse.
I like this. I didn't notice at first but since this gets called in the cache miss and returns without settings the cache, we can recover if someone misbehaves and calls this on something route-able before routing happens. This whole block should be very cheap and infrequently called so it should work really well.
If we have consensus at this point, I'm OK with moving this to RTBC.
Comment #110
effulgentsia CreditAttribution: effulgentsia commentedI think this still needs unit tests. Looks like the tag was dropped accidentally in #96.
Comment #111
effulgentsia CreditAttribution: effulgentsia commentedWould it be better to move this into RouteMatch::createFromRequest() or do we not want RouteMatch having a soft dependency on NullRouteMatch? We could still implement the no-cache behavior in CurrentRouteMatch by having it check $route_match->getRouteObject() before caching.
Needless blank line.
Huh?
Would it be better to apply parameter filtering in the constructor rather than in each of these places separately?
Here and throughout comment module, seems like some unrelated patch hunks made their way into here and need to be removed.
Same for these.
Comment #112
neclimdul1) maybe... not sure.
2) cleaned
3) PHPStorm mangling things...
4) I generally try only do assignment during construction but this might be a good place to make an exception.
5) 6) gugh, I was diff-ing off a branch. looks like I messed up and was reverted some stuff. Cleaned up.
Comment #113
neclimdulComment #115
neclimdulcopy pasting around... should pay attention.
Comment #116
effulgentsia CreditAttribution: effulgentsia commentedJust a reroll for now. Working on tests.
Comment #117
effulgentsia CreditAttribution: effulgentsia commentedNo one objected to #111.1, so this implements that along with other minor cleanup. That helps with being able to unit test RouteMatch::createFromRequest().
Comment #118
effulgentsia CreditAttribution: effulgentsia commentedTests added.
Comment #119
neclimdulNice! Going to take a stab at splitting out the coverage.
Comment #120
effulgentsia CreditAttribution: effulgentsia commentedJust a reroll to keep this applying to HEAD.
Also a question: what do you all think of removing $request from the signature of ThemeNegotiatorInterface::applies() and ThemeNegotiatorInterface::determineActiveTheme()? It's something that pwolanin, Crell, znerol, and I discussed in Austin, but that was before neclimdul raised his concerns in #84/#86. With this patch, none of the negotiators in HEAD use $request within applies(), and only 2 use it within determineActiveTheme(): AjaxBasePageNegotiator and BatchNegotiator. Our thinking was that it shouldn't be a common thing for theme negotiators to depend on $request info that isn't in $route_match, and for ones that do need that, they can get $request_stack injected into their constructors and use $request_stack->getCurrentRequest() where they need to. In theory, it's more limited, because it means they'll only be able to use the "current request" rather than some arbitrarily passed in one, but in practice, is it actually a problem for AjaxBasePageNegotiator, BatchNegotiator, and a minority of contrib ones to have that limitation? Considering that what those negotiators need from $request is stuff like HTTP POST data, I can't think of a practical use case where $request_stack->getCurrentRequest() would be incorrect.
We could punt that question to a followup if necessary, but since this patch is already changing the signatures of ThemeNegotiatorInterface, we can avoid repeated API breaks to the same code if we settle it here.
Comment #121
effulgentsia CreditAttribution: effulgentsia commented@neclimdul: sorry, unassigning you was unintentional, and you don't appear in the list of people I can assign an issue to :(
Comment #122
tim.plunkett@neclimdul wasn't in the list of maintainers on d.o, but is in MAINTAINERS.txt for Queue system, so I added him :)
Comment #123
znerol CreditAttribution: znerol commentedFrom the issue summary:
So, yes. Completing the theme-negotiation API in this issue is in-scope in my opinion.
Note that this will result in a strange implementation of the
theme.negotiator.request_subscriber
service to be introduced in #2016629: Refactor bootstrap to better utilize the kernel. The event handler will operate on the constructor-injectedcurrent_route_match
while completely ignoring the passed inGetResponseEvent
. We could resolve this in a later step by introducing a new event type similar toKernelEvents::REQUEST
having route match on the event. I imagine than a new event capable of propagating route match also would lower the risk that the request event gets overcrowded by contrib subscribers.Comment #124
neclimdulOk got all the fires that kept me from digging into the tests put out.
This breaks out tests into units that can get @covers. @covers all the methods being covered. Uses a base class to cover all the methods in CurrentRouteMatch ever if the current implementation is just proxy-ing, and adds a couple of trivial assertions like checking getParamater() in the CurrentRouteMatch test.
It has 100% coverage of the new classes but you'll have to apply #2287385: Fix PHPUnit coverage tests to see that.
Comment #125
effulgentsia CreditAttribution: effulgentsia commentedThanks, neclimdul. Great test improvements! Looks like there are 3 duplicate implementations of getTestRoute() though, and none of them called from anywhere.
Also, do you have feedback on #120 (removing $request from ThemeNegotiatorInterface methods)?
@znerol:
That's not necessarily true, is it? Couldn't we still choose for that service to operate on
RouteMatch::createFromRequest($event->getRequest())
? However, it would mean that a specific negotiator that also received a constructor-injected $request_stack would theoretically (but not in practice) have request information from a different request than the request used for deriving the passed-in route match. Not sure if that's better or worse than having the event subscriber ignore $event->getRequest() entirely. Again, the whole thing is pretty academic, since in practice, $event->getRequest() is always the same as $request_stack->getCurrentRequest().Comment #126
tim.plunkettRerolled for #2278541: Refactor block visibility to use condition plugins, no additional changes.
Comment #127
neclimdulThanks for resolving that tim!
Yeah, those methods where from a early refactor and didn't get removed.
Comment #129
tim.plunkettWhoops, we can't rely on a route object coming back.
Comment #130
tim.plunkettActually, looking at the interdiff in #129, and at NullRouteMatch, I think we either need to make NullRouteMatch::getRouteObject() return an empty Route object (not sure if that's possible or even desired), or adjust RouteMatchInterface::getRouteObject() to mention that it can return NULL.
Comment #132
effulgentsia CreditAttribution: effulgentsia commentedAddresses #130. Related, changes NullRouteMatch::getRouteName() to return NULL instead of empty string.
Still waiting for feedback on #120/#125. I'm thinking I'll reroll with the $request removal in the next day or two if no one raises objections by then.
Comment #133
effulgentsia CreditAttribution: effulgentsia commentedStraight reroll for now.
Assigning to myself while I work on removing $request from ThemeNegotiatorInterface::applies() and ThemeNegotiatorInterface::determineActiveTheme().
Comment #134
effulgentsia CreditAttribution: effulgentsia commented$request removed!
If this comes back green, then I think all feedback has been incorporated, so this is ready for RTBC or new feedback.
Comment #135
znerol CreditAttribution: znerol commentedGreat patch, great progress. I also like the fact that this patch additionally removes the
_theme_active
request attribute.Tiny nitpick and out of scope here. I hate reading code with assignments as conditions. Especially if there is more than one condition.
In this special case it would be possible to simply move the assignment before the if, that would help readability a lot.
Also nitpick, and could be a quick-fix follow-up: Better align the docs here.
is nonsense (just like before the was nonsense.)This is ready in my opinion.
Comment #136
aspilicious CreditAttribution: aspilicious commentedAlso minor
Comment #137
catchI fixed the two minor docs issues on commit, the assignment I don't feel strongly about and it's already there.
Committed/pushed to 8.x, thanks!
Comment #139
tim.plunkettTwo things.
Comment #140
tim.plunkettAlternatively, instead of documenting the change to ThemeNegotiator, let's just fix it: #2292217: ThemeNegotiator::determineActiveTheme() should not require a RouteMatch to be passed in
Comment #141
xjmWe also need to update existing change records, e.g.:
https://www.drupal.org/node/2192317
https://www.drupal.org/node/2274705
https://www.drupal.org/node/2287827
https://www.drupal.org/node/2203305
And possibly others here:
https://www.drupal.org/list-changes/published/drupal?keywords_descriptio...
Comment #142
xjmAlso, should we close #2103301: Add a helper class to introspect a given request as a duplicate now? (And change the CR reference on that issue to reference this one instead.)
Comment #143
catchClosed that one as duplicate and updated the references.
I had it in my head that there was already a change notice for this but didn't actually check, I think I must have been thinking of the API examples from the comparison issue.
Comment #144
xjm@chx posted #2294777: The name RouteMatchInterface is confusing. Still need those change record updates and the new change record though. :) If we change the name later (and I'm not sure we will) then it's just a couple of string-replaces.
Comment #145
tim.plunkettGoing to work on this
Comment #146
tim.plunkettHmm, after updating all of the other change notices, I'm not sure what else we can say here.
https://www.drupal.org/node/2295317
Comment #147
xjmI tweaked the CR a little and then published, just polish: https://www.drupal.org/node/2295317/revisions/view/7395605/7396703
Few more things (I haven't followed up on any of this yet):
menu_get_item()
(this was a deficiency in the original CR but got worse when it was changed to mention pending change records).Comment #148
xjmTried to address #147.2, please double-check:
https://www.drupal.org/node/2023537/revisions/view/6714611/7396741
https://www.drupal.org/node/2067859/revisions/view/7368167/7396747
Comment #149
xjmStarted looking at #147.6 but every usage of
menu_get_item()
I looked at in D7 core and contrib was part of something completely different enough in D8 that the entire code flow has changed (e.g. usingdrupal_goto()
, futzing with titles, altering local tasks, etc.) So maybe we should reference the relevant CRs for those things and just give a relevant D7 menu_get_item()/D8 RouteMatch example in the CR. Started with this for now:https://www.drupal.org/node/2203305/revisions/view/7380117/7396859
Comment #150
xjmSo all done. :) Please double-check these and make any corrections/clarifications/improvements needed:
Comment #151
catch