Currently EntityReferenceRevisions does not accept a single scalar value. This is a problem for

- Legacy code, which expects a paragraphs field (for example) to accept entity ID
- Migrations, since the entity generation destination plugins only store Entity ID in the map... and therefore only offer entity ID in their ERR field value

Attached patch accepts a scalar value, which is interpreted as the Entity ID. The Revision ID is looked up and added to the field automatically.

The interdiff and the test_only are mainly the same file since I didn't touch anything on the solution.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

hctom created an issue. See original summary.

hctom’s picture

Status: Active » Needs review
FileSize
795 bytes

And here comes the patch ;)

Status: Needs review » Needs work

The last submitted patch, 2: allow_single_scalar-2667748-2.patch, failed testing.

The last submitted patch, 2: allow_single_scalar-2667748-2.patch, failed testing.

hctom’s picture

Do these errors have anything to do with the actual code change? Also, when reading the error log, it looks like false positives, doesn't it?

webflo’s picture

+++ b/src/Plugin/DataType/EntityReferenceRevisions.php
@@ -114,6 +114,10 @@ class EntityReferenceRevisions extends EntityReference {
+    elseif (is_scalar($value)) {

It is very common that revision id and entity id it not the same. I think it makes sense to set only the target_revision_id and let the the field compute the target_id by itself. The revision is unique anyways.

The properties are wrong, traget_id and target_revision_id is the proper name.

hctom’s picture

I totally agree with webflo. But what do you mean with the property names?

And I just examined the data type class a little more, and I saw there is a lot of inherited code from the EntityReference class, that does not make any changes, but duplicates the parent methods/properties (e.g. getTargetDefinition(), isTargetNew() etc.).

Berdir’s picture

We cleaned up lots of duplicated code but yes, I guess the target property plugin is still remaining.

Note that storing the target_id does have some benefits. Core currently has no multi loading nor caching of any sort for loading revisions.

I noticed that field_collection attempts to optimize that by defaulting to load the current revision first and only fall back to the specific revision if it is in fact not that. Of course, it would be even better to support those two things in core ;)

edysmp’s picture

Status: Needs work » Reviewed & tested by the community
miro_dietiker’s picture

Status: Reviewed & tested by the community » Needs work

RTBC can not be accepted without any statement. Also i see open items in discussions above.

ohthehugemanatee’s picture

Status: Needs work » Needs review
FileSize
5.21 KB

- Reformatted the method use a switch statement instead of a string of elseifs.
- Added handling for missing target_id or target_revision_id when we receive an array
- When both target_id and target_revision_id are specified, validate that they both refer to the same entity before setting the target_id property.

@todo:
- Validating sane values for entity ID and revision ID should really happen in a constraint.
- Inject the entity type manager and logger
- Tests

Status: Needs review » Needs work

The last submitted patch, 11: allow_single_scalar-2667748-11.patch, failed testing.

ohthehugemanatee’s picture

Status: Needs work » Needs review
FileSize
5.23 KB

sometimes integers are mis-identified as strings. Updated patch to handle it.

Status: Needs review » Needs work

The last submitted patch, 13: allow_single_scalar-2667748-12.patch, failed testing.

ohthehugemanatee’s picture

Status: Needs work » Needs review
FileSize
5.25 KB

whoops, used the $entity to work out the entity type, when entity failed to load.

miro_dietiker’s picture

Priority: Normal » Major
Status: Needs review » Needs work
Issue tags: +Needs tests

Trying to raise prio here once to try creating some focus.
We are preparing the next release and i would love to get this in.

As this adds specific code for new cases, we need tests for those.

heddn’s picture

+++ b/src/Plugin/DataType/EntityReferenceRevisions.php
@@ -93,31 +94,111 @@ class EntityReferenceRevisions extends EntityReference {
+        if (isset($value['target_id']) && !empty($value['target_id'])) {

This assumes the keyed value passed in here is target_id or target_revision_id. But if you are doing a migration, as is mentioned in the OP, the value provided back from a paragraphs migration is actually id. This is because the "key" according to paragraph's entity definition is 'id'.

I'd almost prefer using nixing this approach entirely and add a dedicated paragraphs destination class that returns a properly indexed set of values that are usable with the migration process plugin.

heddn’s picture

#2644634-11: Missing Migration Destination in 8.* has some documentation on how to interact with this thing via a migration. I'll leave it up to you guys to decide if there are use cases (outside of migrations) that require the need to create an ERR with a scalar value. But if there aren't such use cases, I'd recommend marking this "won't fix" and point to the docs on how to migrate paragraphs.

ohthehugemanatee’s picture

I'm 400% in favor of a migration destination as the solution to the migration part of the problem... I actually wrote a destination plugin myself, first. BUT

1) Why make a destination specific for Paragraphs? Every line of that destination plugin solves for the general ERR migration use case. The ERR module page says we have aspirations outside of Paragraphs, like inline entity form and field collections. If that's the case, ERR should be concerned about migration in the general case, and should take over that patch.

2) If there is a legitimate need for ERR to accept a simple, rather than compound, value - and I would argue that there is - then the simple value fix also solves a lot of ERR migration cases, without writing a destination class.

Further to point 2: anything written for pre-ERR versions of modules like Paragraphs expects to be able to pass an entity ID. The ability to accept a simple value is therefore a significant improvement in backwards (and cross-) compatibility. I Am Not A Maintainer for this module, but I strongly suggest this is a use case worth considering.

ohthehugemanatee’s picture

Status: Needs work » Needs review

This needs review (and a decision) from a maintainer. Marking as such.

heddn’s picture

Linking to paragraph's migrate issue.

miro_dietiker’s picture

Status: Needs review » Needs work
  1. +++ b/src/Plugin/DataType/EntityReferenceRevisions.php
    @@ -93,31 +94,111 @@ class EntityReferenceRevisions extends EntityReference {
    +      case 'object':
    ...
    +      break;
    

    Indentation.

  2. +++ b/src/Plugin/DataType/EntityReferenceRevisions.php
    @@ -93,31 +94,111 @@ class EntityReferenceRevisions extends EntityReference {
    +          $entity = $entity_storage->load($value['target_id']);
    ...
    +            if (!$entity) {
    +              throw new \InvalidArgumentException('Value is not a valid entity ID.');
    ...
    +            if (!$entity) {
    ...
    +              \Drupal::logger('entity')
    +                ->error('%type %id is invalid. Ignoring the entered entity ID.', array(
    ...
    +              \Drupal::logger('entity')
    +                ->error('%type %id does not contain revision %vid! Ignoring the entered entity ID.', array(
    

    I would stop earlier if $entity load fails or if it doesn't match. And fail hard instead of just logging an error.

  3. +++ b/src/Plugin/DataType/EntityReferenceRevisions.php
    @@ -93,31 +94,111 @@ class EntityReferenceRevisions extends EntityReference {
    +      case 'integer':
    +        $this->revision_id = (int) $value;
    

    After so much spaghetti of loading, here you just consider this a revision_id and skip setting the entity id...
    You don't even try to load it and check if it exist.

I think if you really want to support both cases, we should introduce a helper
protected function loadEntityBy($id, $revision_id)
And it should allow both $id and $revision_id be empty. Internally you do housekeeping (loading, check if entity id matches id, fail hard if something doesn't match) and at the end assign both consistently.

This leads me back to tests. Before, loading was streamlined and simple. Now you add many conditions and thus special cases.
If we want to support those, we want to add tests that trigger failing conditions and check them.

hctom’s picture

Just discussed this topic with ohthehugemanatee and I will give this a try with a generic ERR destination plugin (namespace entity_revision is taken already), cause right now only migrate produces these problems and in all other cases you SHOULD pass in the target ID and the target revision ID as this is the concern of the field type.

So we will give this a complete new start with the destination plugin and leave the setValue() method as is for now.

hctom’s picture

Okay... I do have to row back a little. I tried the approach with the destination plugin, which worked... until you want to get you referenced entity from another migration. The migration processing plugin itself seems only to return a single value but no compound key for fetched entities, as far as I see.

So my new approach now was to use the entity_revision destination plugin, with which the migration processing plugin finally returns the revision ID of the referenced entity, which in fact is unique and can be passed on to the setValue() method as a single scalar value. So for now we should discuss, if we can rely on the fact, that if a scalar value is passed, it has to be a revision ID - which is the only single ID taht makes it possible to reference the correct entity version.
With this approach it is also easy to set the compound value for the field itself with tweaking the method as follows:

    // ...
    elseif (is_scalar($value)) {
      $storage = \Drupal::entityTypeManager()->getStorage($this->getTargetDefinition()->getEntityTypeId());
      if (($entity = $storage->load($value))) {
        $this->id = $entity->id();
        $this->revision_id = $entity->getRevisionId();
      }
    }
    // ...

... of course the entity storage dependency should be injected, but this was a fast draft to see f it is working. And this solved all our problems with wrong connected referenced entities that rely on a specific revision.

So what do you think about it? It is almost the same approach as ohthehugemanatee's patch, but without all the other refactoring.

miro_dietiker’s picture

No patch here. :-)

Thinking about the Scalar case:
If we want to support being a similar target like EntityReference is, we should treat a Scalar similarly. In that case it would be the Entity ID and we would resolve to the default revision.

@@ -93,31 +94,111 @@ class EntityReferenceRevisions extends EntityReference {
We are subclassing EntityReference here, so we should not behave differently with a value passed in.

So how would we solve the migration target case if we stay strict?

In case of a UUID, we could try to support both cases... (if the revision has an own)

ohthehugemanatee’s picture

Status: Needs work » Needs review
FileSize
989 bytes

@miro_dietiker thanks for checking in, and good point about subclassing. We should assume the scalar value is entity ID, and lookup the default revision. Attached hctom's snippet as a patch. Yes, we should inject the service properly, but we don't do it elsewhere in this class either.

The migration problem is a separate issue. But if Migrate returns only one of a set of compound keys, that's a Migrate bug for sure. Scanning through the code it looks like it does a straight up DB query, which would return [sourceid1 => 123, sourceid2 => 456] . Let's look into this deeper in #2809793: EntityReference migrate destination.

ohthehugemanatee’s picture

TIL that typed data plugins can't receive services through dependency injection. #2053415: Allow typed data plugins to receive injected dependencies

Still, calling the global container inline is not nice. Patch re-rolled with entity storage moved into its own property.

Removing "needs tests" tag because we removed the complexity that required it in the first place. Rewriting the description for clarity.

Status: Needs review » Needs work

The last submitted patch, 27: allow-single-scalar-2667748-27.patch, failed testing.

pmelab’s picture

Fixed broken constructor type hints in #27.

stBorchert’s picture

Status: Needs work » Needs review

Update ticket status.

johnchque’s picture

Status: Needs review » Needs work

Nice, but we still need tests. :)

heddn’s picture

The patch above is failing for me in EntityReferenceRevisionsItem->onChange(), where it wants the revision and target id. It fails with null value around ~213.

MegaChriz’s picture

#2843278: Fatal error on migrate-import was closed as a duplicate of this one. If the issues are really exactly the same, then a test for this issue can be found in comment #8 of the duplicate issue.

heddn’s picture

I'd be willing to bet the reason for errors here is one should use the solution adopted in #2809793: EntityReference migrate destination. I'd suggest this get closed as duplicate to it even, but I think that the "bug" folks are seeing is coming from upstream in something like Feeds. The real fix would be to pass in both pieces of info from Feeds, like migrate does.

zweishar’s picture

@heddn From what I can tell it seems like migrations are also affected by this issue. Specifically I'm seeing the exact same behavior as described in #2903495: Migration won't work if we are migrating a translation. when attempting to migrate paragraph translations. That issue was closed as duplicate of this one, which is why I'm commenting here.

bapi_22’s picture

Issue tags: +Migrate critical
n1k’s picture

Rerolled #29 since the patch didn't apply due to changes introduced by #2673076: Investigate loading pattern of field_collection.

cleaver’s picture

Patch #37 made it possible for me to migrate paragraph translations. Thanks!

csedax90’s picture

Patch #37 is working fine, now I could migrate my paragraphs without problems

idimopoulos’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
12.71 KB
12.71 KB
15.29 KB

The default entity reference field supports scalar values so I guess that this is how this should also work.
In my case, I ran across the issue by running behat tests where the entity could not be passed as a value.

I can confirm that the patch resolves the issue and I am also attaching a test for this. I did a small refactor in one of the test classes too to make it more readable and I am attaching a test-only file as well.

The last submitted patch, 40: test_only.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

idimopoulos’s picture

Oops I removed a leftover comment.

pcambra’s picture

Status: Needs review » Reviewed & tested by the community

Found this wile testing a form with IEF and ERR, patch in #42 solves the issue!

Error: Call to a member function getRevisionId() on null in Drupal\\entity_reference_revisions\\Plugin\\Field\\FieldType\\EntityReferenceRevisionsItem->onChange()

DieterHolvoet’s picture

RTBC+. The last patch fixes a couple warnings/errors for me when trying to write a paragraph ID to a paragraph field in a custom migration:

[warning] Illegal string offset 'target_id' EntityReferenceRevisions.php:116
 [warning] Illegal string offset 'target_revision_id' EntityReferenceRevisions.php:116
 [warning] Illegal string offset 'target_id' EntityReferenceRevisions.php:120
 [warning] Illegal string offset 'target_revision_id' EntityReferenceRevisions.php:121
 [error]  Error: Call to a member function getRevisionId() on null in Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem->onChange() (line 216 of /[..]/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php) #0 /[..]/web/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php(132): Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem->onChange('entity', false)

urvashi_vora made their first commit to this issue’s fork.

kreatIL’s picture

RTBC+ (since 2020 in my use case)

Berdir changed the visibility of the branch 2667748-allow-single-scalar to hidden.

Berdir changed the visibility of the branch 2667748-allow-single-scalar to active.

Berdir’s picture

Status: Reviewed & tested by the community » Needs work

Posted a review.