Problem/Motivation
Follow-up to #1606794: Implement new routing system
As of right now, the new routing system passes attributes from the request (such as path argument placeholders) verbatim by name. That is all well and good, but is a regression from the Drupal 7 menu router where we can up-cast, say, an id to a node object. We want to do that, but be even more cool about it.
Proposed resolution
Symfony full stack has a mechanism we can likely copy that up-converts objects based on the type hint of the parameter in the controller. That's probably the optimal way to do that. I believe that code is in the FrameworkBundle, but I haven't looked at it in detail yet.
Remaining tasks
Implement the above.
User interface changes
None.
API changes
TBD.
Additional information on upcasting:
- https://www.drupal.org/docs/8/api/routing-system/parameter-upcasting-in-...
- https://www.drupal.org/docs/8/api/routing-system/how-a-node-id-gets-conv...
Comment | File | Size | Author |
---|---|---|---|
#158 | upcast-example.patch | 2.18 KB | effulgentsia |
#139 | 1798214-upcasting.patch | 6.89 KB | g.oechsler |
#138 | 1798214-upcasting.patch | 5.55 KB | g.oechsler |
#137 | 1798214-upcasting.patch | 3.98 KB | g.oechsler |
#135 | 1798214-upcasting-suggestion-135.txt | 10.37 KB | effulgentsia |
Comments
Comment #1
sunComment #2
Crell CreditAttribution: Crell commentedLooking through SensioFrameworkExtraBundle, which is where this code lives in Symfony, I'd recommend the following approach:
* Register ParamConverter objects in the DIC, with a tag.
* Each ParamConverter should be responsible for converting a single data type (eg, Node, User, etc.), based on reflection. (Symfony also does annotation-based conversion, but I think we can skip that for now.)
* Add a controller listener that takes the request, checks its parameters, and then calls the appropriate ParamConverters to convert the argument to its corresponding object.
For reference material see ParamConverterListener in SensioFrameworkExtraBundle.
Comment #3
Crell CreditAttribution: Crell commentedCourtesy of the plane right back from Symfony Live...
I'm not 100% sold on how I have things wiring up here, but it does seem to work. There are some @todos for later refactoring once more things are injectable. As implemented here, it should be trivial to support any entity (give or take entity refactoring to make the code injectable and therefore testable), and in concept anything that is a classed object type hint can be implemented fairly easily. (Think Views, Config objects, Configurable Thingies, individual Fields, Types, Widgets, plugins, hell even database connections if you really wanted to.) The only limitation is that it is a 1:1 match between an object class name and a converter. So everyone will always have the same converter for something type hinted as Foo. I don't think that's really a problem, and if you have some extra special weird case there are ways around that. (I can think of at least two.)
First patch is the one to review. Second patch includes the routing branch for testbot.
Comment #5
Crell CreditAttribution: Crell commentedForgot tags.
Comment #6
katbailey CreditAttribution: katbailey commentedTagging for PNW sprint
Comment #7
tnightingale CreditAttribution: tnightingale commentedRerolled against HEAD with a added try/catch to ignore cases where we don't have a registered param converter such as 404. 404 results in a ExceptionController being passed into the ParamConverterManager which causes an uncaught exception to be thrown causing all sorts of weird stuff in _drupal_log_error(). The caught exception just returns out of the param converter manager without doing anything allowing Drupal to continue to do its thing.
Comment #8
tnightingale CreditAttribution: tnightingale commentedStill sprinting at the PNW WSCCI Sprint :)
Comment #9
tnightingale CreditAttribution: tnightingale commentedThe green got me excited :( but empty patches always pass... trying again.
Comment #11
Crell CreditAttribution: Crell commentedCatching and swallowing an exception is rarely wise. It sounds like the issue is rather in the converter logic, which is making too many assumptions.
Comment #12
tnightingale CreditAttribution: tnightingale commentedYeah I had the feeling that wasn't going to fly :)
I am not familiar with the lifecycle of a ExceptionController, it seems to be handled elsewhere. Is it safe to assume that it has no business in the ParamConverterManager? If that's the case I guess there should be logic in the ParamConverterSubscriber to prevent going further for controllers that don't require param converting. Are there other types of controllers where this is the case?
Comment #13
Crell CreditAttribution: Crell commentedExceptionController should not be special cased. Rather, whatever it is about it that is making ParamConverter barf should be accounted for by ParamConverter. My guess is it's something bleedingly obvious (like, empty-array-handling levels of obvious), because my tests were incomplete. :-)
Comment #14
tnightingale CreditAttribution: tnightingale commentedHeh yeah, you're right, it is bleedingly obvious. I figured out ParamConverterManager just needs to check for one more condition; that whether the parameter actually needs converting to whatever the type hint says it should be. Tests are green on my local, just cleaning up the patch.
Comment #15
tnightingale CreditAttribution: tnightingale commentedHere's patch & interdiff from Crell's original patch
Comment #16
tnightingale CreditAttribution: tnightingale commentedComment #18
tnightingale CreditAttribution: tnightingale commentedwhoops, not sure how that typo got in there :-\ trying again while on the bus back to Vancouver
Comment #20
Crell CreditAttribution: Crell commentedLooks like we just need an extra is_object() in there.
Remember, unit tests are dead simple to run and very fast, so you can make sure all of those work before uploading a patch. :-)
Comment #21
tnightingale CreditAttribution: tnightingale commentedStill getting familiar with Drupal's testing framework but things are starting to make sense :).
I have the ParamConverter & ParamConverterManager unit tests green now though there is a 'Verbose message' debug notice - not sure what to do with that.
Comment #23
tnightingale CreditAttribution: tnightingale commentedJust working on rerolling patch against latest 8.x code...
Comment #24
tnightingale CreditAttribution: tnightingale commentedComment #25
tnightingale CreditAttribution: tnightingale commentedoh dear, sorry for my flailing around - still working on my patch workflow. That last patch was missing one change (the change existed on my local but wasn't staged when i made the patch). Trying again.
Comment #26
dipen chaudhary CreditAttribution: dipen chaudhary commentedWhile reviewing this I wrote a test to see if it upcasts user id to a user object, found out that only Node class was registered with Converter service. I've tried to make it generic by looping over entity info and registering the class name for all entities. I'm not sure if that was the right thing to do, there must be a better way. Also I saw refactor @todo for the time entity is injectable, for now maybe this could work? Attached is a interdiff and a patch, would love feedback.
Thanks
Comment #28
dipen chaudhary CreditAttribution: dipen chaudhary commentedThis is indeed breaking Drupal installation because of the changes in CoreBundle.php, not sure why?
Update: Fatal error: Call to undefined function field_read_fields() in core/modules/field/modules/field_sql_storage/field_sql_storage.instal
Comment #29
katbailey CreditAttribution: katbailey commentedYes, that code does not belong in the core bundle. What we need is a compiler pass for processing these paramconverters. If you look in /core/lib/Drupal/Core/DependencyInjection/Compiler you'll see a few examples of compiler passes. Looking through the patch, it's not actually immediately obvious to me how to do it, because it's not as if each entity-type-providing module (like node module) is supposed to register a new tagged service (which is what you usually do with compiler passes if you look at the other examples). But hey, figuring that bit out will be the fun part, right? :-P
Comment #30
tnightingale CreditAttribution: tnightingale commentedMoved call to entity_get_info() to a compiler pass, seems like the obvious approach.
Will need someone more familiar to pass judgement on whether it's the 'right' way to do this.
Comment #32
tnightingale CreditAttribution: tnightingale commentedSo I guess calling entity_get_info() in a compiler pass is functionally the same as doing it directly in CoreBundles.php :P
Comment #33
katbailey CreditAttribution: katbailey commentedHmm, how gauche an idea is this: each module that provides an entity type *does* register a separate service to the DIC in its bundle, but the service is created using a factory method that always returns the same instance of the entity paramconverter. And then it's tagged with the 'paramconverter' tag and a 'classname' attribute whose value is the fully-qualified class name of the entity. Something like:
Then in the compiler pass you go through all services tagged as 'paramconverter' and add the services to the paramconverter_manager.
Worth a shot?
Comment #34
Crell CreditAttribution: Crell commentedKat, that is underhanded, dirty, and evil. Let's do it. :-)
Comment #35
sunI don't really get that. Why isn't there a mechanism for converting dynamic request path parameters into data resources built-in in the Routing component? Doesn't it support dynamic arguments à la
{node}
already?Or are you aiming for #1821662: Register entity controllers with the DIC ?
An abstract "ParamConverter" service would not make sense to me. The point we need that, we've abstracted too much, way too much.
Comment #36
Crell CreditAttribution: Crell commentedThe Router itself doesn't do any upcasting. It maps variables based on name, but doesn't do any conversion for classed objects on its own. Symfony full stack has a system for doing so in SensioExtraFrameworkBundle, and what I've done here is modeled on how that works, perhaps a bit simpler.
This is basically a port of menu load callbacks, modeled on what Symfony does.
Comment #37
tnightingale CreditAttribution: tnightingale commentedI'm not sure I understand why a ParamConverter service is too abstract. Different types of parameters require different methods of loading - it's no different in concept to the *_load() method in D6/7.
In saying that, Kat's proposed approach seems a little obscure. I'm still getting familiar with the services and the DIC but it I'm worried that it won't be inherently obvious to entity-providing module devs exactly why they should jump through those hoops just to declare a param converter.
Comment #38
katbailey CreditAttribution: katbailey commentedWhile I think @tnightingale does make a valid point about making things complicated for entity-providing module devs, in the absence of any alternative suggestions, I've gone ahead and implemented my suggestion in #33. I've added a doc block to the NodeBundle which hopefully does a decent job of explaining what's going on. Really, all they'll have to do is copy and paste that code and change the service id and class name.
Comment #39
katbailey CreditAttribution: katbailey commentedComment #40
sunThis duplicates and hard-codes entity info into service container parameters. From an architectural entity system perspective, I do not think we want that.
Closely related:
#1763974: Convert entity type info into plugins
#1821662: Register entity controllers with the DIC
This particular change proposal still leaves me in a deeply skeptical mood. Providing the equivalent of router argument loaders is one thing. Turning that into an overly abstract, generic service that isn't even remotely related to the router, is an entirely different thing. I do not see, nor do I understand why we'd want to have an abstract service that is in itself only a meta-factory that (ab)uses the service container by leveraging service tags to retrieve an actual factory that is able to instantiate the class, which in turn, is ultimately responsible for turning a simple string into typed data. — Seriously?
Additionally, we're turning the service container more and more into a true God Object. This is guaranteed to get us into a bloody race condition hell. And not only that — it will render sites unusable, in unrecoverable WSOD states. You will need a Drupal core developer to recover from the broken cross- and intertwined-interdependencies that resulted in the un-buildable and un-parseable and/or un-compileable God Object. The only other remotely possible way to generally recover from that will be a
/rebuild.php
that performs loads of conditional checks on all subsystems, extensions, and services to figure out what exactly went wrong and what needs to be done to get the God Object functional again. Given where we're heading currently, that rebuild.php is almost guaranteed to be in core for D8, and, it will be frequently needed.So back to the point and this issue: Can we pretty please limit this?
- First and foremost, the class belongs into Drupal\Core\Routing.
- Second, why isn't this a property on the route that defines a callable, like we do have today?
In the end, why can't we have this?
If not, then I really need a clear and concise explanation for why this insane amount of abstraction is needed.
Comment #41
tnightingale CreditAttribution: tnightingale commentedWhile I think your concerns of service container abuse are valid, they should probably be discussed in a place more visible than this issue.
As to requiring a callable property for each argument on each route, that isn't like what we have today. Today we have a single callback per argument type that is based on a global argument/function naming convention. I would much rather see required argument objects created (consistently) based on reflection than force (allow?) developers to provide a unique callable for every route parameter they define. Not only is it overly verbose, but it opens itself for abuse not too dissimilar from that which we are seeing with the service container.
Comment #42
Crell CreditAttribution: Crell commented1) The container is not a God object, in that it doesn't "do all the things". It is just a giant index of all the things. That's fine, because the Container is still doing one and only one thing: Instantiating objects on demand, with lazy dependencies. That's it.
2) I see nowhere in here that there is a race condition. Unless there's a specific example you can point to, I call FUD. Nor do I see how there's going to be a registry-esque circular dependency of doom.
3) A factory that is container aware and does nothing but map things to other service IDs is not a problem, as discussed in #1810912: [meta] Decide on pluggability
4) We certainly could skip the lazy-service-instantiation and have the converter manager just depend on all the different converters. And then we'd have to instantiate every single one of them, and all of their dependencies, on every single request, even those requests that won't be upcasting anything at all. That's doubleplusungood for performance. This way, we only instantiate what we need.
5) I've been saying for 6 months we need to have a "Rebuild all the things" CLI command in core, and it should be build on Symfony's Console component. This issue is not the reason why, however. Compiling the container in the first place, compiling Twig files, and things like that are. (I wish someone would take me up on that and bring in the Console component. It would be extremely useful.)
6) Requiring people creating routes to specify the converter on the route is *more* work than they have to do now, where it is automated. That would be a regression, which is a no-go. Keeping it automated lowers the bar for module developers creating routes, as there will be about two orders of magnitude more people writing routes than writing new things that would need a converter.
7) The code to write a new converter this way is dead simple. It's about 4 lines of boilerplate in the bundle class, and then a small class that matches an interface. That's hardly complex.
8) I'm open to allowing entities to auto-register like we want to do with entity storage objects and such, but as currently implemented there is only a single converter for all entities anyway. Which means when defining a new entity, all you need is 4 lines of boilerplate bundle code and you're done. That's even easier.
9) The tag-and-it-just-works approach is something we're using in a couple of places, including all event subscribers. It's going to be very common.
Comment #43
klausiparameter type should include the full namespace. Variable name could be just $manager?
Should be "registered".
Why do you need sprintf() here? No long arguments are used, so it hurts readability here.
Do not use "self" where possible, use "static" to fully leverage PHP's late static binding.
So that means upcasting only works for concretely type-hinted parameters like "Node $node", but not for "EntityInterface $entity". So even if I have "{node}" in the route I will not get a loaded node into $entity :-(. I would get an error anyway, because {node} could not be mapped to $entity as the variable name differs. I conclude that it will not be possible to use upcasting with generic entity controllers?
wrong comment?
wrong comment
empty function?
each class should be in its own file, PSR-0 ...
And that has to be repeated for every entity type? I think there should be just one EntityBundle that registers all entity classes available in the system.
Comment #44
fubhy CreditAttribution: fubhy commentedMinor clean-up pretty much along the lines of #42.
Additional changes:
That is just temporary. Currently we can't loop on entity_get_info() / $container->get('plugin.entity.manger')->getDefinitions() because of the dependencies that comes with. #1821662: Register entity controllers with the DIC is basically blocked due to the same problems.
Two (important) things that this patch currently doesn't cover:
implode('|', array_keys(entity_get_info()));
and put that into the regex validation. However, that doesn't work anymore once you have multiple related arguments in the path that are built on each other (e.g. "{entity_type}/{bundle}/{field}").Comment #45
Crell CreditAttribution: Crell commentedComment #46
fubhy CreditAttribution: fubhy commentedOpened #1837388: Provide a ParamConverter than can upcast any entity. as a follow-up. I think that upcasting and validating whether or not a path actually exists are closely related to each other.
Comment #47
tnightingale CreditAttribution: tnightingale commentedRerolled against head - test routes are now in yaml.
Comment #48
tnightingale CreditAttribution: tnightingale commentedNoticed some of the style and commenting issues pointed out in #43 still hanging around. Updated patch.
Comment #49
Crell CreditAttribution: Crell commentedCan I just say that this block makes me feel warm and fuzzy inside? :-)
Throwing an exception is not "nothing". It's a rather significant something. :-) I think the code is correct, since this line won't be reached unless the system has been explicitly told to use this object to upcast that class. That means the comment needs to be revised.
If we're going to have a fugly static factory, at the very least we should use static:: here instead of specifying the class by name.
I think once those two are resolved I'm comfortable committing this patch and moving forward.
Comment #50
tim.plunkettIt should also be
public static function getParamConverter()
, with public first.Comment #51
fubhy CreditAttribution: fubhy commentedYahr, now that you point that out though I think we have to throw that out :(. Doesn't play well with entity info caching. We should not copy that data. Instead we should just do:
After all, that is still just a temporary solution. entity_get_controller(), entity_get_info(), etc. should be removed from that at some point anyways.
Addressed the other concerns as well and updated to "Contains" instead of "Defintion of" in @file.
Comment #53
fubhy CreditAttribution: fubhy commentedWhoops.
Comment #54
Crell CreditAttribution: Crell commentedSweet.
Comment #55
fubhy CreditAttribution: fubhy commentedCool. Now that this is RTBC I would like to draw some attention to #1837388: Provide a ParamConverter than can upcast any entity.! :)
Comment #56
sunWhy don't we use container parameters instead of separate service definitions per entity type for this?
I.e., why not simply this?
Comment #57
fubhy CreditAttribution: fubhy commented@sun: We won't have the ability to override upcasting for specific entity types anymore (not sure if that is even a use-case but still... we would remove that option). Also, parameters should rather be used for injecting single values into the DIC. We would (ab)use them as tags with this. The only way to filter out the right parameters would be to iterate over all of them and then doing something like strpos() to filter out the correct ones.
However, I am uploading a patch which uses parameters instead. Not suggesting we should use it, I am just uploading it for people to see what the difference would be.
Comment #58
fubhy CreditAttribution: fubhy commentedOh well, the Factory would be redundant too of course.
Comment #60
Crell CreditAttribution: Crell commentedParameters are a no-go. For one, as fubhy says that's an abuse of parameters. They're not supposed to serve actual objects, just small "config lite" type data.
For another, converters will have dependencies. The entity converter, for instance, will want to have a dependency on the entity storage manager object, once that becomes injectable. For it to have dependencies that are not hacked in, it needs to be a service in the Container. That kills parameters right there.
The patch in #53 is still RTBC.
Comment #61
sunYes, I understand that the entity parameter converter has dependencies.
A single entity parameter converter service is vastly different from a dozen or more copies of the exact same entity parameter service, which, each, need to be defined in a very special way, involving a non-trivial amount of expected arguments. Failing to meet that exact, custom expected service registration for any particular entity type results in broken implementations. This is undocumented and not "documentable" in the first place — it's an extrapolation of arrays of doom to registered services on the DI container.
What I do not understand is why we have to re-register and re-define the entity parameter converter for every single entity type that exists in the system.
This adds a large amount of duplicate code all over the place, involving a poor DX, and registers a dozen or even more services that do not differ in any way.
On top, the manually specified class name for the entity parameter conversion is architecturally not exactly right, since it duplicates the entity type metadata/information in a spot that is completely detached from the rest of the entity system. I do not understand why we're hard-coding the entity class for each entity type into the router to begin with, and why this construct does not rely on the existing entity type metadata/information instead.
Comment #62
Crell CreditAttribution: Crell commentedIf you can come up with a clean way for bundles to dynamically register services based on some other information (eg, entity_get_info()), that would simplify the logic here and probably elsewhere as well. I would like to see such a mechanism.
Baring such a solution presenting itself, however, we cannot let this issue sit indefinitely because we aren't sure about the part that is easiest to change later. (DIC registration should impact nothing but the DIC, by design.)
Comment #63
sunThose keywords essentially trigger a whole other question: Is it even legit to register entity-type-specific services in a bundle?
Clearly, without hard-coding and duplicating entity info into those bundles, the bundles would not be able to register anything at all, since the information they require to register anything in the first place can only be supplied by other services that may be registered already or not — a clear race condition and circular dependency.
Which in turn leads to the question whether entity-type-specific param converters should be registered through bundles on the DIC at all.
The proposed change looks and feels like a big mistake to me.
(Btw, I also don't understand the rush here — these kind of changes are not bound to feature freeze, we have to do them anyway.)
Comment #64
catchNote I've not done a full review here but there's several things on a quick look through the patch that make me uncomfortable, so I'm setting back to CNR for more discussion at least
It's also missing any input from the existing menu system maintainers. Only sun has done significant work with the existing menu system in this issue and his concerns appear to have been brushed over (although they were a bit dramatically stated, but still there's plenty that's valid in there).
First the off-topic bit:
@Crell
That's exactly what #1831350: Break the circular dependency between bootstrap container and kernel is. This issue isn't the same problem as that, but it's not as if these issues don't exist. While the DIC/module list dependency issue is 'fixed' (via a hard-coded config file storage call, not exactly pretty and that was my idea at BADCamp :(...), I don't think we're out of the woods there by any means. Most of the stuff we're using was not designed for code being dynamically loaded based on configuration in the way Drupal does and we're running into issues everywhere with that. For another example #1846376: Namespaces for disabled modules are registered during the first request after a module is disabled. It's a shame to see those issues being dismissed so easily as FUD.
Now the patch:
I don't think this hunk is acceptable at all, it's a massive, massive developer experience regression compared to having a function call mything_load() and pointing the menu callback at it. Why does this have to be completely alien to the route definition?
It also goes completely against the aims of #1838676: Support dynamic entity type management, which just about every Entity system patch is slowly getting us closer to being able to do.
So let's discuss this some more and see if we can't have the entity system handle it centrally based on entity info.
Apart from that, I don't see any discussion in this issue on how this will integrate with access checks for menu links. The only way (at least at the moment, and no-one's proposed anything different that I know of) to check access on a menu link is to load the full object then run access checks on it - since the access check usually depends on the object, especially for entities.
For some discussion of how things currently work and some of the performance issues with it see #638070: Router loaders causing a lot of database hits for access checks or #881468: Performance: Static cache output of views_arg_load() (or many other issues).
So to get the object from a menu link, it looks like we'd have to mock the request object now?
I also don't understand the hurry here at all, it's a regression that the new router system can't do this, it might even be critical unless we decide that's a feature we want to drop.
Comment #65
Crell CreditAttribution: Crell commentedRestoring tags...
First: The reason I called FUD on the circular dependency claim is that it was provided without context or evidence. "There's a problem here, but I won't say what or how" is FUD. I still don't see anywhere in this patch that is a circular dependency. If the problem is dynamically defining entity services, then yes that is a circular dependency/race condition (I'm not sure which it would be, technically), but that's exactly why we're manually defining the converters here. The patch here has no circular dependency that I can see.
The "rush" here is because at some point we're going to have to unleash a horde of contributors on core to convert menu callbacks over to the new system, and I really do not relish telling them "well you have to take in ints and create objects yourself like in Drupal 5, sorry, oh wait we fixed that now so you have to go back and redo it." Even if this is treated as a regression and therefore not affected by feature freeze, converting enough other code over to the new router that we can remove the old menu_router system is affected by code freeze, which will be here frighteningly soon.
As for dynamically defining new Entity types, I'm happy to go on the record as thinking that's actually a bad idea and is taking flexibility to an absurd degree, which would run into problems such as, yes, circular dependencies in registration. At some point, "stop clicking and just write some damned code" is the answer to some problems, because otherwise you end up in an "Enterprise Rules Engine" and find yourself on TheDailyWTF because no one can understand anything.
Yes, it would be nice if it were automatic. However, the same challenge exists for other parts of the entity system already, such as entity storage controllers. (There's an issue for that somewhere but I couldn't find it off hand.)
As for the specific implementation, doing upcasting based on the controller signature has been part of the road map since the meeting we had back at DrupalCon Denver, which included Menu System co-maintainer Peter Wolanin. The specific implementation here dates from late September, and is very loosely modeled on the upcasting system in Symfony Full Stack (in the SensioFrameworkExtraBundle, IIRC, although I think it also supports annotation based upcasting as well). This is not a new or unconsidered approach.
As stated in #42, I don't believe putting the upcasting rules on the route is reasonable. We cannot rely on the placeholder names, because those are used to match to controller arguments and cannot be duplicated. So assuming {node} should translate to a Node (basically what we do in Drupal 7) would mean you can never pass 2 nodes to one controller, which is clearly not an option. The DX cost for adding a new converter is non-zero, I agree; however, there will be on the order of tens of people writing new converters; there will be on the order of tens of thousands of people writing new routes. While it would be technically possible to define the upcasting rules on the route, that's an awful lot of work to put on an awful lot of people.
For menu links... I don't have a solution there. It did occur to me that the request dependency was potentially an issue, however, passing $request->attributes instead doesn't help because that's a ParameterBag object, not an array. That means we would either have to make anyone using the upcasters create an HttpFoundation ParameterPag, and then probably convert back to an array, or we would have to make the upcasting system convert the ParameterBag into an array, run conversions, and then convert it back, something I didn't think would be viable for performance.
Comment #66
Crell CreditAttribution: Crell commentedSeriously, Drupal, what is up with your tag handling...
Comment #67
catchThere's already a Drupal 7 module that people use which does this, which has just under 1,000 users, so are we saying that's not possible even in contrib then? And not only that, are we saying it's impossible to define anything dynamically that will require upcasting request arguments?
I assume we could get around this in some way by having a uri like entity/node/1.
If that's doable it might be an acceptable limitation, but if not this needs a lot more consideration before committing a regression.
OK well we need to come up with one...
That sounds roughly like what I had in mind, we should try it, profile it and see whether it's viable for performance or not.
Having menu links that point to a 403 page is not going to be releasable. We're likely to take a performance hit with menu links as entities anyway, so we'll need to find some ways to ameliorate that (probably more aggressive caching at this point unless someone comes up with something amazing). The object / array conversion doesn't sound like it'd be particularly bad - it's certainly not in the magnitude of having a menu link pointing to a view with 100 displays.
Comment #68
effulgentsia CreditAttribution: effulgentsia commentedThis is a reroll of #53 to chase HEAD. Also renamed EntityParamConverterPass to RegisterEntityParamConvertersPass for consistency with other compiler pass names. CNR for bot.
Comment #70
Crell CreditAttribution: Crell commentedJust a note: g.oeschler is working on a new direction here based on the WSCCI meeting last week.
Comment #71
effulgentsia CreditAttribution: effulgentsia commented#68: 1798214-68.patch queued for re-testing.
Comment #72
effulgentsia CreditAttribution: effulgentsia commentedYay. I haven't worked on this at all other than the #68 reroll. I just asked for a retest, cause I can't reproduce the test failure that bot crashed on last time. I'm looking forward to seeing what g.oeschler comes up with.
Back to needs work, since that's the real status. #68's status change was just for bot.
Comment #73
Crell CreditAttribution: Crell commentedThis is currently blocked by: #1855204: RouteBuilder needs to respect route options set in yaml file
Comment #74
effulgentsia CreditAttribution: effulgentsia commentedI'm assuming none of the Drupal committers will agree to release Drupal with 2 routing systems, one that upcasts and one that doesn't, so I think that makes this critical.
Comment #75
klausiwe have #1848648: [meta] Convert menu system to new routing system as critical placeholder.
Comment #76
g.oechsler CreditAttribution: g.oechsler commentedThis showcases the idea of upcasting based on guessing the class we want to convert to from the variable name.
Let's suppose that most of the time controllers will take common things like users and nodes as parameters. So given a routing pattern /operate/on/some/{node} we could just guess that Drupal\node\Plugin\Core\Entity\Node was meant.
For sure there will be cases where this will not work, so we need to give the ParamConverter a hint what it should do. This is achieved by adding options to the route definitions. That's why this patch contains #1855204-14: RouteBuilder needs to respect route options set in yaml file.
This implementation actually manages to upcast {node} by guessing and {other_node} by hinting, but is catching an Exception elsewhere. See ParamConverterTest::testCanRoute().
This is just a prototype! I'm fully aware that this passes the target class name, rather than using the respective paramconverter.entity.* services.
Comment #78
g.oechsler CreditAttribution: g.oechsler commentedrerolled
Comment #80
Crell CreditAttribution: Crell commentedWe can definitely do a better job of guessing here. :-) The question is how.
Possibilities I can see:
1) Converters provide a list of placeholder names that they will "claim". During, uh, some build process we generate that list and stick it in the DIC, then inject it into the manager. If we find a token that isn't in our lookup list, then we look to the route definition.
2) Some other mechanism than Converters provides a default list of placeholder=>converter mapping. Not sure what.
The problem with option 1 is that I don't know how we can get it into the DIC nicely, since bundles don't like being dynamic and Drupal does.
Here's a thought... Converters can expose the placeholders they will "claim". Then we at some point iterate those and store that list in the KV system? It's not quite cache data, but, eh, maybe it is. I don't know. :-) I'm just trying to avoid an n^2 algorithm for determining which placeholder goes to which converter.
Comment #81
Crell CreditAttribution: Crell commentedAlso, if we rely on the guessing and the route for mapping information and not the controller signature, then we can move the conversion from the controller event to the request event, with lower priority than the matcher but higher priority than the access checkers. That would be the ideal place for it, because then access checkers have access to the upcast objects.
Comment #84
webchickThis is apparently blocked on #1874500: CMF-based Routing system.
It's also apparently required for SCOTCH.
Comment #85
webchickComment #86
sdboyer CreditAttribution: sdboyer commentedyep, confirming this is necessary for SCOTCH...at some point. not immediately.
Comment #87
EclipseGc CreditAttribution: EclipseGc commentedConditions is moving along and has the beginnings of a legitimate context system in it. The sooner this is in, the sooner we get to figure out how routing can feed that sort of context system and by proxy, blocks will likely inherit the exact same mechanism. Sooner is much much better than later.
Eclipse
Comment #88
Crell CreditAttribution: Crell commentedThe blocking patch is in, so this is a go. I just talked to g.oechsler in IRC and he's working on a new Route Enhancer-based approach.
Comment #89
xjmComment #90
g.oechsler CreditAttribution: g.oechsler commentedHere's upcasting based on the new Symfony-CMF Routing.
Right now there are three phases for upcasting:
This will upcast the node id from pattern to a node.
This will upcast node id from node to a node.
For the signature SomeController::someMethod($article) the pattern '/some/{pattern} would return an error because the Symfony router expects a parameter called 'article'.
In this case we can map the arguments to the right name.
Here we get a node object in $article.
Comment #91
BerdirI think it should now be possible to inject the entity manager and get the entity controller through that...
the entity_get_info() can afaik not yet be injected, requires changes to the manager, which are planned.
Coding style :)
Should be done as a @file docblock. It's kinda weird but we require that every file has one.
Comment #92
Crell CreditAttribution: Crell commentedNice, work, George! I like what I'm seeing overall. Some comments:
For most of the Symfony-derived code, we're using priority, not weight. Same idea, but it sorts the other direction (high numbers win). We should go with priority here for consistency with other Container-registered services.
We should probably do this with another compiler pass so that we can pick up an arbitrary number of route enhancers.
I think this needs to be an isset($attributes[0]['weight'] check, which of course means we can't use a shortened ternary. Oh well.
Strictly speaking this is $entities, since it's an array. That would also help clarify the next line.
Don't inline an if statement. Either break it out to multiple lines or use a ternary. In this case I think you can use a ternary.
I don't think this comment is necessary.
Docblock first line should be a single line.
Since in this model we are running all converters all the time (since we expect there to be far fewer of them), we can probably drop it being ContainerAware and just pass in instantiated objects from the DIC. That keeps this object from being polluted by the container, and shouldn't be any different performance-wise since we're still instantiating the same number of objects via the DIC. We're just moving around where that happens.
First doc line needs to be on a single line.
This converter both excites and scares me. (Read into that what you will...) On the one hand it's quite cool. On the other, I fear that it would lead to confusion since sometimes variable names match up an sometimes they don't.
Either way, I don't think this needs to be a ParamConverter. It's a renamer, which means it can be its own Route Enhancer, shouldn't it?
We should probably use the interface constant here instead of the string, for consistency.
What does ~ mean? That's not at all self-evident.
This is exactly the sort of confusion I'm afraid of. :-) Perhaps we can punt "map" to a separate issue?
Comment #93
sun~ means NULL in YAML. Part of the YAML spec. Not sure why we'd declare anything anywhere with a NULL value (instead of omitting the key/property altogether), but if that's API-wise legit/needed in the first place, then ~ is perfectly fine.
Comment #94
Crell CreditAttribution: Crell commentedSilly YAML. I don't think the NULL/~ is needed here, then, unless we actually want to do some sort of conversion on that parameter. If we're not converting it, it should just not be specified.
Comment #95
g.oechsler CreditAttribution: g.oechsler commentedThe idea behind
node: ~
is to explicitly prevent an argument from being upcast. EntityConverter would see "node", find a matching entity and perform upcasting on it. But if the EntityAliasConverter running before EntityConverter finds this option, it will mark node as upcast without actually touching it. This definitely need documentation.Btw I'm not really happy with the name EntityAliasConverter. Maybe we should call it ExplicitEntityConverter or something. Any suggestions?
Comment #96
Crell CreditAttribution: Crell commentedI think in that case, rather than mapping it to a NULL converter we should just say "use a different name". If $node is a node object, put in a parameter of {nid} and then you know that you'll get an int, as you expect. That also means your controller would get a $nid, not a $node, which helps with DX because you know which you're getting. In fact, you can even then add a requirement that nid be an int via regex. That's what the requirements block is for. :-)
Comment #97
Crell CreditAttribution: Crell commented@g.oechsler: Any sign of a revised patch here? Or do you want me to take a crack at it? (If so, make sure there's a clean branch in the WSCCI repo and let me know what it is.)
Comment #98
g.oechsler CreditAttribution: g.oechsler commentedWhew, lots of homework. :)
Comment #100
g.oechsler CreditAttribution: g.oechsler commentedThere are two failures in CollapsedDrupal\language\Tests\LanguageUILanguageNegotiationTest.
I have no idea how this could be related to upcasting. Just re-running the test to make sure this was no random failure.
Comment #101
g.oechsler CreditAttribution: g.oechsler commented#98: 1798214-upcasting.patch queued for re-testing.
Comment #103
Crell CreditAttribution: Crell commentedThis is looking great, George. Some stylistic comments below, but nothing show-stopping.
I also asked Kat to have a look at the failing tests here, since they don't make sense to me either.
Coding standards say this should be $entityManager, not $entity_manager. (Object properties are lowerCamel, like methods.)
Same as above.
This can now be reduced to a call to $this->entityManager->getDefininitions(). Yay, one fewer function call.
I think we can remove the "service" part of this method name, since we're adding objects, not services. So just addConverter(), and change $service to $converter.
For long code lines like this, Drupal convention is to just "let them be long" rather than breaking them out to multiple lines, most of the time.
If you want to shorten it, I would be OK with using an inline {$node->uid} in the string itself. That would make it easier to read as well as shorter. Ibid on the other lines further down.
Comment #104
g.oechsler CreditAttribution: g.oechsler commentedThe fail of LanguageUILanguageNegotiationTest it strange.
If I take both converters out of the DIC it passes. If only one of them is active the test fails, although it's not even running code from the converters. *shrug*
Comment #105
g.oechsler CreditAttribution: g.oechsler commentedYou added a patch which is testable by our state-of-the-art continous integration test environment. Are you sure you don't want to change the status to "needs review"?
[X] Yes [ ] No
Comment #107
katbailey CreditAttribution: katbailey commentedThe problem with the UI language negotiation test is that that particular language negotiation method calls current_path() to check whether it's an admin path. The current_path() method checks to see if we're in request scope, in which case it returns the system_path attribute of the request, however with this upcasting stuff happening, a situation arises where we are within request scope but the system_path attribute has not been set on the request yet, and the EntityManager makes a call to language() for caching purposes - this then invokes the language negotiation method which eventually ends up making the current_path() call which retrieves the value NULL. If this explanation is too long and rambling, just see the interdiff :-)
Comment #108
Crell CreditAttribution: Crell commentedOh that legacy code...
Awesome work, George and Kat! I think this is ready.
Comment #109
fubhy CreditAttribution: fubhy commentedI am afraid that you might hate me for this but I strongly believe that this is not the right way of doing this. I know we really need to find a solution for this quickly but at the same time I am really worried about this piece of code because it's such an essential part of the API and one of the things that developers will have to touch a lot. DX and stability is crucial here...
What bugs me most is the whole concept of priorities. We haven have two layers of these it seems... once for the enhancers, and then once more in the param converter manager. That doesn't look stable at all. We don't want to get to a place where our developers have to worry about the priority of their converters and the naming of their {slugs}.
I am also worried by the guessing going on here... That, together with priorities simply screams 'race condition'.
I find this also a bit strange.. Having to coupe with this $defaults array and managing and populating that in every converter properly doesn't look right to me either. There shouldn't be a need for something like that. Writing a new, custom converter should really just be as easy as returning as receiving a value and returning an upcast object/whatever from it. We certainly don't want make our converters responsible about that metadata.
Okay.. So I don't want to spoil the party without offering an alternative:
I just had a short conversation with fago about this issue and we concluded that we should probably consider going with TypedData all the way. That would mean we would run upcasting through the TypedDataManager... That way we don't only cover entity types out of the box but basically cover everything that can be expressed as typed data plugin (which should be more or less everything that we have in Drupal at some point plus simple types like lists, strings, booleans, etc.). We would then be able to run our upcasting through the createInstance() logic of the typed data plugin manager. Unless I am missing something here we would not need any custom logic for loading stuff...
So, when defining a route we would basically only declare the typed data type of a {slug} in the route options and be done with it...
I am not a TypedData expert but according to fago there are also concepts for loading things based on conditions or to load lists of things easily.
I am happy to elaborate more on this or even provide a patch if this sounds appealing. However, there are still a few TypedData issues that have to be solved before we would be able to fully leverage that awesomeness (e.g. #1868004: Improve the TypedData API usage of EntityNG).
Comment #110
fagoRegarding loading - nope you don't need to handle this, you just need to call getValue() on a typed data object. I'm not into this issue, but from my distant-point-of-view relying upon typed data would make sense here.
This is how entity loading via typed data works right now:
Once #1845546: Implement validation for the TypedData API is in, we could even think about leveraging validation here. Not sure this makes sense here performance wise though.
With #1868004: Improve the TypedData API usage of EntityNG it would work just with type "node", but I'd suggest supporting whole data definitions anyhow as this gives us more flexibility and would also support lists or custom "computed property" classes, such that you can easily plug-in in your own "load XY" logic.
Comment #111
fubhy CreditAttribution: fubhy commentedOkay... So what we would have then is a single ParamConverter service into which we would inject the TypedDataManager. In that ParamConverter we would basically read the typed data definition array(s) from the route options and iterate over them to upcast the respective {slug}'s accordingly. All that said ParamConverter have to do would be to pass those arrays to the TypedDataManager->create() method one by one.
"What if my {slug} does not have a TypedData plugin and is therefore not know by the typed data API?" Well, write one... :)
So, I am 100000000++ for outsourcing this data handling and loading to TypedData API. In fact, I am so intrigued by this idea that I am going to write a patch in the afternoon.
Comment #112
Crell CreditAttribution: Crell commentedTo fubhy's concern about double-priorities: Two entirely different things. 99% of developers don't need to worry about RouteEnhancers, just a converter. In fact, 99% of developers won't need to write new converters, either. Any entity Just Works(tm). We are not going to see a proliferation of dozens of converters (or if we do, it's because someone doesn't understand how to use them properly); I'd be surprised if more than 2-3 appear in contrib, total. (I could see one that converts a comma separated list into an array, for instance, but can't think of many others. Although that's probably a Route Enhancer directly.)
Earlier versions of this issue tried to have a converter-per-parameter. If you look at the earlier patches, you can see they were much more complex and required per-entity-type registration in the DIC. This approach does not. That makes it a better DX than what we were trying to do before.
To the question of using entities vs. typed data: Separate patch. Frankly I find the "all objects with data should be entities" push to be very wrong, and pushing typed data on it as well worries me greatly. I am not sold on that here at all, but we have to move forward.
This issue works, the architecture is clean, and it unblocks SCOTCH as Kris has been harassing me for weeks that he needs working upcasting. The details of the EntityConverter can be tweaked for months without breaking any APIs, and from the comments above there are other needs-work and not-even-a-patch-yet issues that would be needed there. We cannot hold up this issue any longer on issues that are not yet in.
fubhy: If you want to tweak the EntityConverter, please put that in a follow-up issue, not here.
Comment #113
Dries CreditAttribution: Dries commented1) There are some code style issues in the patch, and variable names could be cleaned up for better readability.
2) Code style issues aside, I think this patch could be simplified. We could easily combine the explicit and implicit entity converters into a unified entity converter that does both in a single pass. In other words, merge the two classes into one class and do both from a single process() function. It seems unnecessary, wasteful, and more complex to take two separate passes.
3) I'd also like to discuss the term 'converter'. I'm not sure it offers the best UX. At least in the case of entities, or anything else that is stored in the database, it is a lot more like 'loading' (vs 'converting'). If we used the term 'loader' it would be more consistent with Drupal 7 and optimized for the 80% case, which is loading things from the database. I also don't like the word 'converter' in the YAML files. In the YAML file, we're really mapping a token to a type, rather than converting a token to a type.
Comment #114
Crell CreditAttribution: Crell commentedDries: What code style issues and/or variable names? We need something to go on for what you find insufficient.
I don't think the 2 passes are as complex as you make it out to be, since they're now just method calls; they don't need to go through the event system separately. I'll have to check with George for if that would complicate the code or not to merge them.
Converter vs. Loader: Meh, I have no preference.
Comment #115
sunThanks for the hard work on this so far! :)
Spent some time reviewing this patch in some more depth. I think we're on the right track, but as @Dries already mentioned, I also think there's a lot of room for simplification. Substantial room, even.
Aside from the architectural/technical issues that I'll note below, the code/patch could really use some love with regard to its coding style and documentation. Let's bear in mind that the routing system always was and remains to be a very complex part of Drupal's architecture.
Therefore, I do not feel comfortable with introducing a substantial base layer that claims to be able to handle - pretty much - THE most important aspect of the routing system, without substantial documentation, which makes it crystal clear how the individual parts are supposed to be used (and how they're NOT; i.e., what's a legit param converter, and what's not), and how the individual pieces are wired together in the end.
As I'll note below, I also think that the actual parameters that parameter converters receive (duh) are too obscure right now. You can replace $defaults with array $options, and doing so translates into how other developers are interpreting this API. $variables and $options are somehow extracted and derived out of $defaults, which turns $defaults into something that's way beyond Array Of Doom™. From an API and architectural perspective, that simply looks wrong and we need to provide a proper API for upcasting/loading arguments.
The other detail I'm concerned about is that the decision of whether an argument has been converted already, or not, is left to parameter converters. In terms of architecture, those converters should not be aware of that. They simply take an input and convert it into an output, if applicable. It is not their business to validate whether it is even valid to convert the input in any way. That logic must be performed by the wrapping/routing layer already.
Overall, I think we've made good progress here in the meantime. But I also think that there is still a lot of work to do for this patch. I do not consider it ready yet, mostly from an architectural perspective.
I do not understand why we would want to fall back to the non-request-aware path, if there IS a request scope.
This would imply that we're unable to resolve a path, even though there is a request. I do not think that this situation should be possible.
If there is a request, then we must be able to resolve a path.
1) Using tags for this registration looks very odd. Are we re-inventing the EventDispatcher with a custom-flavored ContainerBuilder tagging mechanism?
2) Without looking into the other changes of this patch (which probably resembles the 99% experience of all developers who will have to deal with this), I am not able to make sense of "ExplicitEntityConverter" vs. "EntityConverter". Both are in the same namespace, and one of both just happens to be registered before the other, so what gives?
3) A priority of 10 does not leave sufficient room for all of contrib to sneak their way in. Should be bumped to 100.
I am able to relate to the idea that architectural purity might be able to see a difference between "dynamic routes" and "converters for parameters of dynamic routes", but honestly, that's architectural purity only. The separation makes absolutely no sense for Drupal, and I cannot see why we need two different classes/services that are responsible for doing one and the same job.
A "dynamic router" has to be able to convert parameters. Otherwise, it is simply not dynamic. By design. And dictated by simple, human, non-technical logic.
s/$bucket/$priority/
This track record should not be necessary.
The routing system should already know which parameters it upcasted already.
Also, the phpDoc states the direct *opposite* of what the code is doing.
1) Wow.
2) I wouldn't have expected that it is possible to call methods on an array key of $defaults (!) - denoted by a constant of another class.
3) A
compile()
method that is effectively executed at runtime, on every single request, looks bogus. Anything that needs to be compiled should be precompiled already, or be cached appropriately.4) Why aren't $variables and $options injected into param converter classes? The code looks too generic, so it doesn't look like a responsibility for each of them to figure that out.
Minor: Various (but heavy) code style issues here and elsewhere.
Looking at this interface (and the implementations), I have the following problems:
1) Incomplete documentation.
2) I can make sense of $request (even though it is not type-hinted), and for now I assume that it is what I assume it is.
3) I do not know what $defaults is supposed to mean, what it contains, and where it comes from.
4) I do not understand why I should accept "defaults" and, alas, return $defaults. If parameter converters are supposed to alter existing data, then the data should be taken by reference instead of being returned.
5) In general, however, the code gives little to no idea what is supposed to be done by classes that implement this interface/method. The actual implementations/examples did not help to understand it, since they are too complex/specific to begin with.
Comment #116
Crell CreditAttribution: Crell commentedsun: Thank you for calling out some of the variable name/style issues explicitly.
As to some of your other points:
1) Tagging as a registration mechanism is increasingly common. Route Enhancers and Route Filters do the same, as does the serialization system, and a few other things. When the number of implementations of something is small (as in this case), it's simpler and faster to not introduce a dependency on the event dispatcher (which adds non-trivial if manageable overhead) and just register the objects directly. In this sort of case, whenever the ParamConverterManager is instantiated we are pretty much guaranteed to need to call all of the associated converters,, and there's a small number, so this keeps the complexity down and performance up. An event would be more appropriate if those dependent objects were only sometimes called. Looking at it, I agree that there's not a clear-and-obvious lien to draw between the two scenarios. That's something we may want to discuss elsewhere, and talk to the Symfonians about.
2) "A priority of 10 does not leave sufficient room for all of contrib to sneak their way in." By design, "all of contrib" won't be adding converters. Because this code handles *all* entities, anyone writing a new entity gets conversion support for free. (Contrast to the the original code that required every new entity type to get a new registration in the DIC, which you objected to.) I can only think of one, maybe two other converters people would even want to write, and in most cases the priority won't actually matter. If we expected dozens of converters to be running at once, we would have to change the architecture entirely (probably to be more like the access checker objects with a compile-time pass, but let's not do that if we don't have to).
3) "Dynamic router" has nothing to do with upcasting. It's the name of the class Symfony CMF chose. I have no idea why. :-) Frankly I think it's a misnomer, but c'est la vie.
4) The "compiled" route cannot be cached with the route object. I tried originally and it caused a fatal error trying to deserialize. I therefore added code up in Symfony itself to strip the compiled route when serializing the route object to the database, because it broke otherwise. However, the route is already compiled by the routing step earlier and saved as a property on the route object, so there's no extra cost to calling it here. I think the code you highlight would likely be easier to grok if we passed the route object to each converter as its own method parameter, which is a fair point and would probably eliminate your concern there.
5) To the architectural point of pass-based converters rather than mapping individual arguments to a converter: We've tried it both ways now. You don't like either one of them. Suggest something else. As is, this is simple, fast, and "just works" for the majority case. Bringing back lots of smaller converters would probably make it *slower*, not faster.
I will also note: The best is the enemy of the good. This issue has been open for 4 months. It's holding up SCOTCH/context work (or so Kris keeps telling me). We have something here that works, is simple, and is fast. We can tweak the architecture to our heart's content for months, but as is this issue is a critical and we should not hold it up on ensuring that it's a perfectly polished diamond. As with access checkers, we need to be able to move forward and not sit here forever on this one issue.
Comment #117
sunCan you clarify to which part(s) of my review this maps?
It's not clear to me — it's possible that you're interpreting more into #115 than I actually mentioned? (which is perfectly possible, as we're coming from two completely different perspectives; i.e., neck-deep into the [new] code + neck-deep into the [old] code :))
I also want to stress on the fact that we definitely need this, in order to move forward. I'm also aware that we're doctoring on this for a long time already. However: As I've tried to clarify, this issue/patch forms the architectural basis for pretty much the entire functionality that Drupal delivers (since by design, Drupal is based on dynamic routes and stuff). As such, it's not that kind of arbitrary class + service that we can simply slap into system — the moment we add this, an armada of contributors will start to make sense of it and try to convert existing routes to it (give or take the lack of an access controller; that's a detail which doesn't matter at all). And like any other code that is changed, some other core contributors will run into neck-deep details of the dynamic router and param converter systems (because their stuff suddenly breaks, even though it did not before), and they will need to be able to understand what's going on.
So, overall, I think we're (relatively) close, could do with some simplification wherever possible [less manager classes?], better input/output parameters and variables, a more controlled flow of what needs and what gets converted, definitely some good chunk of documentation, and we should finally be able to move forward. :)
Comment #118
Crell CreditAttribution: Crell commentedQuoting from #117:
Quoting from #63:
We can either have a small number of converters that work across a wide swath of objects with a simple system (the latest patch), or a large number of converters that work on specific objects with a more complex system (the patches from a few weeks ago, before RouteEnhancers). But so far no one has come up with a small number of converters that work on specific objects in a simple system. If you have a brilliant idea for that you've been hoarding, please share. :-) If not, I refer back to "the best is the enemy of the good". (On this point; the variable naming and passing the route in as a separate parameter I agree with. We may also want to remove the request from the values passed to the converters, so that the converters can be used outside of the request for rendering links.)
I'm also not too worried about module developers starting to convert to this and it then changing; the number of people/patches that would be affected is quite small, since the actual controllers would be unaffected either way; they still just get a node, and don't care where it came from. As with the D7 menu system, the vast majority of module devs don't go deep into the bowels of the system, nor should they have any reason to.
Comment #119
g.oechsler CreditAttribution: g.oechsler commentedAs I read the last few comments, I get the impression that there's a major missconception of what this patch (#107) does and what not.
So here's in short what it does with some symfonic prologue: (@Crell: Please correct me if I'm wrong, I'm explaining it to myself right now...)
The heart of the new style routing is a service registered as "router.dynamic", which is a Symfony\Cmf\Component\Routing\DynamicRouter.
The method DynamicRouter::matchRequest(Request $request) is where it happens: You hand in a Request, you get back a route object. Bingo: Routing done.
Unfortunately that's not entirely true: You get back an "array of doom" called $defaults, holding the route object, the parameters, the machine name of the route and the controller responsible for processing it. Before this $defaults array is returned, we get the opportunity to alter these defaults. They are passed alongside the incoming request to the route enhancers. Route enhancers have to implement RouterEnhancerInterface which looks like this:
<scope of this patch>
The 'paramconverter_manager' allows bundles to register parameter converters to run in specific order. See Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass.
ParamConverters have to implement ParamConverterInterface which looks like this
Yes, that's basically mimicking RouterEnhancerInterface. It was introduced to have a clear distinction between RouteEnhancers and ParamConverters to avoid confusion. But in the end it's operating on the same data. (And yes, it's missing type hints, I'll fix that!)
</scope of this patch>
I have no idea if anyone will ever want to run a converter between these two. But if so, this architecture won't stop them from doing it.
So, Dries, this is why it was made like this. Surely it's possible to merge these two converters. If you are willing to trade this (admittedly excessive) flexibility for one object and a method call, let me know.
Quoting #115:
Sun, would you please point out where you found the "substancial base layer [...] to handle [...] THE most importing aspect of the routing system" in this patch? I think that's too much honor. It's just upcasting. Routing works perfectly fine without it.
Uhm, looks like excessive omission can be misleading. Look at this:
The distinction should be clear from the explanation above. Just to make sure that the options set in YAML take precedence over the "guessing" of the entity type by name, ExplicitEntityConverter has to run before EntityConverter.
There is a test with a ridiculously made up edge case to showcase this:
Now that I looked over the code again and again to argue against the last comments, I found some of the code style issues myself. (Although I don't consider an inline else as heavy issue, but that's probably because I'm an old yaph. :P)
There will definitely be a reroll, but before this we should agree on naming.
I'm not sure if "Loader" is a good choice: ParamLoaderManager sounds like something that provides parameters after loading whatsoever. Here we provide something which is loaded by taking the parameters as argument.
What came to my mind was the term "Inflation": Converting parameters to "fully blown" entities.
So what about ParamInflatorManager, ParamInflatorInterface, EntityInflator, ExplicitEntityInflator?
Comment #120
Dries CreditAttribution: Dries commentedSo, Dries, this is why it was made like this. Surely it's possible to merge these two converters. If you are willing to trade this (admittedly excessive) flexibility for one object and a method call, let me know.
Yes, to me it felt like we are over-engineering this, hence my suggestion to have a single EntityConverter. Why would someone want to plug in another converter in between these two specific converters? If they want to change the behavior, they could also register another Converter before and/or after this one, or even replace this Converter with a completely new one. It just feels like we're going a bit overboard with offering flexibility, at the cost of the developer's experience.
Comment #121
Crell CreditAttribution: Crell commentedThe Symfony equivalent system is called ParamConverter:
https://github.com/sensio/SensioFrameworkExtraBundle/blob/master/Request...
https://github.com/sensio/SensioFrameworkExtraBundle/blob/master/EventLi...
I'm fine with leaving it at ParamConverter for consistency. Dries, if you want a different name please just state one. This does not need a bikeshed, it just needs a decision.
One addition to George's opening explanation: The $defaults array is, technically, the merge of the default values from the route and the values provided by the path via placeholders. The values of this array get added to the $request->attributes variable after the enhancers do their thing. That's why it's called $defaults in the Route Enhancers. I'm fine with renaming it in the converters, though, to help separate them.
I also talked with fubhy in IRC. He's going to try replacing the entity converter with a typed-data converter in a follow-up patch. I am fine with such experimentation once this patch gets in.
Comment #122
g.oechsler CreditAttribution: g.oechsler commentedI merged the two converters and took care of the coding style issues.
Also, Crell proposed eliminating the request from ParamConverterInterface to make it easier for access checkers to use this outside of the route enhancing/request pipeline.
Comment #123
Crell CreditAttribution: Crell commentedI think I agree with sun that we shouldn't reuse the $defaults name here. That conceptually couples it to routing too tightly. We will want to use this in the generator, too, most likely.
Rather, something like "an array of values to convert to their corresponding objects, if applicable." And then the variable should be $values, since from this perspective that's what they are.
Comment #124
fubhy CreditAttribution: fubhy commentedI opened #1906810: Require type hints for automatic entity upcasting as a follow-up.
Comment #125
fubhy CreditAttribution: fubhy commentedI just had a chat with @goechsler. Here is the (simplified/slightly modified backscroll)... Note: We agree to move ANY of this (if we even agree on it / consider it) to a follow-up. Architecturally, even though I don't agree to some parts as described in the backscroll and previously in this issue we should still move forward. My concerns are just nuances and can be fixed later as this already works they way it is.
Comment #126
g.oechsler CreditAttribution: g.oechsler commentedHere's ParamConverterInterface with a renamed parameter and I was able to save a few bytes of whitespace, yay! For it is sunday, I added an interdiff.
Comment #127
fubhy CreditAttribution: fubhy commentedSince we agreed to solve the remaining concerns in the follow-ups (I've yet to set up another follow-up for simplifying the converters field of responsibility @see comment #125) I am okay with settings this back to RTBC. Here comes my last nitpicky review for now.
We are missing an empty line there. Also: "Entity manager which _performs_ the upcasting in the end." Note the missing "s".
The modified variables now, eh? :)
The $converters could use a docblock.
"Add_s_ a converter to the paramconverter service."
Comment #129
g.oechsler CreditAttribution: g.oechsler commentedThis only addresses the nitpicks from #127.
I have not looked at Drupal\openid\Tests\Upgrade\OpenIDAuthmapUpgradePathTest yet.
Comment #130
Crell CreditAttribution: Crell commentedLooks like that was just testbot instability again. Sigh.
Bad me for not mentioning this earlier, but would it be better, since we're changing the signature anyway, to make $converted its own array rather than tucked into the $variables array? If it's better this way that's OK, but it's worth mentioning. We can also RTBC this as is to get things unblocked. :-)
Comment #131
g.oechsler CreditAttribution: g.oechsler commentedI just wondered, if this might eventually start to shine, if we polish it any longer...
Comment #132
Crell CreditAttribution: Crell commentedLet's try this again...
Comment #133
catchThis looks great overall. I noticed a lot of nitpicky stuff, some of which is below but I didn't include everything, also one question about the enhance() method on the ParamConverterManager.
This isn't really for registering of entity route parameters at all.
I really like this. It feels a lot nicer than a service per entity type, and it's easy to follow.
s/exist/exists/
I don't really understand this summary, really a lot of the phpdoc from the entity converter could be moved to here if it was slightly more generic.
Summary isn't one line.
The enhance method gets a request object, but that isn't used anywhere. Does this mean then that when we're upcasting router objects for menu links (which is going to have to happen for access checks) then we'll need to mock the request object each time? It'd be good to avoid that, and seems a shame to be doing it even if our default implementation doesn't even require it.
"prevent ... to process", then successful is mis-spelled later. I'm not doing a full comment review nitpick atm but there's a lot of grammar issues in the commments generally.
"with this the signature.". Also while it'd be fun to have a function called f() that seems like an unnecessary abbreviation here.
Comment #134
sdboyer CreditAttribution: sdboyer commentedfinally got to reviewing this - yay!
i'm really happy with what i see here. parameter upcasting as a step in route enhancement, with its own layer of indirection to support multiple approaches to upcasting, but with a sane default for entities. yay. nicely enough, i also see exactly what i need to do in order to set up the route enhancer for displays :)
as catch said, a number of nits on docs. i didn't detail those, either. however:
nit: it's weird that the name of this variable containing upcasted/converted variables is
$converters
. over in EntityConverter it's called$converted
, and that's much better. let's change it to that here.Comment #135
effulgentsia CreditAttribution: effulgentsia commentedI don't want to un-RTBC this issue, because I'm fine with remaining work being done in a follow up. However, posting my own DX improvement suggestions here as an interdiff on top of #131, in case committers kick this issue back for more DX polish. It doesn't address all of #133, but does indirectly address some of it and #134.
Comment #136
catchThis blocks so many things I've gone ahead and committed it. However let's leave it as a critical task until there's a follow-up with the feedback post #131 'cos it's not quite shiny yet ;)
Comment #137
g.oechsler CreditAttribution: g.oechsler commentedIt's in! :)
Now, back to polishing. Wax on, wax off ...
Comment #138
g.oechsler CreditAttribution: g.oechsler commentedSorry, incomplete patch. Let's try this.
Comment #139
g.oechsler CreditAttribution: g.oechsler commentedHow about moving the code to an other method and glueing it to enhance()?
Comment #140
fubhy CreditAttribution: fubhy commentedCan we create a follow-up issue for that stuff and do the change notification first?
Comment #141
sunSome other issues of #115 were also not addressed yet. The two I care about most are:
1) current_path() falls back to the non-request code when in request scope, which doesn't make sense to me.
2) The documentation on how paramconverter implementations work should be moved entirely from the EntityConverter implementation into the interface.
Comment #142
webchickThis has been waiting for a change notice for over 3 weeks now. Let's please get this knocked out.
Comment #143
dawehnerI changed the original change notice for the routing system a bit: http://drupal.org/node/1800686/revisions/view/2555920/2587906
Comment #144
Crell CreditAttribution: Crell commentedI was kind of surprised that this was still sitting here. I looked #139 over and it looks good to me. sun, I don't know how you feel about your comments in #141.
The change notice looks OK to me, so I'm dropping the priority on this down to major.
Also retesting, just to see what happens.
Comment #145
Crell CreditAttribution: Crell commented#139: 1798214-upcasting.patch queued for re-testing.
Comment #146
Crell CreditAttribution: Crell commentedComment #147
webchickLooks like catch may be best to review this one to make sure it's shiny. :)
Comment #148
catchVariables is passed by reference in the phpdoc, not in the method signature, and then it's returned anyway. Let's pick one.
Comment #149
fubhy CreditAttribution: fubhy commentedAlso, while we are at it let's change this in a way that allows ParamConverters to only interfere with {slug}'s. Currently it can basically inject anything into $defaults which renders ParamConverters rather pointless because they are more or less able to do the same as full-fledged RouteEnhancers (the only difference basically being the obligatory HttpNotFoundException() that's automatically invoked if upcasting fails).
Something along the lines of this:
EDIT: That was nonsense.. Will post a patch for that.
Comment #150
xjmUgh, let's really file followup issues instead of repurposing the original issue into followups. It took me several minutes to figure out what the status was here. @g.oechsler or @fubhy or someone else, could you please file the followup as requested in #136 and move these patches there? I don't think the current patch is major, but actually I really have no idea.
Comment #151
fubhy CreditAttribution: fubhy commentedOkay, sure!
#1943846: Improve ParamConverterManager and developer experience for ParamConverters
Comment #152
xjmThanks @fubhy! :)
Comment #153
webchickEr wait. I think this is still CNW as of #148.
Comment #154
xjm@webchick, the followup issue includes the patch that is CNW as far as I can see. There was already a commit in this issue back in #136. (Not changing the status in case I am wrong.)
Comment #155
webchickOops my mistake. I thought it was just for fubhy's concern in #149. Cool then. :)
Comment #157
Gábor HojtsyI've been looking at implementing this for edit module, but since there are custom loading arguments needed there (type/id across entities for example), we need the fullest API used there. The code comments in the custom example does not sound very promising...
It would be great to get some eyes on this example, since it is crucial for the conversion of Drupal and the contrib modules and *especially* for security.
The edit module uses a requirements key (http://drupalcode.org/project/drupal.git/blob/refs/heads/8.x:/core/modul...) and the access class validates and upcasts the path arguments (http://drupalcode.org/project/drupal.git/blob/refs/heads/8.x:/core/modul...). This was made up before this solution landed obviously, but I'm not seeing how we could apply the new approach based on the docs.
Comment #158
effulgentsia CreditAttribution: effulgentsia commented@Gabor: here's an example of how you can integrate {entity_type} and {entity} into the existing EntityConverter. To do {field_name} and {langcode}, I suggest waiting on #1906810: Require type hints for automatic entity upcasting, unless there's some pressing need to get to it sooner. If there is that need, you'd need to create your own class that implements ParamConverterInterface and register it as a service with the 'paramconverter' tag. You may also be interested in #1943846: Improve ParamConverterManager and developer experience for ParamConverters if you run into DX difficulties with that.
Comment #159
effulgentsia CreditAttribution: effulgentsia commentedThe access check service's access() method receives a $request. Everything it needs should be available in $request->attributes.
Comment #160
Gábor HojtsyThanks @effulgentsia for the tips, I've opened #1979566: Use request upcasting functionality in edit module with a slightly improved patch :) Adding test coverage as well.
Comment #161
pbcelery CreditAttribution: pbcelery commentedAdding links to help with context or learning about upcasting. They're
- https://www.drupal.org/docs/8/api/routing-system/parameter-upcasting-in-...
- https://www.drupal.org/docs/8/api/routing-system/how-a-node-id-gets-conv...