Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
Replace the taxonomy ref field with entity reference in the standard profile, try to use the node preview feature with unsaved terms and experience the multiple levels of failure:
1) formatters don't output anything in preview mode
2) widgets lose their data when returning to the node edit form
3) you might get an exception or two along the way, depending on which formatter/widget you chose
Proposed resolution
- fix the widgets
- fix the formatters
- add tests
Remaining tasks
Consider giving me (amateescu) a bunch of brown paper bags at the next Drupal event.
User interface changes
Nope.
API changes
Nope.
Comment | File | Size | Author |
---|---|---|---|
#29 | interdiff.txt | 2.65 KB | yched |
#29 | 2370703-ER_autocreate_fixes-28.patch | 27.89 KB | yched |
#23 | interdiff.txt | 4.18 KB | yched |
#23 | 2370703-ER_autocreate_fixes-23.patch | 27.85 KB | yched |
#20 | interdiff.txt | 1.06 KB | amateescu |
Comments
Comment #1
amateescu CreditAttribution: amateescu commentedAnd a patch.
Comment #6
amateescu CreditAttribution: amateescu commentedThe semantics of the return value for
EntityReferenceFieldItemList::referencedEntities()
has changed so TaxonomyFormatterBase needs to take that into account.I'm torn between the fix attached vs. introducing a
$return_unsaved_entities = TRUE
parameter toreferencedEntities()
. I'm going to assign this to @yched to get his opinion :)Comment #7
yched CreditAttribution: yched commentedWow, lots of nice catches. "autocreate" never stops being a PITA...
Aw, good catch. Could use a word of comment though.
Logic needs some adjustment IMO : $ids might be empty, and then we don't want to run loadMultiple($ids).
I get why, but could also use a word of comment. "Ensure the returned array is ordered by deltas" ?
Not in the patch, but EntityReferenceFormatterBase::prepareView() still has a
that doesn't look OK for referencing user 0 ?
(might belong to a different patch though ?)
Given that ER formatters are now supposed to use ERFormatterbase::getEntitiesToView() to retrieve the entities to display, it doesn't look like we still need this at all ? (the "set the Item to NULL and run filterEmptyItems() to remove invalid items for the subsequent view() method" dance)
Which is a win because it would remove the "viewing an ERFieldItemList modifies its content" ugliness.
Same, add a word of comment ?
Plus - unconditionally doing $item->entity will single-load the entity on regular (non-autocreate) items ?
The entity will already have been multiple-loaded above, but I don't think we want to rely on the fact that the entity type uses a static cache ?
Shouldn't it be smthg like:
?
Indeed too (although, shouldn't we fix $entity->urlInfo() to return NULL if entity->isNew() ?)
Which brings - the method to use for checking "autocreate items" is:
- $item->hasUnsavedEntity() when working on $item / $items
- $entity->isNew() when working with the entities that come from getEntitiesToView().
which is a bit confusing... When you think of it, "is new" and "is unsaved" are two ways to say the same thing, but the connection is not exactly intuitive when you look at the method names.
We're not going to rename Entity::isNew(), but maybe ERFieldItem::hasUnsavedEntity() to hasNewEntity() ?
Nitpick : "thus testing the autocreate feature" would be more accurate ?
Doc is copy/pasted form the previous member :-)
s/don't/do not :-)
Nitpick, but it sounds a bit weird to have a helper method that hardcodes the entities being referenced ?
It so happens that all tests there currently use the same two referenced entities, but why not pass that helper an array of entities to reference ?
Also, not sure the method really needs $field_name as a param, it could internally use $this->fieldName ?
Sounds wrong, 'multiple_values' is an entry in the @Widget annotation, not a widget setting.
Use WidgetBase::handlesMultipleValues() ?
I might be wrong but - if this handles the "non-tags" widget (one copy per value) , then the condition should be !$handles_multiple_values ?
Also, this code will run getReferencedEntities() (= "multiple load the entities for all deltas") once per delta. Again, not great if no static cache...
It's already an issue in the current HEAD code, so maybe punt on this for now, but there might be some rethinking needed around the protected getLabels($items, $delta) - I know, the API for "multiple values" widgets kind of sucks :-/
Nice :-)
Comment #8
yched CreditAttribution: yched commentedHeh, and I skipped the actual question I was asked :-)
referencedEntities() returning "autocreate" entities seems right. Are there cases where we use referencedEntities() and would *not* want them ?
Comment #9
jibranWow what a review I must say just by reading it i learnt a lot. Thanks @yched and thanks for the patch @amateescu
Comment #10
amateescu CreditAttribution: amateescu commentedLots of nice catches back at you :D
Not until you realize that
hasUnsavedEntity()
does the same single-load internally :P But I agree that it looks cleaner that way and if we actually want to optimize something, we need to reverse the order of the conditions.$entity->urlInfo()
throws an exception for new entities, so we're good. And the rename makes sense so I did that too.I should be removed from the maintainers list for this :) Fixed.
As for "multiple load the entities for all deltas", apart from maintaining our own static cache (which we'd need to maintain as well), I'm not sure what else we can do given the current API for "multiple values" :)
I couldn't think of any case either but a second opinion never hurts.
Comment #11
yched CreditAttribution: yched commentedStill broken, no ? If $ids and $target_entities are both empty, then this runs loadMultiple($ids).
Should be just if (empty($ids)) {return $target_entities;} ?
(although then for clarity I'd tend to do :
but that's a matter of taste)
Yeah... not sure why we still have that unofficial/hackish ->access property on the items, since formatters are only supposed to render the entities returned by getEntitiesToView() now, and that includes access checking ?
OK - so now it looks like prepareView() could just defer to getReferencedEntities(), avoiding duplicating the "load or unsaved" logic :
?
*If* we get rid of ->access, this could become just $referencing_entity->{$this->field_name} = array_values($referenced_entities);
Comment #12
amateescu CreditAttribution: amateescu commentedNope, that means we'll do the multiple-entity-load multiple times (hah) and since we're talking about the view layer here I think it's important to keep the query count as low as possible.
Comment #14
amateescu CreditAttribution: amateescu commentedHm.. it seems the ->access stuff is actually used a lot in tests.
Comment #15
yched CreditAttribution: yched commentedI don't get this.
getReferencedEntities() does one multiple-entity-load, but so does ERFormatterBase::prepareView().
How would calling getReferencedEntities() instead result in *more* multiple-entity-loads ?
I'm just saying : we have an official method to get the referenced entities, and it handles the subtleties of "autocreate". ERFormatterBase::prepareView() needs to get the referenced entities, and thus should use that method instead of custom code that duplicates the "autocreate" logic ?
Comment #16
amateescu CreditAttribution: amateescu commentedBecause prepareView() operates on multiple entities? :) You can see it even in the small piece of code you wrote, you call
referencedEntities()
on each $items object (hence multiple multiple-entity-load), while the current code does a single multiple-entity-load on the target_ids collected from all $items.Also, let's open a follow-up for the ->access stuff, this patch is doing enough as it is..
Interdiff is to #10.
Comment #17
yched CreditAttribution: yched commentedOh gee, of course, sorry. I should be removed from the maintainers list for this :-p
Comment #18
yched CreditAttribution: yched commentedYeah, would be good to have a closer look at ->access in a separate issue.
RTBC for that one here.
Comment #19
yched CreditAttribution: yched commentedJust : maybe add a comment in prepareView() about why we do not call referencedEntities() and thus have to re-do its internal logic ?
Maybe overkill : we could have a static ERFieldItemList::loadReferencedEntities(ERFieldItemList[] $items), that could be used both by ERFieldItemList::getReferencedEntities() and ERFormatterBase::prepareView()...
Comment #20
amateescu CreditAttribution: amateescu commentedAdded a comment to
prepareView()
.Yes.. that would be quite an overkill IMO. And we won't have access to the ->list protected member anymore.
Comment #21
yched CreditAttribution: yched commentedWe would if the static method lives in ERFieldItemList ? But ok, possibly overkill.
Final remarks - mostly nitpicks, but one is a stale doc, so back to NW - sorry :-/
We are looping through the list anyway in the next code block, so that could be removed by doing
- else {
+ elseif ($item->target_id !== NULL) {
$ids[$delta] = $item->target_id;
}
in the foreach.
That comment is kind of gibberish - existing code, but we could clean that while we're in there. What this does is place the referenced entity in originalEntity for getEntitiesToView() - which is actually the intent of the whole prepareView() method.
Related: the phpdoc for ERFBase::prepareView() method is quite stale now.
--> Suggestion: remove that inline comment, and replace the phpdoc with:
"Load all entities referenced by the multiple entities being viewed, and place them in a custom property for getEntitiesToView()".
"attached" is vague / misleading. Why not "referenced" ?
Also, striclty speaking, the initilization of $target_id is part of "load referenced entities, for which we cannot use referencedEntities()", so it should be below the comment :-)
Comment #22
amateescu CreditAttribution: amateescu commentedThis sounds a bit confusing, but maybe because I'm on mobile and I lack the code context. Anyway, feel free to update the patch, it will be easier for committers to credit you anyway :)
Comment #23
yched CreditAttribution: yched commentedOK - attached patch adresses #21.
Since ERFormatterBase::prepareView() is the typical (and possibly single) reason for the existence of FormatterInterface::prepareView(array $multiple_item_lists), I tried to be extra clear on what we do here.
Plus, in ERFormatterBase::prepareView() :
- renamed $target_ids to $ids for consistency with the var name in ERFieldItemList::referencedEntities()
- while we're in here, switched it from entity_load_multiple() to EM::getStorage()->loadMultiple()
- expanded the last "$item->originalEntity = ..." part to make it a bit clearer.
To be honest, I'm not sure why prepareView()/getEntitiesToView() use a custom $item->originalEntity rather than $item->entity, but we can probably try to sort this out in the same issue where we double-check the $item->access stuff.
Comment #24
amateescu CreditAttribution: amateescu commentedThanks! The interdiff looks good except for this part. See for why I didn't went with your proposed code in my reply from #10.6 :) Basically, the
isset()
condition is faster thanhasNewEntity()
because that accesses ->entity internally.Comment #25
yched CreditAttribution: yched commentedI don't think that is true :
So $this->entity is accessed only if target_id is NULL, in which case:
- there is supposed to be already an (autocreate) entity in $this->entity, so no loading
- if not, the target_id is NULL anyway, so the auto-loading in $this->entity will do no actual entity_load()
So $item->hasNewEntity() never loads anything ?
Comment #26
amateescu CreditAttribution: amateescu commentedHm.. ok, I guess I have to try and be more clear myself:
an
isset()
call on an array structure ($target_entities
) is *always* faster than a method call ($item->hasNewEntity()
) + a magic method invocation (FieldItemBase::__get('target_id')) + another magic method invocation (FieldItemBase::__get('entity')) in the case where target_id is NULL.Furthermore, the 'view' case for "autocreated" entities is the 1% use case (e.g. previewing a node), everything else will always go to the second condition.
I realize that this is just a micro optimization but it's a very trivial one to make and the code is not uglier in any way if we switch the order of those conditions :)
Comment #27
jibranSending it to test bot.
Comment #29
yched CreditAttribution: yched commented@a 'fliptable' mateescu (I need to look for the backstory of this :-p) :
Oh, sorry - you're right, that totally makes sense.
Then, same logic also applies to EntityReferenceFieldItemList::referencedEntities(). Switched the order there too.
+ fixed hasty copy/paste around $target_type & EM::getStorage()->loadMultiple(). Self-slap.
Comment #30
yched CreditAttribution: yched commentedOpened #2374019: Cleanup the use of custom item properties in EntityReferenceFormatterBase for $item->access / $item->originalEntity
Comment #31
amateescu CreditAttribution: amateescu commented@yched, this is the backstory: #2354271-17: Rename the "combined ID" separator (|) :D
The documentation changes since #23 are perfect so back to RTBC.
Comment #32
Wim LeersExciting! :)
Comment #33
alexpottThis issue addresses a major bug and is allowed per https://www.drupal.org/core/beta-changes. Committed c18ba6a and pushed to 8.0.x. Thanks!
Minor fix on commit.