Quantcast
Channel: Issues for Drupal core
Viewing all articles
Browse latest Browse all 295840

Circular dependency between bundle and data type info

$
0
0

Problem/Motivation

Note: This could just as well be marked against the entity system.

Data type info depends on bundle info

\Drupal\Core\Entity\Plugin\DataType\Deriver\EntityDeriver generates a data type for every entity bundle, so that you can, for example, validate that a given data structure is a valid entity:node:article and not just any entity:node. Thus, the data type info depends directly on the entity bundle info.

Bundle info depends on data type info

Bundles for one entity type are usually provided by (configuration) entities of another type. Those other entities, however, are themselves instances of data types, for example entity:node_type. So by loading those entities during the building of the bundle info it "conceptually" depends on the data type info. "Conceptually" here means that due to a variety of circumstances it happens to be the case that the typed data info is never actually primed during the loading of those bundle entities. Thus, as it stands the bundle info can be built correctly and completely without any recursion.

But if - for whatever reason - any contrib or custom module (or combination thereof) leads to TypedDataManager::getDefinitions() being called during the loading of a bundle entity this will start to go south. Examples of how this can happen:

  • You use content entities as bundles* If you have a field with a property constraint on that entity, simply loading the entity will build the field definitions and that will end up calling TypedDataManager::getDefaultConstraints() for that field which calls TypedDataManager::getDefinitions(). Alternatively if you interact with any field FieldTypePluginManager::createFieldItemList() will trigger Entity::getTypedData().
  • You trigger validation Even though generally this is only done before saving an entity, there really is no reason why it should not be allowed to validate an entity on load. You may not be able to resolve any errors, but you may want to log something or show a warning in the user interface that there is a problem.
  • Generally speaking: You trigger Entity::getTypedData() This is a bit vague, but the existence of that method is a clear pointer that you should be allowed to interact with the typed data system during the loading of an entity. JSON:API does this in ResourceObject::extractContentEntityFields() to get a list of non-internal fields. That isn't directly called on entity load, but there's no reason why another module shouldn't be allowed to have that exact information during entity load.

* Even though this is not done in core, there is nothing that prevents you from using a content entity as a bundle entity for another entity. If you know that you are never going to have more than a handful of those entities but you want those entities to be managed by the website admins and not be reverted by a config import, that's a great fit. In fact for many sites things like menus or webforms would be better suited as content entities and those could be bundles for other entities (menu links and webform submissions, for example). That's not to say, that we should necessarily have a "bundle content entity" in core, rather to give some rationale for why that's not as far fetched as it may seem at first thought.

The symptom: missing data types

One might think that if this recursive dependency is triggered as described above and neither the bundle info nor the data type info are cached, this leads to infinite recursion. This is not the case, however, because of the specifics of how EntityTypeBundleInfo works: It first checks whether there is something in its internal static cache before building the bundle info. It then directly (partially) populates that static cache with the result of hook_entity_bundle_info() implementations and proceeds to amend that cache with the info from the various bundle entities. If during the loading of the bundle entities it happens to recurse into itself via TypedDataManager it will find an already - partially! - populated static cache and pass that partial info along. TypedDataManager (or to be precise EntityDeriver), thus, may not derive bundle data types for entities which actually do have bundles, because EntityTypeBundleInfo didn't have those bundles yet. The data type info will then be cached without those bundle data types leading to various errors down the road if those data types are (rightly) assumed to be present on the system.

Assuming no bundle info or data type info cache a more concrete code flow for this could be:

  1. EntityTypeBundleInfo::getAllBundleInfo() gets called by something
  2. EntityTypeBundleInfo::getAllBundleInfo() does $this->bundleInfo = $this->moduleHandler->invokeAll('entity_bundle_info') so that the static cache is no longer empty
  3. EntityTypeBundleInfo::getAllBundleInfo() builds the bundles for the node entity type and calls $this->entityTypeManager->getStorage('node_type')->loadMultiple() for that purpose. At this point the bundles for the taxonomy_term entity type have not been built. (The order here is unspecified so let's just assume this to be the case in this example.)
  4. Some module calls $nodeType->getTypedData() in hook_node_type_load(). (See above for real-world scenarios.)
  5. This calls into the TypedDataManager::getDefinitions() via EntityBase::getTypedDataClass() and TypedDataManager::hasDefinition()
  6. The typed data discovery calls EntityDeriver::getDerivativeDefinitions()
  7. EntityDeriver::getDerivativeDefinitions() calls EntityTypeBundleInfo::getBundleInfo() which calls EntityTypeBundleInfo::getAllBundleInfo(). Since $this->bundleInfo was set to something non-empty in 2. above, it is returned directly. This is repeated for every entity type until taxonomy_term is reached
  8. Since the bundle info for the taxonomy_term entity type is empty, only the entity:taxonomy_term data type is derived, but not entity:taxonomy_term:tags
  9. TypedDataManager::getDefinitions() now caches the list of data types withoutentity:taxonomy_term:tags
  10. The outer call to EntityTypeBundleInfo::getAllBundleInfo() now resumes building the bundle info for all entity types including bundles for taxonomy_term
  11. The entire (correct) bundle info is cached

At this point, given that the entity type bundle info will (correctly) return the tags bundle for the taxonomy_term entity type, it is valid to expect \Drupal::typedDataManager()->getDefinition('entity:taxonomy_term:tags') to work. That will, however, yield a PluginNotFoundException.

Steps to reproduce

  1. Create a module that calls $nodeType->getTypedData() in hook_node_type_load()
  2. Clear the bundle cache
  3. Fetch the bundle info before the data type info is built**
  4. Call \Drupal::typedDataManager()->getDefinition('entity:taxonomy_term:tags')

** If the typed data info is built first, that will call into the entity type bundle info and that will trigger the code flow described above. However, the outer call to TypedDataManager::getDefinitions() will then receive the correct bundle info from the (outer) call to EntityTypeBundleInfo::getAllBundleInfo() and it will overwrite the incorrect cache entry set by the inner call to TypedDataManager::getDefinitions() with the correct data. Thus, in the end everything is correct.

Proposed resolution

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet


Viewing all articles
Browse latest Browse all 295840

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>