Problem/Motivation

The EntityTypeRepository checks that there is only one class per entity type. However, in contrib such as ECK, entity bundle classes are generated dynamically from configuration with the module not able to dynamically create classes per configured bundle. This means that the class itself must be the same as PHP does not support Generics. So creating two bundles derived from the same ECK entity currently throws an AmbiguousEntityClassException.

EntityTypeRepositoryInterface::getEntityTypeFromClass triggers this issue. The workaround to manually create a class for every ECK entity type renders the module unusable, but even if it were to be possible, it would still fail on this line (is_subclass_of($class_name, $entity_type->getOriginalClass())) because the original class is still Drupal\eck\Entity\EckEntity, regardless of any overrides.

The code in question was introduced in #2570593.

Steps to reproduce

  1. Install the eck module
  2. Create two eck entity types + entity type classes subclassing EckEntity, and register them using hook_entity_type_alter
  3. Create a bundle on one of those entity types + a class for that bundle, and register it using hook_entity_bundle_info_alter
  4. Call EntityTypeRepository::getEntityTypeFromClass() with the bundle class name.

Proposed resolution

Require passing entity bundle info to the EntityTypeRepository to avoid ambiguous calls when there are two or more bundles for the same entity class.

Issue fork drupal-3257457

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:

Comments

DieterHolvoet created an issue. See original summary.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

dieterholvoet’s picture

Title: EntityTypeRepositoryInterface::getEntityTypeFromClass fails with eck module » AmbiguousBundleClassException if multiple entity types share the same class
Status: Active » Needs review

I created a MR with a proposed fix.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Bug Smash Initiative, +Needs tests

Moving back to NW. The MR is failing the test cases. If it's a bug it will need it's own test case also.

Would you say this is a duplicate of https://www.drupal.org/project/drupal/issues/3233143#comment-14631762 ?

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

dieterholvoet’s picture

StatusFileSize
new5.33 KB

I fixed the existing test and added a new test demonstrating the bug.

dieterholvoet’s picture

StatusFileSize
new5.16 KB
dieterholvoet’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests

The test-only patch is failing, the MR with the fix is passing. Setting to Needs review.

smustgrave’s picture

Status: Needs review » Needs work

Can the MR be updated in 11.x?

Also will need a trigger_error for the new parameter. To not break existing sites.

dieterholvoet’s picture

I created a draft change notice to mention in the deprecation message. Am I correct if I'm saying this will be deprecated in drupal:11.0.0 and required in drupal:12.0.0?

dieterholvoet’s picture

Status: Needs work » Needs review
smustgrave’s picture

Not deprecated. Would have to look up an example but required in D11 but added in 10.2

11.x is actually going to be 10.2
Think of it as a master branch now and releases will be off 11.x till gitlab

dieterholvoet’s picture

Okay, I updated the versions in code and in the change record.

smustgrave’s picture

Status: Needs review » Needs work

For the CC failure in the MR.

dieterholvoet’s picture

Status: Needs work » Needs review
smustgrave’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Needs Review Queue Initiative

Thanks for taking care of that

Running the tests locally without the fix fails correctly

Drupal\Core\Entity\Exception\AmbiguousBundleClassException : Multiple entity types found for Multiple bundles are using the bundle class Drupal\Tests\Core\Entity\RoyalGala..

So even though the issue summary is talking eck tests show this is possible without a contrib too.

larowlan’s picture

Status: Reviewed & tested by the community » Needs review

Coming from the original issue, this was added for following use-case

Say you have a node-type called page with a bundle class \Drupal\mymodule\Entity\Bundle\Page

You should be able to do the following

use \Drupal\mymodule\Entity\Bundle\Page;

// Note we don't pass ['type' => 'page'] here.
$page = Page::create(['title' => 'A page']);
// But this will still echo 'page'.
echo $page->bundle();

So it would be good to confirm we have test coverage for that, and that this doesn't cause a regression.

If you can identify an existing test and/or confirm this still works - fine to self RTBC

smustgrave’s picture

Status: Needs review » Needs work

Searching for a test that has a namespace for \Entity\Bundle\ I don't find anything so guess we don't have test coverage for that scenario.

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

scott_euser’s picture

Status: Needs work » Needs review

Added a line to BundleClassTest::testEntitySubclass() to verify that ->bundle() still returns the given bundle class. Not 100% sure though if that is what was meant in #19.

smustgrave’s picture

Status: Needs review » Needs work

Left some comments on MR.

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

ankithashetty’s picture

Status: Needs work » Needs review

MR updated as per the feedbacks shared on MR. Also rebased it.
Thanks!

smustgrave’s picture

Status: Needs review » Needs work

Still failing.

scott_euser’s picture

Status: Needs work » Needs review

Probably technically a problem with the coding standard itself as having a full stop to end the sentence after a version number probably should be allowed. Anyways I updated this to follow the sentence structure of similar deprecations.

smustgrave’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

Tests are green,

I see there is already a change record

Issue summary appears complete.

Believe this is good to go.

dieterholvoet’s picture

Is there anything left that needs to be done here?

dww’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: +Needs subsystem maintainer review

Before this is RTBC, I believe this needs a closer look by the entity system maintainers, and probably more of the folks involved in the original bundle subclass efforts. I'm not officially an entity subsys maintainer, so I can't sign-off on that side. I would need more time to carefully review this to be able to "sign-off" (such as it is) as one of the bundle subclass "sub-subsystem maintainers". 😅

berdir’s picture

The issue summary needs to be updated, it doesn't explain what the patch is trying to do.

> Leave out is_subclass_of($class_name, $entity_type->getOriginalClass())? I'm not sure why that condition exists.

