Problem/Motivation

EntityManager::getAllBundleInfo() returns bundle information for the node entity type even when there are no node types. This incorrect information leads to bugs in the Views UI and Content translation UI.

This is critical because it can lead to all sort of odd unpredictable behaviour for anything that uses this information. Imagine if you had one node type with the machine name 'content' and you then deleted it. According to EntityManager::getAllBundleInfo() it would still exist. This is nuts.

Proposed resolution

Only return fake bundles when the entity type does have a bundle entity type.

Remaining tasks

Write tests.

User interface changes

None.

API changes

EntityManager::getAllBundleInfo() only creates fake bundles for entity types that are not bundleable.

Data model changes

None

Previous issue summary

That function currently does this to check if an entity_type supports bundles or not:

$optional = $bundle_name != $entity_type;

There is AFAIK no such requirement, you could have an entity foo with bundles foo and bar. Then this would result in very ugly bugs.

I'm wondering if we need that check at all? Can't we just put everything in optional? That would result the complexity of that hook and also hooks that extend/alter the information in there, see #1818556-120: Convert nodes to the new Entity Field API for a bug that was caused by this complexity.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Berdir’s picture

Status: Active » Needs review
FileSize
1.79 KB

Simple, untested patch.

Status: Needs review » Needs work

The last submitted patch, simplify-field_entity_field_info-1919468-1.patch, failed testing.

Berdir’s picture

Status: Needs work » Needs review
FileSize
2.69 KB

Add default bundle logic for no bundle and a single bundle to getFieldProperties().

Status: Needs review » Needs work
Issue tags: -Entity Field API

The last submitted patch, simplify-field_entity_field_info-1919468-3.patch, failed testing.

Berdir’s picture

Status: Needs work » Needs review
Issue tags: +Entity Field API
fago’s picture

