Updated: Comment #49
Problem/Motivation
Too much business logic hiding in attachLoad(), we want to make storage controllers as focused on *storing* as possible and make it easy to provide different storage controllers that don't have to implement a trillion different entity-type specific things.
Proposed resolution
- Move as much as possible of that logic to a new postLoad() method on the entity class, similar to other post/pre methods.
- Remove $revision_id from attachLoad()/postLoad()
- Kill a few storage controllers that we don't need anymore.
Remaining tasks
Hope that it passes and RTBC it.
User interface changes
Niet.
API changes
attachLoad() renamed to postLoad(), new method on entity that entities should use instead.
Related Issues
#2130499: Move entity hook invocation from Storage to Entity classes - As discussed in Prague but decided to put of to a follow-up issue as it would make the patch more complex.
Original report by @chx
We already had two rounds of this (save/delete/etc and baseFieldDefinitions); now I am doing some of the load code and a little residual cleanup in Node.
Comment | File | Size | Author |
---|---|---|---|
#84 | storage-logic-2095283-84.patch | 56.69 KB | Berdir |
#79 | storage-logic-2095283-79.patch | 56.73 KB | Berdir |
#74 | storage-logic-2095283-74.patch | 60.05 KB | amateescu |
#70 | storage-logic-2095283-70-interdiff.txt | 1.7 KB | Berdir |
#70 | storage-logic-2095283-70.patch | 60 KB | Berdir |
Comments
Comment #1
chx CreditAttribution: chx commented(note for webchick: see, I didn't file this as critical. Do I get a brownie point?)
Comment #3
chx CreditAttribution: chx commentedComment #5
chx CreditAttribution: chx commentedComment #7
chx CreditAttribution: chx commentedComment #8
BerdirWe could probably move this into a parent class, not sure if we want to do it here, but I'm already adding a different implementation in a different issue, so this would help avoid code duplication. The weird class thing will go away there too.
The @todo should actually be possible now. Can you try to add it to baseFieldDefinitions() ?
I think we can use $this->database here.
Comment #9
chx CreditAttribution: chx commented> We could probably move this into a parent class,
Moved.
> The @todo should actually be possible now. Can you try to add it to baseFieldDefinitions() ?
How so?
EntityManager::getFieldDefinitions
caches the definitions. And I do not know how to add a default.> I think we can use $this->database here.
I think so too :)
> The methods that you add should also allow us to kill EntityTestStorageController::create() as that needed $this->entityType.
Killed. Also, if I would know how to add a default, I perhaps could kill the new EntityTest::preCreate() as well because $entity_type is handily passed in.
'value' => $entity_type
?Comment #10
chx CreditAttribution: chx commentedlarowlan and andypost pointed out how to add a default value. So trying that for entity test.
Comment #12
chx CreditAttribution: chx commentedI hit a phpstorm bug -- the recursive search-replace didn't actually work.
Comment #14
BerdirAs discussed, this doesn't work, as we need to bundle to get the fields.
Comment #15
chx CreditAttribution: chx commentedDiscussed with berdir and while this is the way to add a default it wont work because type is the bundle key.
Comment #16
BerdirThis needs to be updated. The base class doesn't attach fields anymore and the things below aren't correct anymore.
Comment #17
chx CreditAttribution: chx commentedThis one moves some logic into ShortcutSet, renames attachLoad to postLoad because that just makes sense, look at the other methods in EntityInterface. Also, we added postLoad already for this purposes just was unused until now.
Comment #19
chx CreditAttribution: chx commentedComment #21
chx CreditAttribution: chx commentedComment #23
chx CreditAttribution: chx commentedComment #25
chx CreditAttribution: chx commentedComment #27
chx CreditAttribution: chx commentedComment #28
BerdirThis seems to be the only case where we move a hook invocation to the entity classes. Why?
Comment #29
chx CreditAttribution: chx commentedWe can move invokeHook to entity in the next patch. This is already a hodgepodge a bit. Also, invokeHook just recently slimmed down enough to be moveable.
Comment #30
fagoI don't think it makes sense to invoke the entity load from here. The regular pattern is the storage controller invoking the hook + the entity class method later on. Are there any reasons to not follow the same pattern here?
E.g. that's what we do for preSave():
Comment #31
chx CreditAttribution: chx commented> The regular pattern is the storage controller invoking the hook + the entity class method later on. Are there any reasons to not follow the same pattern here?
That's not a regular pattern, that's a stupid thing we did because invokeHook was dealing with fields. No matter the storage controller, you need to invoke hooks so why it's on the pluggable storage controller? Just count how many times the invoke code was duplicated (three times!) before.
Edit: 36 files changed, 221 insertions(+), 272 deletions(-)
Comment #32
fagoAvoiding code duplication is not a reason for moving it to the entity class, we can add a method invokeLoadHook() on the storage controller also.
It's the pattern. We can argue on whether it's stupid or not, sure. But even if it's stupid, it should be fixed for all places in a consistent manner unless there are good reasons for being inconsistent. However, I do not see any?
Comment #33
chx CreditAttribution: chx commentedComment #34
fagoImo, the goal of this issue was to move entity type postLoad() specifications to the entity classes, as we already do it for e.g. save. Whether we want to move general, not entity-type-specific storage logic to the entity class is another issue and needs to be discussed first. If you are insisting to not separate that discussion to its own issue, then okok - let's have the discussion here.
So let's clarify what we are talking about. What lives in the storage controllers what is not storage engine specific? I find the following:
a) invokeHook(), invokeTranslationHooks(), maybe invokeHookLoad() or so also
b) invokeFieldMethod(), invokeFieldItemPrepareCache()
c) cacheGet(), cacheSet()
Anything else? Going with that for now.
So, when we remove that general logic from the storage we could move it to the entity class, but I dislike adding that much of general purpose code in people's entity classes. We should try to keep them as simple as possible. I know they aren't really simple but it would be way worse if we'd move all that stuff on entity class as well.
So, instead what about moving that general-purpose logic to suiting services we partly already do, e.g. for getting entity field definitions or instantiating fields. We could move a) to entity manager, b) to the entity field manager (= field type manager?) and c) could be moved to a EntityCacheController/Handler/Service which we have been discussing doing for entity cache. Then, we could go and call this services from the entity class and have the logic removed from storage controllers, while entity classes do not become heavier. Thoughts on that?
Comment #35
chx CreditAttribution: chx commentedComment #36
chx CreditAttribution: chx commented*exasparated* There are constraints we need to work with but the size of the entity class is not one. On the other hand, the size of the storage controller class is one because it's pluggable and every. single. plugged. version needs to copypaste the code. Storage controlllers should contain code that wont need to be copypasted. Very simply. And yes, there's more to move. Moving the rest we can do in a followup. This is already byzantine and took some work to pass.
Comment #37
BerdirRe-roll, let's see if this still works.
Comment #39
BerdirUps, this should fix hopefully the fails and the exceptions.
Comment #40
BerdirOk, so now I better understand what's going on here.
These are the options that we have to solve that load hook disagreement that we had in Prague:
1. Do what the patch does, move the hook invocation to the entity class. We can still do b) or c) additionally too.
2. Keep it in the storage controller, then we need to do one of a/b/c:
a) Make hookLoadArguments() on the entity class public so that we can call it from there.
b). Generalize the additional load hook arguments and pass along the bundles for all, including the generic hook_entity_load()
c). Drop the additional load hook arguments feature completely, unless there's a more useful use case than passing through the bundles. You need a loop and check every $entity *anyway*, as there could be multiple bundles and you only apply to one of those. The only thing you win is to be able to initially decide that no entities interest you, therefore saving a loop over them. The performance gain is tiny, and it's actually slower if you do apply to at least one entity, because you need to check twice. And you need more code, so it's hardly a DX improvement.
Thoughts? I actually kind of like c), although that would be an API change and I'd prefer 2 just because there's less conceptual change in this patch and we can open a follow-up to discuss and eventually move all hook invocations to the entity classes.
Comment #41
fagoThis comment seems to stem from node-ages, just as the whole hook look arguments code. Do we still need this?
After reading options of #40: I guess I like c)!
Now it's inconsistent with other post-* crud hooks, as said - whatever we do it should stay consisent.
As discussed in Prague, having the entity taking care of invoking it's own hooks actually makes sense and I have to agree with chx that the increased size of entity class isn't that problematic, plus it doesn't look like being a lot of code to add anyway. -> So let's do 1)!
However, we really should do that for all hooks - so things stay consistent. So let's move invokeHook to the entity class and use it from there?
Is there a way to avoid accessing the global module handler? Maybe, get it from the passed storage controller, or better have the entity manager providing a helper for invoking entity hooks + get that via the storage controller?
Comment #42
Berdir- Re-rolled
- Hook load arguments dropped. Updated hook_node_load() documentation. The example might now look slightly more complicated, but the old one was kind of weird in that it checked but then still used all nids. Usually, you'd need that loop & check anyway IMHO.
- Tried to move invokeHook() but it's not that easy unless we make it a public hook. There are a few hooks that have no corresponding method on entities, e.g. delete revision (this should have one I think as we have save revision too) but not sure what to do about the translation hooks. Seems like they should also move to postSave(), but then we need to do the dataTable check there. Also not sure about the field invoke method helper method, should that move to Content entity base?
Where I'm getting at is that moving everything isn't trivial, so I'd rather not do it here. Either move on with this or keep load in the storage controller too for now. And open a follow-up issue to move all/the rest of it.
Comment #44
BerdirAttempt to fix those tests. The actual implementations show nicely that no real implementation even bothered to look at $types, some don't even define it (e.g. forum_node_load()) except of that node load test.
Comment #46
BerdirRe-roll. Removed the load hook tests for node and custom block completely. We have generic entity crud hook tests for various entity types and it's all unified now, no need for custom tests for that stuff now that $types is gone.
Comment #47
fagoYeah, let's go through that in a follow-up.
id_id
That's in there several times, best s/r to fix the s/r error.
Unneccessary empty line.
Else patch looks good.
Comment #48
amateescu CreditAttribution: amateescu commentedThe revision_id of which of the loaded entities? :)
Comment #49
BerdirThis kills the weird $revision_id argument completely :)
It's used for exactly one thing, to figure out if we should load the fields specific revision or not. Which is something that we can now calculate automatically. We need to refactor the field stuff further to not require loaded entity objects and might need to re-introduce it there again, but that won't go through postLoad() anymore.
This has the additional benefit of removing the concept of a $revision_id/flag from EntityInterface and the storage controller base class, which don't know anything about revisions.
Also found some more postLoad() methods that I could kill. The only one that survived is comment, because that needs to happen *before* we map the data to entities. We'll get to that later...
Comment #51
BerdirI will never learn to at least do a simple syntax check before I upload broken patches...
Comment #53
BerdirWow. that means we invoke the load hooks for config entities every time they're accessed, because we don't have a static cache. That's... crazy, need to do some profiling on how costly config loading is.
This should fix that, let's see if there are additional errors.
Comment #55
BerdirWould be too embarrassing to post an interdiff.
Comment #57
yched CreditAttribution: yched commentedThis look like an awesome move overall.
I'm just a bit worried about the amount of stuff that's dumped into the postLoad() step, either in the controller or the entity class. It makes it difficult to figure out what happens in which order, you need to unwind the class hierarchy and the order of the parent:: calls.
Also, having field values loaded in a step called postLoad() seems a bit weird/counterintuitive ?
I didn't really follow the issue, so feel free to discard this...
Comment #58
BerdirYou are perfectly right, loading field values should not be in there nor converting things into entities, and it will have to move around so that we can load the field values before we create the entity objects. The way it works right now is a left-over of when we had the NG storage controller, that had to pick what it changed to not having to duplicate too much code.
I need to create an issue, but there's already a bullet point for this in the meta issue.
Comment #59
yched CreditAttribution: yched commentedThanks @Berdir, makes sense.
Not fully clear to me what's for this issue and what's for followups / side issues then ?
Comment #60
BerdirAh, now I found what @chx was referencing to. There was one piece where the actual revision id was used. And it switched back and forth between a $load_revision and $revision_id, weird stuff. And it had a fall back in place anyway. So I just removed all that code and it looks like the entity tests are still passing.
It is still a bit weird, because we conditionally convert $entities from arrays to objects if there is *no* data table, and the other code only runs if there is a revision data table, so it's fairly sure that it doesn't run when $entities is an array of entity objects, but to be sure, I still added an is_object() check there.
@yched: Not sure, I think we shouldn't do more here other than fixing the tests, unless you see something. This basically just involves moving things from entity storage to entity classes, anything that's internal refactoring should imho be done in a follow-up issue, unless directly necessary for this issue.
Comment #62
BerdirSo we need to pass $entities around by reference.
IMHO it's wrong that the store returns differently sorted entities, but that's a different topic.
Comment #63
BerdirSome nitpicks that I noticed, will re-roll myself tonight or so unless someone else has time first.
Not sure about this change, should probably just have the before load hooks invoked part removed. The important part is what it can be used to do and when it's called, not what the default implementation does.
Missing an array
Missing descriptions of the @return.
Comment #64
vladan.me CreditAttribution: vladan.me commentedDocumentation updated, patch still applies
Comment #65
fagoLooks like this becomes a great clean-up :-) Here a review:
Also, if it would stay in the entity class the interface docs should probably define that so implementers know.
I'm confused by "current" here. What does this mean or refer to?
Same here.
This does not call parents while others do. Even if it's empty, it might not stay empty?
Same here.
again, call the parent?
Comment #66
BerdirOk, moved the hooks over, calling parents and removed "current". Let's see if this works.
Comment #67
fagodreditor just ate my review for breakfast ;-/ anyway, here the short version:
All postLoad() overrides should use default ordering now as there is no reason left not to, i.e. call parent first.
$revision_id should be gone here, not ?
Again.
deprecated comment
Comment #68
BerdirOk, cleaned that up too. Checked with @amateescu, he said it's ok to remove the @todo If I understood correctly :)
Comment #69
fagoFound two more - else this is RTBC imo.
Another $revision_id left over.
Should move up also.
Comment #70
BerdirOk, cleaned that up as well.
Comment #71
fagoThanks!
Comment #72
BerdirThanks, follow-up issues: #2137801: Refactor entity storage to load field values before instantiating entity objects and #2137807: Move entity storage related hooks to pre/Post methods on entity classes.
Comment #73
Berdir70: storage-logic-2095283-70.patch queued for re-testing.
Comment #74
amateescu CreditAttribution: amateescu commentedRerolled after #2076445: Make sure language codes for original field values always match entity language regardless of field translatability.
Comment #75
chx CreditAttribution: chx commentedThe title was misleading.
Comment #76
effulgentsia CreditAttribution: effulgentsia commentedProbably moot, since this is already RTBC, but just in case, tagging as a beta target, since it touches on data storage issues.
Comment #77
Berdir74: storage-logic-2095283-74.patch queued for re-testing.
Comment #79
BerdirRe-roll after the aggregator categories were removed.
Comment #80
BerdirRTBC no longer triggers testbot?
Comment #81
BerdirPassed, back to RTBC.
Comment #82
fago79: storage-logic-2095283-79.patch queued for re-testing.
Comment #84
BerdirComment #85
alexpottTagging and setting correct priority after discussion with @catch
Comment #86
BerdirThat was just a re-roll, so setting back to RTBC.
Comment #87
alexpottCommitted 65be82a and pushed to 8.x. Thanks!
Comment #88
sunAmazing work, all! :)
There's just one detail I wondered about: Some postLoad() overrides are enhancing the queried entities with entity specific data, and they call into parent::postLoad() to attach fields and invoke module hooks.
However, the position of when parent::postLoad() is called and in turn, the point in time when module hooks are invoked, is not consistent for all entity types. Most of them are calling parent::postLoad() as the first operation, before any additional operations are performed. As a consequence, module hooks will not be able to act on additional data supplied by the entity class, or am I missing something?
It looks like parent::attachLoad() was always invoked as a last step previously, so that module hook implementations always had the full data available?
Is that something we're going to polish via #2137807: Move entity storage related hooks to pre/Post methods on entity classes, or should we open a separate issue for that?
Comment #89
Berdir@sun: Just discussed this with Xano as well in IRC too. postLoad() right now is still weird, because FieldableDatabaseStorageController still relies on something that dates back to a hack introduced in the NG storage controller to avoid duplicating the load method. Therefore, that specific implementation receives stdClass objects and converts them to entities within that method, before calling the parent.
I will work on this in #2137801: Refactor entity storage to load field values before instantiating entity objects.
Comment #90
BerdirCreated a change notice about the removed $types argument/additional load arguments support: https://drupal.org/node/2166881
There is afaik no specific change notice about the other moved methods, only a short note in https://drupal.org/node/1400186, added this issue as a reference in that as well (because there aren't enough yet :)). I intend to write this down in the documentation when we're done with the changes.