That condition exists because if you change the bundle class of Article, then Node::load() must still work, so we also always need to check and support the original class, and we must not break existing code that might not even know that there is a bundle class.

I would expect that tests fail in this scenario and that we have tests for this.

dieterholvoet’s picture

Okay, that changes things. Drupal assumes that EntityType->originalClass contains the entity type class, but in ECK's case it contains a common entity class that can be used for different entity types. Who's in the wrong here? I would think ECK, because it defines Drupal\eck\Entity\EckEntity as the entity type class, which it isn't. But on the other hand, there's no other way for ECK to define a common entity class for all its entity types. Users would be required to manually create classes for every entity type, but that would kinda defeat the purpose of the module. Or we could consider automatic code generation, but that's a can of worms I'd rather keep closed.

smustgrave’s picture

Status: Needs review » Needs work

Thanks @berdir for taking a look!

berdir’s picture

Ok, I need to clarify/extend #31 a bit, after a closer look at the changed code and the context. I don't think the problem is the original class, the MR anyway doesn't just remove that, so that question/suggestion in the issue summary is misleading and definitely needs an update.

Unless it's altered, orignalClass is identical to the current class and I'm pretty sure that is the case here, entityClass is also going to be the same for all of them. The part that I mentioned in #31 should still work, a practical use case for that is the file_entity module in contrib, and that's not changed.

I understand the proposed change a bit better now and it makes sense. I was wondering if we could drop the duplicate check entirely but it's technically still possible to register the same bundle class for two different entity types, especially with ECK, and then you can't do BundleClass::load().

Apart from the issue summary update, I would propose that we change the logic to be a positive check and not negative with continue, this seems to be optimized for fewest line changes in the MR, but the resulting code is harder to read IMHO and that's what people will look at in the future. Instead I would do this:

if (isset($info['class']) && $info['class'] === $class_name) {
  $entity_type_id = $info_entity_type_id;
  // same class check
}}

that means an indentation change for the same class check, but we also have an extra loop now, the check itself is closer to what we have now in HEAD.

scott_euser’s picture

Issue summary: View changes
Issue tags: -Needs subsystem maintainer review

Attempt at updating the issue summary. Removed subsystem maintainer review needed, as @Berdir has reviewed (or should it stay there, apologies if wrong to remove).

scott_euser’s picture

Status: Needs work » Needs review

Okay updated the code as per #34 and best I could do with issue summary from my understanding of it. Tests are passing again (issue fork was missing 11.x branch), so I believe this is now ready for review again.

Edit: just thought it would be helpful to note I did also retest this using ECK creating multiple bundle classes and using eg BundleClassNameHere::create($properties) - which subsequently calls the problem code in question. 11.x branch it fails and on this branch I can successfully run the code.

smustgrave’s picture

Changes look good.

Left 2 small nitpicky suggestions.

Will leave in review for additional eyes.

scott_euser’s picture

Thanks for checking! Fixed the nits and some coding standards issues, tests green again. Ready for review.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Been about a week and don't want the issue to stale so will mark.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

Nice to see this close, couple of questions on the MR - thanks!

larowlan’s picture

Issue credits

scott_euser’s picture

Fixed the phpcs:ignore issue.

Are there any existing test entity-types we could use to test this in a kernel test? The amount of mocking has me wary of a false sense of security here.

Hmmm maybe @Berdir or someone could point me in the right direction and I could work on implementation within the tests? For now all I can think of is creating a test module that effectively reproduces ECK as minimally as possible. Not sure if that is what you are after though @larowlan?

scott_euser’s picture

Status: Needs work » Needs review

Okay, new test added that creates two bundle classes of the same entity class dynamically. Tests only triggers the ambiguous bundle class exception on it, with the change, problem solved. This is as minimal as I could manage to mimic what ECK is doing there.

Perhaps someone else might have a cleaner idea, but I think it resolves @larowlan's concern about the amount of mocking.

Some notes/further thoughts:

  1. Was not sure whether I should remove any of the existing new tests from this branch or keep them in
  2. The full test run first failed on core/tests/Drupal/Tests/Core/Command/QuickStartTest.php, I could not understand why - running locally no issue and re-running no issue. Seems unrelated..
smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Believe all feedback from @larowlan has been addressed.

Applied 2 very nitpicky return types.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

PHPUnit tests look like they are failing.

smustgrave’s picture

Status: Needs work » Reviewed & tested by the community

Rebased the MR.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I've left some comments on the MR - we just need to tidy up a few things I think.

smustgrave’s picture

@scott_euser don't want to step on any toes. let me know if you want me to try and take a shot at it.

scott_euser’s picture

@smustgrave all good if you want to give it a shot, I likely won't be able to for a couple of weeks. Thanks!

smustgrave’s picture

Status: Needs work » Needs review

Today is a contribution day of mine so had time to take a look, let me know what you all think.

scott_euser’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me, thanks for taking care of it! Back to RTBC

alexpott changed the visibility of the branch 11.x to hidden.

alexpott’s picture

Version: 11.x-dev » 10.3.x-dev
Status: Reviewed & tested by the community » Fixed

In #34 @Berdir asked for the subclass check to be kept around... but I think we already have that covered in \Drupal\Core\Entity\ContentEntityStorageBase::getEntityClass() and the \Drupal\Core\Entity\Exception\BundleClassInheritanceException it can throw.

Committed and pushed 644d8f2abd to 11.x and f342c259e9 to 10.3.x. Thanks!

  • alexpott committed f342c259 on 10.3.x
    Issue #3257457 by DieterHolvoet, scott_euser, smustgrave, ankithashetty...

  • alexpott committed 644d8f2a on 11.x
    Issue #3257457 by DieterHolvoet, scott_euser, smustgrave, ankithashetty...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.