+++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php
@@ -699,6 +699,17 @@ public function getFieldDefinitions(array $constraints) {
+      if (count($bundles) == 1) {
+        // If there is only a single bundle, default to it.
+        $bundle = key($bundles);

That we cannot do. That would mean the metadata of an entity changes once you add another bundle - wait, what happened?? ;-)

+++ b/core/modules/field/field.module
@@ -431,8 +431,6 @@ function field_entity_field_info($entity_type) {
-    $optional = $bundle_name != $entity_type;

imho the wtf stems from entity::bundle() defaulting to $entity_type. As you say we do not really know whether it's just a bundle that's called that way or there is no bundle. This really is a field-API left-over we should fix to have the function return FALSE if there is no bundle.

Then, the current concept of having entity-type fields + bundle specific fields saves to have lots of base-fields in bundle maps and allows you to properly add a field to an entity-type without having to care about adding it to all bundles (what is a WTF also). Even worse, this code would have to adapt if a entity-type is altered (to get bundles later on, as first adding fields to "entity_type would suffice while later on not - yuck.

The idea is that you always have a set of fields per entity-type + optional extensions per bundle. That's imho the straight-forward appraoch one would expect.

So what about fixing Entity::bundle() instead??

Berdir’s picture

Hm.

Wouldn't bundle() being FALSE prevent us from differentiating between a entity type that has no bundles one where we don't know the bundle?

I'm actually not sure about the whole bundle mapping stuff. I'm wondering if we shouldn't drop that completely and instead cache by bundle (and a separate for no bundle maybe that would only list fixed properties). Field API switched to that too because it results in *huge* caches otherwise. I have a site with 250 fields and 500 field instances and the global field instance cache alone was between 10 and 15MB. I really don't want to back to that in 8.x.

One problem that it would solve are differences between field instances. Right now, the label is the field name which is IMHO a considerable problem when trying to do something with this information (like extract translatable strings for TMGMT and send it to a translation agency, display in review forms with sane labels and so on). Also, the label is completely useless right now, I already know the field name.

There are also other use cases, for example, there is an issue about making the translatable setting per-bundle, which is impossible right now with NG.

And the entity generic comments table has a very interesting problem of the entity_id pointing to a different entity_type per bundle. That might be possible to define per bundle then although I'm not sure if that would really work. I'm actually not sure at all how that should work, probably needs a DynamicEntityReference where the type is another field, I have multiple modules in contrib that will need something like that. One way or another, doing something like adding a reference in views from comments to nodes is going to be a very interesting problem.

fago’s picture

Wouldn't bundle() being FALSE prevent us from differentiating between a entity type that has no bundles one where we don't know the bundle?

First off that should not matter e.g. when code conditionally incorporates bundle specific logic. Howsoever, we do not support entities without a bundle yet - if they have bundles.

I guess we could have something like $entity_type be our default-bundle if someone adds in bun

I think we could solve per instance modifications like that by relying on methods. So you have a static field definition that's used to create the typed data object. But once you have the object it can take contextual information into account (= the entity's bundle) and add make use or provide us with even more specific metadata then. I.e. it could add the entity type to the reference then.

Berdir’s picture

Status: Needs review » Needs work

Ok, this came back in a different way.

entity_test test entities have a default bundle that is == $entity_type but it's possible to add other bundles, which is used in tests.

This condition caused #2044841: EFQ relationships broken for entity types with bundles to not fail as it should have from the beginning. However, my suggestion above would not have caught it either. Not sure..

One thing that we could do is change the default bundle to something else, but that's going to require tons of field creation changes in tests that reference the bundle and there are two huge (500kb and 300kb) patches already messing around with that right now.

Berdir’s picture

Status: Needs work » Closed (won't fix)

The (required) follow-up of #2047229: Make use of classes for entity field and data definitions to support per-bundle definitions will mean this is a non-issue.

fago’s picture

Title: Single bundle check in field_entity_field_info() is not safe » Clarify handling of entity types without bundle
Category: Bug report » Task
Priority: Normal » Major
Issue summary: View changes
Status: Closed (won't fix) » Active

I gave the bundle issue some more thought. We still have the inconsistency in the system between some code that handles the no-bundle case differently; there is field API related code which does $bundle == $entity_type then and there is code related to the new entity field API that does $bundle == NULL.

Strategy 1: For the new entity field API differentiating between $bundle == NULL and $bundle == 'something' is necessary as it supports the case of having fields attached to NO bundle (e.g. node base fields), while having fields attached to some other bundles at the same time (e.g. fields attached to node article).

Strategy 2: While field API and related code does not support having no bundle fields plus bundle-specific fields at the same time, it only needs to support he no-bundle field case when there are no other bundles - so assigning the no-bundle fields to the $entity_type bundle is fine then. However, as pointed out above, we cannot do that for all entity fields.

Summarizing so far - we've have code that supports no-bundle fields and bundle specific fields at the same time by using strategy (1), and other code which doesn't by using strategy (2). I think this is totally fine as long as this meets the desired use cases.

So what are the use cases we have right now? I'd mostly think of
a) - Defining fields no-bundle fields in code (strategy 1)
b) - Defining configurable fields (strategy 2)
c) - Defining form and view displays (strategy 2)

b&c are perfectly covered by strategy (2) I think, having screens that split up between no-bundle and bundle-specific fields would be certainly confusing. So while code has the power to do strategy (1), only strategy (2) is exposed to end-users.

Let's consider how that works out:
1) The default bundle of every type is $entity_type, as defined by $entity->bundle(). This lets strategy (2) work with the no-bundle case. This means we should always have at least one bundle.
2) Considering a module adding bundles to an entity type which originally has none, there will be new bundles in addition to the default bundle. This means configuration/code using strategy (2) won't apply to the added bundles, but strategy (1) would.
3) Entity types having bundles by default, work fine with strategy (1) and (2).

My conclusions:

  • We've built-in support for a default bundle $entity_type, it would make sense to ensure that $manager->getbundleInfo() honors that and returns the default bundle also. (However, we'd have to take care that the default does not vanish as soon as a module adds a non-default bundles).
  • Given that, the default bundle does not necessarily exist if entity types come with built in bundles, thus an assumption of $bundle == $entity_type -> entity-generic is wrong.
  • The question raised in #2114707: Allow per-bundle overrides of field definitions is how to assign configurable fields to bundles now - should they be assigned using $entity_type or using NULL. The simple approach and from a DX standpoint logical approach seems to be using $entity_type bundle, while from a user-perspective the desired behaviour is probably having them be no-bundle fields. However, that would have more UX implications given displays etc. will have to keep following strategy (2), so this is probably best investigated/covered by a dedicated module.
almaudoh’s picture

