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
When using Drupal\Core\Entity\ContentEntityBase::toArray() to create an array of an entity, the resulting array cannot be used to create a new entity with entity_create().
$node = \Drupal\node\Entity\Node::load(4);
$data = $node->toArray();
unset($data[$node->getEntityType()->getKey('id')]);
unset($data[$node->getEntityType()->getKey('revision')]);
unset($data[$node->getEntityType()->getKey('uuid')]);
$node = entity_create('node', $data);
$node->save();
Doing so results in the following error:
Fatal error: Call to a member function getFieldStorageDefinition() on a non-object in core/lib/Drupal/Core/Entity/ContentEntityBase.php on line 972
Proposed resolution
Ensure that Drupal\Core\Entity\EntityInterface\ContentEntityStorageBase::doCreate() can accept a bundle from an array rather than a string.
Remaining tasks
- Test Patch
Comment | File | Size | Author |
---|---|---|---|
#53 | interdiff-53.txt | 845 bytes | amateescu |
#53 | 2443165-53.patch | 3.2 KB | amateescu |
#51 | interdiff-51.txt | 783 bytes | amateescu |
#51 | 2443165-51.patch | 3.05 KB | amateescu |
#47 | 2443165-47.patch | 2.71 KB | amateescu |
Comments
Comment #1
davidwbarratt CreditAttribution: davidwbarratt commentedComment #2
BerdirWhat exactly did you do?
Blind guess is something with the bundle is not right.
Comment #3
davidwbarratt CreditAttribution: davidwbarratt commentedI'm working on a contrib module that exports entities as YAML and then imports them back in to another environment (I couldn't find a module like it, so I thought I'd write one).
Here's an over-simplified version of the problem:
When you execute
$node->toArray()
the resulting output is:I believe the problem is, is that properties of the entity are in multivalue arrays, and entity_create() expects them to just be values.
I was thinking that when Drupal\Core\Entity\ContentEntityBase::toArray() creates the array, it should check to see if the value is a property or field and output the array accordingly.
Comment #4
BerdirEverything is a field.
Yes, I'm pretty sure that it's the bundle detection that is flawed. ContentEntityStorageBase::doCreate() assumes that it's just a string. We can try to make that code more generic, but the easiest solution for you is probably just to special case that.
Untested, but that should do the trick and is generic.
Comment #5
davidwbarratt CreditAttribution: davidwbarratt commented#4,
That did the trick!
I actually changed it around so it would be on import rather than export:
You can see the whole thing here:
https://gist.github.com/davidbarratt/5aace2d13f54e511698c
Comment #6
davidwbarratt CreditAttribution: davidwbarratt commentedComment #7
davidwbarratt CreditAttribution: davidwbarratt commentedAttached is a patch that fixes the problem. :)
Comment #8
davidwbarratt CreditAttribution: davidwbarratt commentedwhoops... let's get rid of that whitespace.
Comment #9
BerdirThat's not generic enough, the bundle is not always in a target_id property. EntityTest for example doesn't use a reference field, so it is in 'value'.
The best way might be to just reset() in a loop until we no longer have an array.
We also need tests.
Comment #10
BerdirBtw, your code is flawed for the same reason. Mine works no matter how the bundle is stored, because it uses the API.
Also, you could also use the serialization module for this instead of your own implementation. See \Drupal\hal\Tests\EntityTest for examples. That allows entity and field types to customize the process.
Comment #11
BerdirComment #12
davidwbarratt CreditAttribution: davidwbarratt commentedYou're right, I did make an assumption that it's always
target_id
, thanks for the information, I didn't understand that sometimes it'svalue
.However, while my code does make an assumption, it is not inherently incorrect.
The problem is, is that there is no API to determine if
target_id
orvalue
should be used.I looked into the Serialization module, that yes, that's basically what I was trying to do manually, so it's nice to see that there's an API for that. :)
However, the module suffers from the same problem. I rewrote my import/export test to use Serialization:
https://gist.github.com/davidbarratt/a1e805aef086d535b8c3
The export works perfectly, but when you try and import you get this error:
If you look in Drupal\serialization\Normalizer\EntityNormalizer::denormalize() you'll see this line:
It looks like Drupal is making the same assumption I did, but in reverse. We either need to have an API to determine the bundle key from the array, or ensure that Drupal\Core\Entity\EntityInterface\ContentEntityStorageBase::doCreate() (and any other method that would get the method from the array) properly handles either case.
Comment #13
davidwbarratt CreditAttribution: davidwbarratt commentedI've attached a patch that loops through the array until a non-array is reached. I've also removed the work-around that was added in #1892320: Add deserialize for JSON/AJAX.
I haven't added any tests yet (if needed), just wanted to make sure it looks good to everyone first.
Comment #14
davidwbarratt CreditAttribution: davidwbarratt commenteddamn you whitespace!
Comment #17
davidwbarratt CreditAttribution: davidwbarratt commentedNeeded to update the texts since there are two methods that are no longer called. Also, the
$test_data
remains the same throughout the test. :)Comment #18
davidwbarratt CreditAttribution: davidwbarratt commentedDoes this still need tests or are the existing ones sufficient?
Comment #19
BerdirThat's a creative way of writing a normal while loop as a do/while.. :)
We also need a comment here, something like this:
The bundle could be a string or it might a field based structure. We can rely on it being a single string, so remove all wrapping arrays until we have that string.
Yes, I I think we need explicit tests for this.
Grml. json serialization is crap.
I recommend to use hal+json from the hal.module instead. Doesn't mean that we shouldn't fix this, of course.
Comment #20
davidwbarratt CreditAttribution: davidwbarratt commentedI've updated the
while()
loop, but I couldn't figure out where to add the test. I actually don't see any tests forContentEntityStorageBase
. Am I missing something? Do we need to create a new test file?Comment #21
davidwbarratt CreditAttribution: davidwbarratt commentedComment #22
yched CreditAttribution: yched commentedRerolled, fixed the comment wrapping and adjusted the wording a bit.
Other than that - a bit ugly indeed, but no better suggestion on my side :-/
Comment #23
davidwbarratt CreditAttribution: davidwbarratt as a volunteer and commentedI've added some tests. There isn't really much to test since the
Drupal\Core\Entity\EntityInterface
does not have a__construct
. I'm not sure how we can actually rely on an entity (content or config) implementing the constructor in the same way we are calling it. :/Perhaps the class calling this class should provide an empty entity class? or perhaps the
__construct
should be added to the interface?Then again, there is no way to enforce scalier values (
string
) in PHP 5.4 (that I am aware of)Comment #24
mgiffordPatch no longer applies.
Comment #25
andypostComment #26
kostyashupenkoRerolled patch #23
Comment #27
andypostComment #28
andypostlost in re-roll
Comment #30
HOG CreditAttribution: HOG at Skilld commentedComment #32
davidwbarratt CreditAttribution: davidwbarratt as a volunteer commentedI don't understand the test failure...
Comment #33
davidwbarratt CreditAttribution: davidwbarratt as a volunteer commentedHopefully kicking off the test bot to get an error message that makes sense.
Comment #34
davidwbarratt CreditAttribution: davidwbarratt as a volunteer commentedI opened #2674428: Php Core dumps should be more visible to end users to hopefully get a better explanation of the failure.
Comment #35
tstoecklerFWIW, I don't think this is a particularly good idea. If we want to support multiple possible inputs we should do so more explicitly. The current patch allows, for example
Node::create('type' => [[[[[[[[[['article']]]]]]]]]])
. These are the sort of things that - in my opinion - come to bite us years from now because this will hide some other obscure bug that would generate obscenely nested arrays.Edit: fix example code
Comment #36
davidwbarratt CreditAttribution: davidwbarratt as a volunteer commented#36,
Right now, there isn't a way to serialize an entity, and then unserialize it without changing the
bundle
to a string. It's the only field that has this problem. :(I don't really care how we fix this, but you should be able to execute the test code in the issue description and it should work without error.
Comment #37
tstoecklerOK, sorry, maybe that was unclear.
I'm totally fine with supporting all of the below:
which - unless I am mistaken - is all the variants we support for all other fields, as well. But we IMO we should support those explicitly and not arbitrarily deeply nested arrays, which is what the current patch does.
But of course me having that opinion does not mean that it has to done that way :-)
Comment #38
MixologicIf you click on the test results->view results on dispatcher->console log->console log full, there you will see the culprit(s):
https://dispatcher.drupalci.org/job/default/84494/consoleFull
Those are all essentially segmentation faults or something like it. Whatever you are doing in that test, its causing phpunit to crash, hard.
Sorry that the error is "PHPunit Test failed to complete " -> thats actually in core and is what the testbots tell you: http://cgit.drupalcode.org/drupal/tree/core/modules/simpletest/simpletes...
Comment #40
xjmThe core committers and Field API maintainers discussed this issue today. We decided to defer triage since we did not really have consensus on whether the issue was a bug vs. a task and whether it was major vs. normal. Both Entity system maintainers considered it a bug despite being developer-facing only, and emphasized that it was a very common usecase for the API, so leaving at major to revisit later.
Comment #43
hkirsman CreditAttribution: hkirsman commentedThis once fixed a save error in Paragraphs module which I can't reproduce at the moment (will post if it comes up again). Must have been a very specific case. I can't apply the patch to Drupal 8.3 so could anybody clarify what's the status of this? Is it somehow fixed from. How serious is this bug? Thank you!
Comment #47
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedI just bumped into this problem while working on another patch :/ It would be nice to have it fixed, major or not.
Here's a new patch, written from scratch to take into account @tstoeckler's feedback from #37.
Comment #49
andypostLooks good to go
Comment #50
alexpottI think it is worthwhile to have a test something like the above to prove that we don't regress the reported bug in some other way.
Is it worth having a static method that does this in a more generic way. I.e. do we do something similar anywhere else?
Comment #51
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedThanks @alexpott for the review :)
Re #50:
1. That's a very good idea, added the test to the patch.
2. We have
\Drupal\Core\Field\FieldInputValueNormalizerTrait::normalizeValue()
with a purpose similar to the code from this patch, but that helper does a lot more work than we need because it is normalizing a field value in an array of field properties keyed by delta, and we just need the scalar value instead. The code here is also more performant because it doesn't require any knowledge about the definition of the bundle field in order to get its main property name.However, to answer your initial question,
FieldInputValueNormalizerTrait::normalizeValue()
is the method that should be used in most cases.Comment #52
alexpottLet's add a code comment here to point out this is an optimised version of \Drupal\Core\Field\FieldInputValueNormalizerTrait::normalizeValue() so people don't do copy pasta unnecessarily.
Comment #53
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedSure thing, expanded the comment :)
Comment #54
alexpottCommitted and pushed c594d13e25 to 8.7.x and 44c3648423 to 8.6.x. Thanks!
Backported to 8.6.x because this is a bugfix that does not have an API impact. The original code path of providing a scalar value is unaffected.