Updated: Comment #38
Problem/Motivation
Contrib (and even optional core modules like config_translation) have no uniform way to add and invoke custom controllers.
EntityType does not have bespoke getters for controllers, and is missing setters for some.
If an EntityType class is swapped out and wants to override a controller-specific method, it will be completely bypassed by EntityManager.
Furthermore, these entity controllers share a lot more in common than they did initially.
For example, every single type of controller (excluding forms, which use FormInterface/ContainerInjectionInterface) need the module handler, but half use setter injection and half pollute their constructor.
Proposed resolution
Add EntityManagerInterface::getControlller() to help contrib/optional modules
Add getters and setters to EntityType for core controllers
Add setModuleHandler to all, not just half, of the controller types that require it
Fix config_translation to be the example of best practices, and not a clever workaround
Write tests to cover it all
Remaining tasks
N/A
User interface changes
N/A
API changes
Changes
EntityManager::getControllerClass() is replaced by EntityManager::getController()
Drupal\config_translation\Exception\InvalidMapperDefinitionException is now Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException, and is to be used whenever a plugin definition does not meet expectations (instead of the previous usage of \InvalidArgumentException)
The 'list_controller' property of hook_config_translation_info() is gone, you should use hook_entity_info() or hook_entity_info_alter() to set the 'config_translation_list' controller:
function mymodule_entity_info($entity_info) {
if (isset($entity_info['node'])) {
$entity_info['node']->setController('config_translation_list', 'Drupal\mymodule\MyModuleConfigTranslationListController');
}
}
Additions
List, Access, Storage, ViewBuilder controllers can implement the following two methods if needed.
They are required for EntityForm controllers:
/**
* Sets the module handler for this controller.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*
* @return $this
*/
public function setModuleHandler(ModuleHandlerInterface $module_handler);
/**
* Sets the translation manager for this controller.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager
* The translation manager.
*
* @return $this
*/
public function setTranslationManager(TranslationInterface $translation_manager);
EntityType has getters and setters for the 5 core controller types, and has() methods for the optional ones (hasFormClasses() hasListClass() hasViewBuilderClass()).
Comment | File | Size | Author |
---|---|---|---|
#52 | entity-2168333-52.patch | 141.29 KB | tim.plunkett |
Comments
Comment #1
tim.plunkettI like the idea! Here's a patch.
Comment #3
EclipseGc CreditAttribution: EclipseGc commentedrerolling the patch against head. This should not fix any tests.
Eclipse
Comment #4
EclipseGc CreditAttribution: EclipseGc commentedFixed a handful of tests. Setting needs review to get a complete list of still failing tests.
Eclipse
Comment #6
tim.plunkettOkay, scaled this back drastically after talking with msonnabaum, dawehner, and EclipseGC.
Basically, \Drupal\Core\Entity\EntityType is a domain object, and should not have instantiation logic.
EntityType will still get specific controller getters, but they will return the class name. EntityManager still handles the instantiation.
One other improvement from the patch is maintained, and that is bringing list controllers inline with form and access controllers, and using setter injection for the module handler.
Retitling, but the issue summary still needs an update.
Comment #8
tim.plunkettComment #9
tim.plunkettSo we need a better name for this. I'm open to suggestions.
Comment #10
EclipseGc CreditAttribution: EclipseGc commentedWhy not just do this in getDefinition?
Eclipse
Comment #11
tim.plunkettThis compromises and adds a param to getDefinition to throw an exception.
It turns out that ever view builder and storage class also needs the module handler, making everything but forms the same. So I streamlined that.
config_translation, once again, was doing weird stuff, so I tried to clean that up. It helps highlight how contrib should define and invoke custom controllers.
Comment #12
tim.plunkettAfter some discussion on IRC, decided to repurpose InvalidMapperDefinitionException as a generic exception.
Comment #15
tim.plunkettFixed the bug, and wrote a full EntityManagerTest.
The diffstat looks bad, but that's 878 lines of new test coverage.
Comment #16
tim.plunkettThe old title is one thing this does on the path towards this goal: Contrib should be able to sanely provide custom controllers.
config_translation is an example of how it was not easy, so workarounds and hacks were added.
This cleans them up, provides the ability for contrib to follow suit, and adds a good amount of test coverage.
Comment #18
tim.plunkettThe class_exists() check @dawehner asked for broke my entity form mode code. Fixed here.
Comment #19
yched CreditAttribution: yched commented- getList() / getAccess() - makes no real sense, doesn't return a list, doesn't return an "access".
I don't think we want to be rehashing the whole controller/handler/(null) debacle here. For now all of those are still called controllers, why don't we stick to that here, and rename in the issue that's supposed to rename ?
- Great thing that we unify how those various objects are instanciated, but:
Some receive a moduleHandler, some don't ? Yet they all have relatively equal chances to have a need for invoking a hook or an alter ?
Similarly, it's common for one controller to need functionnality provided by another controller, thus needing to get back to the EntityMangar. Could we inject the entity manager in all of those ?
(hm - or is that covered by the fact that they receive the EntityType, which now gives you shorthands to any controller ?)
Comment #20
tim.plunkettThose are getters for the similarly named properties. There are already setters with those names.
AFAIK, all of the non-form controllers receive the module handler. All can easily opt into it by implementing a setModuleHandler method...
With the exception of the list controller needing the storage controller, I don't think any controllers ever need the other controllers.
They all can use injection to get whatever they want, as in HEAD, this is not changed here.
Comment #21
tim.plunkettRerolled after #1862202: Objectify the language system.
The EntityManagerTest now has 100% coverage.
1071 lines of tests added.
Comment #22
tim.plunkettWhoops, forgot a couple assertions.
Comment #23
dawehnerThere seems to be documentation left from the original code.
I kind of think that we should still check whether the $definition['class'] exists.
Oh since when don't we use unset() anymore?
Any reason why this was not added onto the interface?
If we need to do that, we should better inject the theme handler and call listInfo in case it is needed.
What does this test?
<3
Comment #24
yched CreditAttribution: yched commentedre: getList() / getAccess():
It's sad that those went in, then... Be it for getters or setters, those name are really unfortunate.
The method names should at least hint that this is about getting and setting classes.
getController() / setController() / getForm() / setForm()
...is pretty misleading.
EntityManager::getController() gives you an object
EntityType::getController() gives you a class ?
EntityManager::getListController() gives you an object
EntityType::getList() gives you a class ?
DX / consistency --
Comment #25
tim.plunkett1) Fixed
2) Added
3) Reverted
4) Fixed
5) Fixed
6) Removed
7) Actually, I removed this as well, since it required overriding the property to make it public, and it didn't really test anything. That was per @msonnabaum's request.
Comment #26
tim.plunkettgetListController vs getList is very clear that they return different things.
The EntityType::getController() vs EntityManager::getController() is unfortunate, but *neither are added in this patch*. That problem is in HEAD, and is really more the territory of #1976158: Rename entity storage/list/form/render "controllers" to handlers
Comment #27
yched CreditAttribution: yched commentedSure, it's very clear that getListController() & getList() return different things, but very unclear what getList() actually returns.
EntityTypeInterface is a messy collection of methods IMO, but well, it's partly in core already, so we go further on that road and leave the cleanup for the already disastrous & hypothetical #1976158: Rename entity storage/list/form/render "controllers" to handlers ?
OK, why not.
Comment #28
tim.plunkettEntityTypeInterface is pretty much just "let's put get/set in front of the old annotation key names". That, plus a few helpers, and that's it.
Comment #29
tim.plunkettFixing a couple docblocks, hopefully the last patch...? :)
Comment #30
fagoThat's quite confusing. Why is the getter necessary, couldn't we go with the controller type only?
+1
Agreed, everything returning a class should have a Class suffix, .e.g. getControllerClass(). Not necessarily part of this patch though?
Comment #31
tim.plunkettThis is internal-only; it just ensures that core uses the custom getters the same as anyone else, in case they are overridden in some way.
@msonnabaum and @dawehner agreed with you in IRC, and @yched said this would address his concerns above. So I went ahead and did that as part of this patch, as we were touching 90% of those lines anyway.
Comment #32
yched CreditAttribution: yched commentedNo, actually - this needs to run on FieldDefinition (base feilds), and not on Field / FieldInstance config entities.
So not checking FiedDefinitionInterface is intentional.
I might be missing something, but I'm not sure I see the interest of the $controller_getter, since the $controller_type param is required anyway ?
Passing a "getter method" name feels a bit weird + wouldn't the generic getControllerClass($controller_type) work anyway ?
At any rate, if that param stays, it should be renamed $controller_class_getter ?
Not really related, but can't those be plain ->invokeAll() now ?
Why not just NULL ?
Inconsistency:
- the generic hasControllerClass() includes a class_exists() check.
- the generic getControllerClass() calls the above and thus includes the class_exists() check
- the specific getXClass() methods call getControllerClass() and thus include the class_exists() check
- but the specific hasXClass() methods are stricly isset() based, thus without the class_exists() check
Should be getStorageClass() ?
Still not familiar with UnitTests, but is it standard to include several classes / interfaces in a test class ?
Comment #33
Berdir3. invokeAll() is not strictly the same as that, the current code theoretically allows to receive the arguments by reference if a hook wants to. Not sure if we do, but it's there right now...
Comment #36
tim.plunkett1) We try really hard to not use concrete classes for things like this, it's a shame setName() isn't on any interface at all :(
I removed the @todo for now though.
2) As I said in #31, "This is internal-only; it just ensures that core uses the custom getters the same as anyone else, in case they are overridden in some way."
But good point on the variable name, changed it.
3) As @Berdir said in #33, that was originally done on purpose, leaving it here.
4) Changed, and updated the docs.
5) Thanks! Those were hurriedly added, I didn't think that through.
6) Thanks, that was the one failure of the last patch.
7) Yes it is, we do this very often.
Comment #37
yched CreditAttribution: yched commentedAnswers & interdiff in #36 work for me.
Many thanks for the setters renames :-)
Temptatively setting to RTBC then...
Comment #38
tim.plunkettUpdated the API additions and changes section.
Comment #39
tim.plunkettTagging
Comment #40
tim.plunkettComment #41
msonnabaum CreditAttribution: msonnabaum commentedThis is a regression. It's always a good idea to wrap access to properties like this in methods, especially in this case where the class has no dedicated factory and provides no default when the setter isn't used.
The old version of that method should be restored on all of these.
Comment #42
tim.plunkettFair point. Adding an abstract class EntityControllerBase for now, this should become a trait as soon as we can.
Comment #43
msonnabaum CreditAttribution: msonnabaum commentedMuch better. Moving back assuming tests pass.
Comment #44
damiankloip CreditAttribution: damiankloip commentedI actually think your changes to set the properties to NULL is correct. Using unset on a property removes it entirely - not really the same as resetting.
Kind of related: This is why I changed stuff in the ViewExecutable::destroy() method a while back; to get a more accurate reset.
Comment #45
fagoAwesome!
I see. It's quite ugly, but when it's internal only it shouldn't be a big deal.
Looking up the patch it appears to be on the interface also though, where it shouldn't be then. -> setting back to needs work.
Why do those methods not follow the get-pattern? I've seen that's not something new, but when we introduce a new base class it should follow current best practices. So, any reasons for not going with the pattern here?
In other patches those have been
Does $this work with IDEs as well?
Comment #46
damiankloip CreditAttribution: damiankloip commentedSee #2158497: [policy, no patch] Use "$this" and "static" in documentation for @return types for more details and discussion on the whole $this thing in docblocks
Comment #47
tim.plunkettI think you misunderstood me. EntityType::getStorageClass is a public method and should stay on the interface. I meant that the $controller_class_getter parameter of EntityManager::getController is internal.
We use getters for properties of the object itself, not for wrappers of services. See ControllerBase for example.
damiankloip pointed you at the issue for @return $this
Comment #48
fagoyes, I was talking about that as well. So if it is internal, it shouldn't be on the interface, right? But the patch adds it to EntityManagerInterface:
So this needs work then.
Not sure I see why that makes a difference, but anyway that's not related to this issue then.
yeah, thanks!
Comment #49
tim.plunkettSure...
Comment #50
tim.plunkettThis was just a documentation change and was exactly what fago asked for.
Comment #51
aspilicious CreditAttribution: aspilicious commentedSomething went wrong, look at the patch size. I also see some twig changes in the beginning of the file shouldn't be in there.
Comment #52
tim.plunkettForgot to rebase.
Comment #53
tim.plunkettOpened #2178795: Allow DiscoveryInterface::getDefinition() to throw an exception for an invalid plugin to discuss moving this upstream.
Comment #54
webchickThis is really helpful debugging information and I'm hard-pressed to think of a reason why you would NOT want it. (In fact, I have some DX issue open about the utter inability to debug a plugin-related error for exactly this reason.) It's weird to a) have a parameter for this, b) have it default to FALSE, and c) not do this on all plugins, not just this one. Tim's opening up a follow-up issue to discuss this pattern and its applicability across all plugin types (he did originally try to do this everywhere, but ran into issues like if (
$this->getDefinition('invalid')...
)Uh, WAT? :)
Here again we're doing weird things here that we don't in other plugins. I asked Tim more about this and he said that in #2005716: Promote EntityType to a domain object we added getters for these class names and so it makes sense to use them, vs. calling
$entity_type->getControllerClass('storage')
like everyone else does. Not sure I get that. Coming back to it in a bit.Ok, talked through with Tim. Basically, the crux of the issue is that config translation introduced its own custom info hook and started monkeying with entity_info properties there, like list_controller. It did so because it couldn't call getController since that wasn't public (now fixed), and added setter methods to mirror the getter methods added in the previous issue, so that it can use those instead. Fixing this accessor issue for config translation hopefully helps fix it for anyone else in contrib who has to pull similar shenanigans.
This is a pre-existing condition, but it'd be great to understand *why* we are special-casing those particular things. A small follow-up for that would be nice.
I love that we replaced like 7 assertions with 1200 lines of full test coverage. :) NICE! There's even a few 'banana' references in there, awww. ;)
OK, I can't find anything commit-blocking here. I'm not 100% sure I grok every line of this patch, but definitely get the gist and it seems solid.
Committed and pushed to 8.x. Thanks!
I don't *think* we need a change notice for this one, as it's mostly internal refactoring, but it wouldn't hurt to look over the various entity documentation to ensure something doesn't need to get updated.
Comment #55
tim.plunkettAwesome! Thanks.
Well #2005716: Promote EntityType to a domain object hasn't had a change notice written yet, and that's the main one that would have been updated. I'll cross-post there.
Comment #57
xjmThe followup issue never got filed for replacing
EntityControllerBase
(nowEntityHandlerBase
). I filed #2471663: Undeprecate EntityHandlerBase for that.https://twitter.com/xjmdrupal/status/497852222600790016 :)