How about using a class constant to define a default bundle. This would help differentiate between a no-bundle case and the case where the bundle name is simply the same as the $entity_type.

Not sure where the class constant would reside (maybe EntityManager::NO_BUNDLE or Entity::NO_BUNDLE)

fago’s picture

Issue tags: +beta target

Not sure, whether this is something that is bound by beta but it would make a lot of sense to clarify it before. Reading my conclusions from #11 they still make a lot of sense.

Berdir’s picture

We've built-in support for a default bundle $entity_type, it would make sense to ensure that $manager->getbundleInfo() honors that and returns the default bundle also. (However, we'd have to take care that the default does not vanish as soon as a module adds a non-default bundles).

The first part is true, but I have no idea how we'd be able to do the second part? EntityManager does not and can not remember what was in the past, so I would say that providing this would have to be done by the module that adds the bundles and is aware that this existed?

I don't care about this in file_entity atm, because file is not fieldable before, not sure if I should.

Given that, the default bundle does not necessarily exist if entity types come with built in bundles, thus an assumption of $bundle == $entity_type -> entity-generic is wrong.

Also, there is absolutely nothing stopping you from creating a node type "node" ;).

3. Yes, changing how that works would likely come with a lot of pain ;)

So yes, I agree with you, but what exactly do we need to do? That's how it already works, isn't it?

fago’s picture

The problem we currently have is basically:

The default bundle of every type is $entity_type, as defined by $entity->bundle(). This lets strategy (2) work with the no-bundle case. This means we should always have at least one bundle.

Also, that means $entity->bundle() is not necessarily a bundle that the entity manager knows about, what seems to be bogus to me, i.e. it is inconsistent.

To fix, I think we should add a bundle with the name $entity_type_id always *by default* unless the entity type providing module already provides bundles. We can check that later on also by only considering the entity type providing module implementation.

Berdir’s picture

Title: Clarify handling of entity types without bundle » EntityManager::getAllBundleInfo() adds default entity_type_id bundle for entity types with bundles
Category: Task » Bug report

This is almost already like it is right now, but as discussed, for modules that rely on a bundle, we should *not* define a default bundle.

We need to figure out how to deal with entity types where another module adds bundle (provider entity type != provider bundle entity type?)

This means that this is a bug, because that is how it worked in 7.x, so changing accordingly.

Berdir’s picture

Status: Active » Needs review
FileSize
937 bytes

Let's try this.

fago’s picture

That looks good, it still leave the issue of an 3rd party module adding bundles later on open though.

jhedstrom’s picture

Status: Needs review » Needs work

Moving to needs work as per

it still leave the issue of an 3rd party module adding bundles later on open though

alexpott’s picture

Status: Needs work » Needs review
Issue tags: +Needs tests
FileSize
4.84 KB

So this rears its head and causes problems in two ways. It breaks content translation UI when there are no node types and it breaks the Views UI Node wizard when there are no node types.

Steps to reproduce:

  1. Install minimal profile
  2. Install Content translation module
  3. Install Views UI module
  4. Go to admin/config/regional/content-language and check the Content tickboxs that appear
  5. Press save... kaboom WSOD.
  6. Go to admin/structure/views/add and you'll be able to create a view listing nodes of type content - but note that no content types exist! If you do add such a view it'll break when you save it because the bundle does not exist.

I think EntityTypeBundleInfo::getAllBundleInfo() should not return anything for an entity type which says this other entity type is my bundle type and there are none of them.no node types. The patch attached is a slightly different approach from #17 and things have been moved a lot so no interdiff.

catch’s picture

Issue tags: -beta target +rc target

Discussed with @alexpott, given this is a fatal error via the UI and should be non-disruptive, tagging rc target.

alexpott’s picture

@catch I'm not sure about the non-disruptiveness of this change since we're fixing what is returned by EntityTypeBundleInfo::getAllBundleInfo()

Status: Needs review » Needs work

The last submitted patch, 20: 1919468-20.patch, failed testing.

alexpott’s picture

Status: Needs work » Needs review

Fixing the tests - it looks the behaviour of Drupal\migrate\Plugin\migrate\destination\EntityContentBase::processStubRow() needs fixing to cope with the fact that the an entity type now might not have bundles - this is an example of the type of disruption this patch will cause.

Status: Needs review » Needs work

