Problem/Motivation
Currently the OO variants of entity_load() and entity_load_multiple() have rather poor DX:
$node = \Drupal::entityManager()->getStorage('node')->load($nid);
Proposed resolution
It would be very sweet if load() and loadMultiple() were provided as static methods on the entity type classes, allowing to use the following instead:
$node = Node::load($nid);
We should also deprecate the current entity type specific implementations of entity_load() and entity_load_multiple() such as node_load(), user_load() and friends, and add suggestions to use the new static methods instead.
Remaining tasks
Finish the tests, review the patch.
API changes
All entity type specific load functions are deprecated and replaced by static methods on the EntityType objects.
// D7
$user = user_load($uid);
$nodes = node_load_multiple($nids);
// D8 (up to now)
$user = \Drupal::entityManager()->getStorage('user')->load($uid);
$nodes = \Drupal::entityManager()->getStorage('node')->loadMultiple($nids);
// D8 (now)
use Drupal\user\Entity\User;
use Drupal\node\Entity\Node;
$user = User::load($uid);
$nodes = Node::loadMultiple($nids);
Comment | File | Size | Author |
---|---|---|---|
#69 | 2190313-69.patch | 61.43 KB | amateescu |
Comments
Comment #1
amateescu CreditAttribution: amateescu commentedBased on the pseudocode proposal from #2144677-2: Add per entity type managers to improve DX, this patch should actually work.
Comment #2
amateescu CreditAttribution: amateescu commentedTurns out
is_subclass_of()
is not so smart :/Comment #5
BerdirAs mentioned in IRC, let's move this logic to a protected static method, so that the create() issue can use it too.
Comment #6
amateescu CreditAttribution: amateescu commentedHere you go :)
Comment #7
BerdirOk, based on a discussion with @timplunkett, this won't work if someone alters the entity class, to for example MyOwnNode implements NodeInterface.
Because the Node will no longer be the or a sub/parent class of the defined klass.
I can see an option or two (EDIT: ok, 4):
a) Add the interface to the annotation, check that.
b) Require that entity class changes *must* subclass the default
c) Keep the original class around somewhere in EntityType, check against thta
d) Go back to msonnabaum's class name => ID magic.
Comment #8
amateescu CreditAttribution: amateescu commentedd) breaks for the same reason, e.g. when the entity class is not named after the entity type.
There's also option e) get the interface that extends EntityInterface at runtime and check against that, but I think I'd prefer adding the interface to the annotation 'automatically' (in the discovery phase).
Comment #9
Berdird) can be fixed by requiring to implement it if your class doesn't match the pattern but I didn't say it's not ugly :)
e) Yeah, was thinking about that, but how do we know which interface is the right now? Could be pretty hard to figure out automatically?
Comment #10
amateescu CreditAttribution: amateescu commentedWell.. just pick the first one that extends EntityInterface :/
Comment #11
xjmComment #12
sunOut of the given options in #7, b) is the most natural, realistic, and conceptually valid:
An entity class represents a domain object. An entity class only exists, because it consists of custom business logic. The (additional) business logic is what differentiates an entity from another entity. The business logic is custom to the use-case of the originating module.
The time of universally relying on Node entities is a relict of the past. If you want the concept of node entities but not Nodes, then what you do is to copy/fork the entire module directory. There's no point in hacking the Node entity class.
Consequently, the only valid use-case for replacing the entity class of a module is to override its methods for very specific (weird) reasons on your custom site.
→ If you need to hack an entity class for whichever weird reason, then it is only sensible that your hack can only be an override of the original class.
→ If that's not your intention, then stop trying to hack Node.
Comment #13
amateescu CreditAttribution: amateescu commentedTotally agreed with @sun. The current patch makes #7 b) an implicit requirement, otherwise you'd get a runtime exception, so should we move forward with it?
Comment #14
BerdirWorks for me I think. It's a separate method, and if you really do something crazy, then you can always overwrite that method and return "node" in your class.
Comment #15
BerdirWhat's the next step here then?
I think some specific test coverage of this would be good, + converting a bunch of exemplary load function calls (entity_load(), but also node_load(), taxonomy_term_load(), .. and the multiple version of them), and deprecate them in favor of this for procedural code/$storage->load()/loadMultiple() for injected code.
We also need explicit test coverage for altering an entity class I think, so making sure that alter Node to TestNode extends Node in the entity type definition and then Node::load() works as expected.
Comment #16
BerdirMaybe we can do that test as a unit test, I'm not completely sure.
Comment #17
webchickFrom #2177031: Remove menu_get_item() and every use of it. :
We desperately need this, IMO.
Comment #18
tim.plunkettBut yet, this issue has only discussed load() and loadMultiple. That last snippet is loadByProperties().
How do we decide what public methods of the storage controller to duplicate on the entity?
And why just the storage controller? We already have access(). Why not view()? Etc.
Comment #19
pfrenssenI'm going to work on the tests for this. I'm going to build on #2134857: PHPUnit test the entity base classes, because it is laying the whole groundwork for unit testing Entities, and it is currently in RTBC so it'll probably get in before this one.
Comment #20
pfrenssenAdded a test. My first PHPUnit test, took some figuring out :)
Comment #21
BerdirThanks, this looks quite good. What would be great is if we could find a way to test the edge cases, like calling load() on a subclass, but I'm not sure if that's possible, maybe we need a real entity class for that? And calling it on an entity class that has no corresponding entity type definition, that should be pretty easy by simply creating a new mock instance with an entity type ID that doesn't exist.
Also wondering if and how much we already want to deprecate on this, I think it would be nice if node_load() and so on would be @deprecated in favor of this. There qre quite a few such functions though and we can't deprecated entity_load() itself on this as it's not possible to pass an entity type ID.
Maybe a handful example conversions, pick one simple module as an example? Then we can have novice issues as a follow-up for each module where it applies?
Wondering if we should use load() of the storage controller directly here?
Not sure if we really want to use random ID's. Probably ok.
I think we should call the method statically here, should be possible with get_class($this->entity) or so?
Comment #22
BerdirFor the subclass use case, can we maybe mock the mock or something like that to get a subclass and then set the entity type class to that and call it on the first mock class? That's what we want to test.
Comment #23
pfrenssenGoing to work on this.
Comment #24
pfrenssenWe're missing something here which we had in entity_load_multiple(): the ability to reset the entity cache. I suggest we put this in.
Comment #25
pfrenssenDiscussed the above with @Berdir, but he suggested that the clearing of the entity cache should be decoupled from loading entities. It is not a common use case.
Comment #26
pfrenssenIn this patch I have deprecated all instances of ${entity_type}_load() and ${entity_type}_load_multiple() and have converted the entity loading functions from the Breakpoint module as an example:
Created a meta issue to do the rest of the conversions: #2225347: [META] Replace calls to ENTITY_TYPE_load() and ENTITY_TYPE_load_multiple() with static method calls
I'm going to continue working on this, but wanted to get some test feedback on the work so far, so setting this to needs review for the bot.
Comment #27
pfrenssenSetting to needs review for the bot.
Comment #28
ianthomas_ukI'm unsure of the rationale for keeping these in Drupal 8.0, given the other wrappers we have removed/are removing, but if we do keep them the comment should be "@deprecated in Drupal 8.0, will be removed in Drupal 9.0".
8.x-dev makes sense when your audience is core developers and bleeding edge module developers, but not for your average API user.
Comment #29
moshe weitzman CreditAttribution: moshe weitzman commentedComment #30
pfrenssenPosting work in progress. Addressed all remarks. Still probably need to deduplicate some duplicated code in the tests, and am not sure if we should check that isSubClassOf() is not called at all, since this is testing the actual implementation of the method.
Comment #32
pfrenssenI introduced the load() and loadMultiple() methods on the interface as I figured we would want to ensure that these are always available from now on. These were not implemented in ViewUI which caused the fatal error.
Comment #33
BerdirAs discussed, I'm wondering if this needs to be a bit more complicated.
Should we prefer if it's an exact match?
Use case:
Someone does Whatever extends Node and exposes it as a new entity. We actually do that for EntityTest/EntityTestRev/EntityTestMul/EntityTestMulRev. So that would result in Node::load(1) suddenly stop working because of that module.
So calling EntityTest::load(1) then you're actually not sure which one you will get.
Second, should we abort if we find more than one match and throw an exception?
So you would basically loop over all, find exact and subclass matches, first look if you have an exact match and then use that, second check subclasses. And check if you have multiple before each and throw an exception.
Comment #34
BerdirNot that anyone would ever do this, but should this call View:: instead?
wrapper for ... load()
do we need this? can't we just have a separate $entity_type_id variable?
Comment #35
dawehnerThis sounds for me like an BadMethodCallException
Ha, this reminds me a bit of ViewUIObjectTest
Can't we exclude this method above and rely on being the mock an actual entity? It would be cool to not rely too much of the internal code, but test the actual behavior
I really like this new comment way for tests!
Can/should we just share this code between the two test methods and even the next one?
Comment #36
pfrenssenPicking this back up.
Comment #37
pfrenssenImplemented @berdir's suggestion from #33. Going to work on the remarks from #34 and #35 now. Will also extend test coverage so we can assure that the exceptions are thrown.
Comment #38
pfrenssenAddressed remarks from #34 and #35. Rerolled against latest HEAD.
I rewrote testLoad() to actually test the behaviour rather than the implementation, using actual entities as suggested by @dawehner. Also provided custom exceptions and wrote tests that check if the exceptions are correctly thrown.
Will now rework the remaining 3 tests to test behaviour instead of implementation.
Comment #39
pfrenssenUpdated issue summary.
Comment #40
pfrenssenRewrote the remaining tests. Because the implementation is no longer tested I could reuse large parts of the test setup. Nice stuff. Am really starting to appreciate PHPUnit :)
Comment #42
pfrenssenAddressed the failures.
Comment #43
xjm42: 2190313-42.patch queued for re-testing.
Comment #45
xjmComment #46
ianthomas_ukMostly a simple reroll due to context changes around use statements, but two things to note:
- I have removed the
use Drupal\Core\Entity\Entity;
fromcore/modules/views_ui/lib/Drupal/views_ui/ViewUI.php
as this is no longer used (see #38)- menu_load() has been renamed to menu_ui_load() since the last patch, and we are now deprecating this function, to be removed before Drupal 9. That seems silly - either the function should be renamed back to menu_load() (and therefore moved out of menu_ui.module) or it should just be removed. See CR https://drupal.org/node/2216585 and #2085243: Rename Menu module into Menu UI module
Comment #47
BerdirWould be nice to get @fago's OK to move forward here, he had some reservations when we talked about it in Szeged.
I do like it a lot and I think the patch is pretty ready, but I haven't done an in-depth review of the test changes in the last few patches.
I agree that menu_ui_load() is weird, but it's already weird in HEAD, I'm fine with removing that, it's pointless to have an entity load function in a UI module.
Comment #48
sunComment #49
fagoThe concern I raised is that having Class::load($type) functions available that are so nice to feature IDE auto-completion will lead to people preferring them over the "right" variant of going with dependency injected services. So the core of the problem is that dependency injected services offer no good DX and no auto-completion.
Still, the status quo is having loader functions what is even worse as you have to create and maintain them, so the patch is an improvement here. However, I wonder why we want to keep the functions - even if deprecated? I can see us keeping a deprecated entity_load(), but is there a point in keeping a deprecated node_load()?
Regardless of the concern I think the patch is good DX improvements for situations where you cannot use dependency injected services anyway, i.e. hook implementations. So I guess it's the right thing to do anyway.
For the concern to be addressed we'd have to do #2144677: Add per entity type managers to improve DX I guess.
Speaking of auto-completion, it's actually not really correct as it should go with the entity type interface instead of the class. So, auto-completion might lead developers to go with public non-interface methods then - not sure that's an issue?
Comment #50
BerdirWe discussed the class vs. interface return before and I think it's ok, public methods on the class that are not on the interface is going to be a very untypical pattern I think.
About the load methods, dropping stuff like node_load() would be ugly as that is called a lot. However, many of the load functions, especially config entities and new entities are hardly ever called and contrib isn't going to care if we drop something like image_style_load() I think? So maybe just get rid of them? I've highlighted the ones that I think we can drop below.
This is used in 3 places, 2 of those are tests.. drop?
This is only used for a #exists callback.. drop?
Same, only use case is an #exists callback.
Used 3 times, only in tests...
Also almost only used in tests and already converted, drop?
Same story, an #exists callback.
Same.
As discussed, let's remove this is as it was already recently renamed and it's weird to have a load function in a UI module.
See @todo, this is unused apart from an #exists callback and new in 8.x, let's just drop it.
Used a few times but probably mostly internal too?
What are we using this for exactly?
Can we call it directly on the mock class?
Can we call EntityTestMulRev::load() here and below?
Comment #51
BerdirOk, discussed the load function removal with alexpott and he was OK with my suggestion, working on that, let's see if I got everything right..
@amateescu: LinkAccessCheck seems to be unused and looks weird, I guess we could drop the whole thing?
->id was not used, removed that, but we can't really change the load calls, calling it directly on Entity is a bit weird but ok...
Also moved the exception classes into the new Exception subnamespace and using @expectedException in the tests.
Comment #54
BerdirForgot to rebase before I uploaded...
Comment #56
BerdirMissed a few calls.
Comment #57
fagoThanks, seeing all the accessors removed is great. I'm fine with keeping a deprecated node_load(), that should ease transition while it's still clear that you should use Node::load().
This is going to fail silently if an entity type implements a deriver to make use of the same entity class for multiple entity types. We should better count the classes as we do for the subclasses to make sure we get the right entity type. -> Setting to needs work.
Besides that I wonder whether this type-search affects performance if quite some entity load calls happen on a page. We could statically cache the "class -> entity type" map?
Comment #58
BerdirHm, *derivers* in the plugin way will not be possible for entity types, because they currently enforce : as the base plugin type delimiter and it is not possible to have : or . in the entity_type_id (routing, combined ID's like fields, and so on..). But it will be possible to re-use the same class with hook_entity_type_build(), so yes, that would be possible. Nobody should attempt to call that directly then, though?
We can check it, makes it even more complicated :(
I doubt that performance is such a big problem, most calls are in tests and so on, I would suggest to not worry about it until we have numbers that can prove it? Most entity loads that happen on real sites will go through routing, entity reviews and views I think, none of those is even capable of using this API as they don't have hardcoded entity types...
Comment #59
fagoSounds good to me - yeah, I'm unsure how much this is going to be used in practice, let's see.
@derivers: Hm, if not possible with the default deriver, the pattern can be easily customized and I could see modules like ECK to pursue that? So as it's conceivable that this situation will happen I think we should check for it. Given we already have a suiting exception that's certainly what one would except to get then.
Comment #60
BerdirYeah, it should be possible to do something like that, but as written in IRC, why would someone call SomeGenericECKEntityClass::load(1), it's kind of obvious that it won't work, as you don't know what entity this is in the first place, unlike Node::load() or User::load(). It's as senseless as calling Entity::load(1) :)
Anyway, if you insist on it, I'll extend it, but probably not before tomorrow.. so if someone else has time before that.. will assign to me when I start to work on it.
Comment #61
BerdirI still think this is unnecessary overhead because I can't think of a reason why you'd want to call the static load on such a generic class, but here's a patch that does it...
Comment #62
fagoWell, it's quite ambiguous if we do the check for SomeEntityConstructionKitType and not for Entity. I'd agree that people probably won't call it for either class, but once we've that exception documented it should work in either case imo.
Changes look good and come with test coverage, thanks! If you are ok with the check, then I'd say this is ready. Should we add a change notice for that?
Comment #63
BerdirCreated https://drupal.org/node/2266845.
I will also go through all change records that mention entity_load() (there are a lot of them) and update them to use this approach where it makes sense once this is in.
I'm OK with this going in as well, let's do it :)
Comment #64
fagoThanks, the change notice looks good.
berdir++
Comment #65
catchThis is possibly the least optimistic method description I've ever seen. It's appropriate for the code it describes, but I think it could do with a bit higher level description about what is and isn't supported. i.e. finds the class or subclass used for an entity type, throws an exception if there is a duplicate or no match?
Comment #66
BerdirRe-rolled (use conflict) and updated the description a bit.
Comment #67
jessebeach CreditAttribution: jessebeach commentedThis would help us with #2228763: Create a comment-type config entity and use that as comment bundles, require selection in field settings form. Assigning to a committer for visibility.
Comment #69
amateescu CreditAttribution: amateescu commentedRerolled after #2222719: Use parameter matching via reflection for access checks instead of pulling variables from request attributes. Thanks, @Berdir, for pushing this to the finish line :)
Comment #70
catchNot sure this needed to be assigned to webchick, so went ahead and committed/pushed to 8.x, thanks!
Comment #72
BerdirYay, published the change record and updated entity_loads() where useful:
https://drupal.org/node/2187853/revisions/view/7161925/7261839
https://drupal.org/node/2083591/revisions/view/2832035/7261851
https://drupal.org/node/2079767/revisions/view/7233657/7261857
https://drupal.org/node/2050669/revisions/view/2778645/7261875
https://drupal.org/node/2032185/revisions/view/2764149/7261877
https://drupal.org/node/2012896/revisions/view/6981155/7261891
https://drupal.org/node/1972410/revisions/view/2784587/7261893
https://drupal.org/node/1910176/revisions/view/2552696/7261955
https://drupal.org/node/1907430/revisions/view/7161951/7261961
https://drupal.org/node/1905910/revisions/view/6921509/7261963
https://drupal.org/node/1895936/revisions/view/2778059/7261971
https://drupal.org/node/1888504/revisions/view/2740903/7261973
https://drupal.org/node/1876852/revisions/view/7161947/7261981
https://drupal.org/node/1873210/revisions/view/2499456/7261983
https://drupal.org/node/1862420/revisions/view/6745963/7261987
https://drupal.org/node/1553180/revisions/view/2651988/7262001
Unrelated fix:
https://drupal.org/node/2054619/revisions/view/6961249/7261865
https://drupal.org/node/1721572/revisions/view/2329728/7261997
Uff.
Comment #73
tim.plunkettI got this issue mixed up with the ::create() one.
No one ever responded to my question in #2096899-1: Add $EntityType::create() to simplify creating new entities.
Now our entity types are not swappable at all, I don't even know why we're pretending anymore. Might as well remove all of the interfaces, they're useless now.
Comment #74
amateescu CreditAttribution: amateescu commented@tim.plunkett, have you seen comments #12 and #14 above?
Comment #75
BerdirThe only limitation this introduced is that if you swap out the entity class then it must be a subclass of the one you're replacing. What use cases do you have to use a completely different implementation that must extend from a different class?
I don't think it would be valid to change a config entity to be something that is not a config entity, and it would be one hell of a task to provide a config entity that doesn't extend from ConfigEntityBase and if you need that, there's usually not much in the specific entity class that could get in the way? Most of the code there is in pre/post methods and accessor methods and that's an important part for that entity to work correctly?
And as mentioned in #2028097: Map data types to interfaces to make typed data discoverable, swapping the class is even less useful for content entities, as they're just a container for their fields which can be extended and altered, and interfaces are limited for them anyway due to configurable fields.
Comment #76
fagoAs far as I understood with this implementation we can still swap out entity classes, it's just the type-hint which would not 100% correct as it still points to e.g. Node instead NodeInterface. That's acceptable though imho.