The last submitted patch, 20: 1919468-20.patch, failed testing.

alexpott’s picture

Status: Needs work » Needs review
FileSize
4.42 KB
9.27 KB
alexpott’s picture

Priority: Major » Critical

I think this is a critical bug. With the current EntityManager::getAllBundleInfo() we are saying that bundleable entity types have bundles when they have none. This can lead to all sort of odd unpredictable behaviour for anything that uses this information. The only reason why we haven't come up against it is most times an entity type that uses bundles has at least one. However, imagine if you had one node type with the machine name 'content' and you then deleted it. According to EntityManager::getAllBundleInfo() it would still exist. This is nuts.

I've made this critical because:

  • I don't think it will as easy to change this behaviour after 8.0.0
  • I think we need to fix this.
alexpott’s picture

Issue summary: View changes
catch’s picture

Status: Needs review » Reviewed & tested by the community

Looks great, one nit fixable on commit.

+++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php
@@ -97,18 +97,16 @@ public function getAllBundleInfo() {
+          // First look for entity types that act as bundles for others, load them

Over 80 chars due to the indentation change.

Given we're short on time, bumping this to RTBC. I'll give this several hours and commit either this evening or tomorrow if no objections.

The last submitted patch, 20: 1919468-20.patch, failed testing.

plach’s picture

Big RTBC +1

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

Just working on some explicit test coverage

alexpott’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
3.59 KB
12.86 KB

Here's some tests - I tried to add to the unit test but the just was too much mocking to be done and would have required large changes to the way the test is setUp.

The last submitted patch, 33: 1919468-33.test-only.patch, failed testing.

Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php
    @@ -97,18 +97,16 @@ public function getAllBundleInfo() {
    -          // If no bundles are provided, use the entity type name and label.
    -          if (!isset($this->bundleInfo[$type])) {
    +          else {
    +            // If bundles are not supported, use the entity type name and label.
    

    I think we need to keep the !isset() check here. We still have the hook as a standalone way to set non-entity type based bundles. This would also add the fallback for those.

  2. +++ b/core/modules/views/src/Tests/Wizard/BasicTest.php
    @@ -189,7 +189,10 @@ public function testWizardForm() {
         $this->drupalPostAjaxForm(NULL, array('show[wizard_key]' => 'node'), 'show[wizard_key]');
    -    $this->assertFieldByName('show[type]', 'all', 'The "of type" filter is added for nodes.');
    +    $this->assertNoFieldByName('show[type]', 'all', 'The "of type" filter is not added for nodes when there are no node types.');
    

    It doesn't really make sense to do *anything* with nodes when there are no types I guess but this is probably good enough for now.

alexpott’s picture

Thanks @Berdir

  1. Fixed and added test coverage.
  2. Yeah I agree there is not much point in having the views wizard and allowing node views if no bundles exist. But this seems the quickest fix.
Berdir’s picture

Status: Needs review » Reviewed & tested by the community

Nice, looks good to me now!

The last submitted patch, 20: 1919468-20.patch, failed testing.

The last submitted patch, 26: 1919468-24.patch, failed testing.

The last submitted patch, 33: 1919468-33.test-only.patch, failed testing.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 36: 1919468-36.patch, failed testing.

alexpott’s picture

Status: Needs work » Reviewed & tested by the community

Unrelated test fails.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 36: 1919468-36.patch, failed testing.

The last submitted patch, 20: 1919468-20.patch, failed testing.

The last submitted patch, 33: 1919468-33.test-only.patch, failed testing.

The last submitted patch, 36: 1919468-36.patch, failed testing.

alexpott’s picture

Status: Needs work » Reviewed & tested by the community

Back to rtbc - these installer fails have nothing to do with this patch.

The last submitted patch, 20: 1919468-20.patch, failed testing.

The last submitted patch, 33: 1919468-33.test-only.patch, failed testing.

The last submitted patch, 33: 1919468-33.patch, failed testing.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.0.x, thanks!

  • catch committed 99507a3 on 8.0.x
    Issue #1919468 by alexpott, Berdir: EntityManager::getAllBundleInfo()...

  • catch committed 99507a3 on 8.1.x
    Issue #1919468 by alexpott, Berdir: EntityManager::getAllBundleInfo()...

Status: Fixed » Closed (fixed)

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