Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / Entity / EntityFieldManager.php
1 <?php
2
3 namespace Drupal\Core\Entity;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Cache\CacheBackendInterface;
7 use Drupal\Core\Cache\UseCacheBackendTrait;
8 use Drupal\Core\Extension\ModuleHandlerInterface;
9 use Drupal\Core\Field\BaseFieldDefinition;
10 use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
11 use Drupal\Core\Language\LanguageManagerInterface;
12 use Drupal\Core\StringTranslation\StringTranslationTrait;
13 use Drupal\Core\TypedData\TypedDataManagerInterface;
14
15 /**
16  * Manages the discovery of entity fields.
17  *
18  * This includes field definitions, base field definitions, and field storage
19  * definitions.
20  */
21 class EntityFieldManager implements EntityFieldManagerInterface {
22
23   use UseCacheBackendTrait;
24   use StringTranslationTrait;
25
26   /**
27    * Extra fields by bundle.
28    *
29    * @var array
30    */
31   protected $extraFields = [];
32
33   /**
34    * Static cache of base field definitions.
35    *
36    * @var array
37    */
38   protected $baseFieldDefinitions;
39
40   /**
41    * Static cache of field definitions per bundle and entity type.
42    *
43    * @var array
44    */
45   protected $fieldDefinitions;
46
47   /**
48    * Static cache of field storage definitions per entity type.
49    *
50    * Elements of the array:
51    *  - $entity_type_id: \Drupal\Core\Field\BaseFieldDefinition[]
52    *
53    * @var array
54    */
55   protected $fieldStorageDefinitions;
56
57   /**
58    * An array keyed by entity type. Each value is an array whose keys are
59    * field names and whose value is an array with two entries:
60    *   - type: The field type.
61    *   - bundles: The bundles in which the field appears.
62    *
63    * @var array
64    */
65   protected $fieldMap = [];
66
67   /**
68    * An array keyed by field type. Each value is an array whose key are entity
69    * types including arrays in the same form that $fieldMap.
70    *
71    * It helps access the mapping between types and fields by the field type.
72    *
73    * @var array
74    */
75   protected $fieldMapByFieldType = [];
76
77   /**
78    * The typed data manager.
79    *
80    * @var \Drupal\Core\TypedData\TypedDataManagerInterface
81    */
82   protected $typedDataManager;
83
84   /**
85    * The language manager.
86    *
87    * @var \Drupal\Core\Language\LanguageManagerInterface
88    */
89   protected $languageManager;
90
91   /**
92    * The key-value factory.
93    *
94    * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
95    */
96   protected $keyValueFactory;
97
98   /**
99    * The module handler.
100    *
101    * @var \Drupal\Core\Extension\ModuleHandlerInterface
102    */
103   protected $moduleHandler;
104
105   /**
106    * The entity type manager.
107    *
108    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
109    */
110   protected $entityTypeManager;
111
112   /**
113    * The entity type bundle info.
114    *
115    * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
116    */
117   protected $entityTypeBundleInfo;
118
119   /**
120    * The entity display repository.
121    *
122    * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
123    */
124   protected $entityDisplayRepository;
125
126   /**
127    * Constructs a new EntityFieldManager.
128    *
129    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
130    *   The entity type manager.
131    * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
132    *   The entity type bundle info.
133    * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
134    *   The entity display repository.
135    * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
136    *   The typed data manager.
137    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
138    *   The language manager.
139    * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
140    *   The key-value factory.
141    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
142    *   The module handler.
143    * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
144    *   The cache backend.
145    */
146   public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityDisplayRepositoryInterface $entity_display_repository, TypedDataManagerInterface $typed_data_manager, LanguageManagerInterface $language_manager, KeyValueFactoryInterface $key_value_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend) {
147     $this->entityTypeManager = $entity_type_manager;
148     $this->entityTypeBundleInfo = $entity_type_bundle_info;
149     $this->entityDisplayRepository = $entity_display_repository;
150
151     $this->typedDataManager = $typed_data_manager;
152     $this->languageManager = $language_manager;
153     $this->keyValueFactory = $key_value_factory;
154     $this->moduleHandler = $module_handler;
155     $this->cacheBackend = $cache_backend;
156   }
157
158   /**
159    * {@inheritdoc}
160    */
161   public function getBaseFieldDefinitions($entity_type_id) {
162     // Check the static cache.
163     if (!isset($this->baseFieldDefinitions[$entity_type_id])) {
164       // Not prepared, try to load from cache.
165       $cid = 'entity_base_field_definitions:' . $entity_type_id . ':' . $this->languageManager->getCurrentLanguage()->getId();
166       if ($cache = $this->cacheGet($cid)) {
167         $this->baseFieldDefinitions[$entity_type_id] = $cache->data;
168       }
169       else {
170         // Rebuild the definitions and put it into the cache.
171         $this->baseFieldDefinitions[$entity_type_id] = $this->buildBaseFieldDefinitions($entity_type_id);
172         $this->cacheSet($cid, $this->baseFieldDefinitions[$entity_type_id], Cache::PERMANENT, ['entity_types', 'entity_field_info']);
173       }
174     }
175     return $this->baseFieldDefinitions[$entity_type_id];
176   }
177
178   /**
179    * Builds base field definitions for an entity type.
180    *
181    * @param string $entity_type_id
182    *   The entity type ID. Only entity types that implement
183    *   \Drupal\Core\Entity\FieldableEntityInterface are supported.
184    *
185    * @return \Drupal\Core\Field\FieldDefinitionInterface[]
186    *   An array of field definitions, keyed by field name.
187    *
188    * @throws \LogicException
189    *   Thrown if a config entity type is given or if one of the entity keys is
190    *   flagged as translatable.
191    */
192   protected function buildBaseFieldDefinitions($entity_type_id) {
193     $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
194     $class = $entity_type->getClass();
195     $keys = array_filter($entity_type->getKeys());
196
197     // Fail with an exception for non-fieldable entity types.
198     if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
199       throw new \LogicException("Getting the base fields is not supported for entity type {$entity_type->getLabel()}.");
200     }
201
202     // Retrieve base field definitions.
203     /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $base_field_definitions */
204     $base_field_definitions = $class::baseFieldDefinitions($entity_type);
205
206     // Make sure translatable entity types are correctly defined.
207     if ($entity_type->isTranslatable()) {
208       // The langcode field should always be translatable if the entity type is.
209       if (isset($keys['langcode']) && isset($base_field_definitions[$keys['langcode']])) {
210         $base_field_definitions[$keys['langcode']]->setTranslatable(TRUE);
211       }
212       // A default_langcode field should always be defined.
213       if (!isset($base_field_definitions[$keys['default_langcode']])) {
214         $base_field_definitions[$keys['default_langcode']] = BaseFieldDefinition::create('boolean')
215           ->setLabel($this->t('Default translation'))
216           ->setDescription($this->t('A flag indicating whether this is the default translation.'))
217           ->setTranslatable(TRUE)
218           ->setRevisionable(TRUE)
219           ->setDefaultValue(TRUE);
220       }
221     }
222
223     // Make sure that revisionable entity types are correctly defined.
224     if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
225       // The 'revision_translation_affected' field should always be defined.
226       // This field has been added unconditionally in Drupal 8.4.0 and it is
227       // overriding any pre-existing definition on purpose so that any
228       // differences are immediately available in the status report.
229       $base_field_definitions[$keys['revision_translation_affected']] = BaseFieldDefinition::create('boolean')
230         ->setLabel($this->t('Revision translation affected'))
231         ->setDescription($this->t('Indicates if the last edit of a translation belongs to current revision.'))
232         ->setReadOnly(TRUE)
233         ->setRevisionable(TRUE)
234         ->setTranslatable(TRUE);
235     }
236
237     // Assign base field definitions the entity type provider.
238     $provider = $entity_type->getProvider();
239     foreach ($base_field_definitions as $definition) {
240       // @todo Remove this check once FieldDefinitionInterface exposes a proper
241       //  provider setter. See https://www.drupal.org/node/2225961.
242       if ($definition instanceof BaseFieldDefinition) {
243         $definition->setProvider($provider);
244       }
245     }
246
247     // Retrieve base field definitions from modules.
248     foreach ($this->moduleHandler->getImplementations('entity_base_field_info') as $module) {
249       $module_definitions = $this->moduleHandler->invoke($module, 'entity_base_field_info', [$entity_type]);
250       if (!empty($module_definitions)) {
251         // Ensure the provider key actually matches the name of the provider
252         // defining the field.
253         foreach ($module_definitions as $field_name => $definition) {
254           // @todo Remove this check once FieldDefinitionInterface exposes a
255           //  proper provider setter. See https://www.drupal.org/node/2225961.
256           if ($definition instanceof BaseFieldDefinition && $definition->getProvider() == NULL) {
257             $definition->setProvider($module);
258           }
259           $base_field_definitions[$field_name] = $definition;
260         }
261       }
262     }
263
264     // Automatically set the field name, target entity type and bundle
265     // for non-configurable fields.
266     foreach ($base_field_definitions as $field_name => $base_field_definition) {
267       if ($base_field_definition instanceof BaseFieldDefinition) {
268         $base_field_definition->setName($field_name);
269         $base_field_definition->setTargetEntityTypeId($entity_type_id);
270         $base_field_definition->setTargetBundle(NULL);
271       }
272     }
273
274     // Invoke alter hook.
275     $this->moduleHandler->alter('entity_base_field_info', $base_field_definitions, $entity_type);
276
277     // Ensure defined entity keys are there and have proper revisionable and
278     // translatable values.
279     foreach (array_intersect_key($keys, array_flip(['id', 'revision', 'uuid', 'bundle'])) as $key => $field_name) {
280       if (!isset($base_field_definitions[$field_name])) {
281         throw new \LogicException("The $field_name field definition does not exist and it is used as $key entity key.");
282       }
283       if ($base_field_definitions[$field_name]->isRevisionable()) {
284         throw new \LogicException("The {$base_field_definitions[$field_name]->getLabel()} field cannot be revisionable as it is used as $key entity key.");
285       }
286       if ($base_field_definitions[$field_name]->isTranslatable()) {
287         throw new \LogicException("The {$base_field_definitions[$field_name]->getLabel()} field cannot be translatable as it is used as $key entity key.");
288       }
289     }
290
291     // Make sure translatable entity types define the "langcode" field properly.
292     if ($entity_type->isTranslatable() && (!isset($keys['langcode']) || !isset($base_field_definitions[$keys['langcode']]) || !$base_field_definitions[$keys['langcode']]->isTranslatable())) {
293       throw new \LogicException("The {$entity_type->getLabel()} entity type cannot be translatable as it does not define a translatable \"langcode\" field.");
294     }
295
296     return $base_field_definitions;
297   }
298
299   /**
300    * {@inheritdoc}
301    */
302   public function getFieldDefinitions($entity_type_id, $bundle) {
303     if (!isset($this->fieldDefinitions[$entity_type_id][$bundle])) {
304       $base_field_definitions = $this->getBaseFieldDefinitions($entity_type_id);
305       // Not prepared, try to load from cache.
306       $cid = 'entity_bundle_field_definitions:' . $entity_type_id . ':' . $bundle . ':' . $this->languageManager->getCurrentLanguage()->getId();
307       if ($cache = $this->cacheGet($cid)) {
308         $bundle_field_definitions = $cache->data;
309       }
310       else {
311         // Rebuild the definitions and put it into the cache.
312         $bundle_field_definitions = $this->buildBundleFieldDefinitions($entity_type_id, $bundle, $base_field_definitions);
313         $this->cacheSet($cid, $bundle_field_definitions, Cache::PERMANENT, ['entity_types', 'entity_field_info']);
314       }
315       // Field definitions consist of the bundle specific overrides and the
316       // base fields, merge them together. Use array_replace() to replace base
317       // fields with by bundle overrides and keep them in order, append
318       // additional by bundle fields.
319       $this->fieldDefinitions[$entity_type_id][$bundle] = array_replace($base_field_definitions, $bundle_field_definitions);
320     }
321     return $this->fieldDefinitions[$entity_type_id][$bundle];
322   }
323
324   /**
325    * Builds field definitions for a specific bundle within an entity type.
326    *
327    * @param string $entity_type_id
328    *   The entity type ID. Only entity types that implement
329    *   \Drupal\Core\Entity\FieldableEntityInterface are supported.
330    * @param string $bundle
331    *   The bundle.
332    * @param \Drupal\Core\Field\FieldDefinitionInterface[] $base_field_definitions
333    *   The list of base field definitions.
334    *
335    * @return \Drupal\Core\Field\FieldDefinitionInterface[]
336    *   An array of bundle field definitions, keyed by field name. Does
337    *   not include base fields.
338    */
339   protected function buildBundleFieldDefinitions($entity_type_id, $bundle, array $base_field_definitions) {
340     $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
341     $class = $entity_type->getClass();
342
343     // Allow the entity class to provide bundle fields and bundle-specific
344     // overrides of base fields.
345     $bundle_field_definitions = $class::bundleFieldDefinitions($entity_type, $bundle, $base_field_definitions);
346
347     // Load base field overrides from configuration. These take precedence over
348     // base field overrides returned above.
349     $base_field_override_ids = array_map(function ($field_name) use ($entity_type_id, $bundle) {
350       return $entity_type_id . '.' . $bundle . '.' . $field_name;
351     }, array_keys($base_field_definitions));
352     $base_field_overrides = $this->entityTypeManager->getStorage('base_field_override')->loadMultiple($base_field_override_ids);
353     foreach ($base_field_overrides as $base_field_override) {
354       /** @var \Drupal\Core\Field\Entity\BaseFieldOverride $base_field_override */
355       $field_name = $base_field_override->getName();
356       $bundle_field_definitions[$field_name] = $base_field_override;
357     }
358
359     $provider = $entity_type->getProvider();
360     foreach ($bundle_field_definitions as $definition) {
361       // @todo Remove this check once FieldDefinitionInterface exposes a proper
362       //  provider setter. See https://www.drupal.org/node/2225961.
363       if ($definition instanceof BaseFieldDefinition) {
364         $definition->setProvider($provider);
365       }
366     }
367
368     // Retrieve base field definitions from modules.
369     foreach ($this->moduleHandler->getImplementations('entity_bundle_field_info') as $module) {
370       $module_definitions = $this->moduleHandler->invoke($module, 'entity_bundle_field_info', [$entity_type, $bundle, $base_field_definitions]);
371       if (!empty($module_definitions)) {
372         // Ensure the provider key actually matches the name of the provider
373         // defining the field.
374         foreach ($module_definitions as $field_name => $definition) {
375           // @todo Remove this check once FieldDefinitionInterface exposes a
376           //  proper provider setter. See https://www.drupal.org/node/2225961.
377           if ($definition instanceof BaseFieldDefinition) {
378             $definition->setProvider($module);
379           }
380           $bundle_field_definitions[$field_name] = $definition;
381         }
382       }
383     }
384
385     // Automatically set the field name, target entity type and bundle
386     // for non-configurable fields.
387     foreach ($bundle_field_definitions as $field_name => $field_definition) {
388       if ($field_definition instanceof BaseFieldDefinition) {
389         $field_definition->setName($field_name);
390         $field_definition->setTargetEntityTypeId($entity_type_id);
391         $field_definition->setTargetBundle($bundle);
392       }
393     }
394
395     // Invoke 'per bundle' alter hook.
396     $this->moduleHandler->alter('entity_bundle_field_info', $bundle_field_definitions, $entity_type, $bundle);
397
398     return $bundle_field_definitions;
399   }
400
401   /**
402    * {@inheritdoc}
403    */
404   public function getFieldStorageDefinitions($entity_type_id) {
405     if (!isset($this->fieldStorageDefinitions[$entity_type_id])) {
406       $this->fieldStorageDefinitions[$entity_type_id] = [];
407       // Add all non-computed base fields.
408       foreach ($this->getBaseFieldDefinitions($entity_type_id) as $field_name => $definition) {
409         if (!$definition->isComputed()) {
410           $this->fieldStorageDefinitions[$entity_type_id][$field_name] = $definition;
411         }
412       }
413       // Not prepared, try to load from cache.
414       $cid = 'entity_field_storage_definitions:' . $entity_type_id . ':' . $this->languageManager->getCurrentLanguage()->getId();
415       if ($cache = $this->cacheGet($cid)) {
416         $field_storage_definitions = $cache->data;
417       }
418       else {
419         // Rebuild the definitions and put it into the cache.
420         $field_storage_definitions = $this->buildFieldStorageDefinitions($entity_type_id);
421         $this->cacheSet($cid, $field_storage_definitions, Cache::PERMANENT, ['entity_types', 'entity_field_info']);
422       }
423       $this->fieldStorageDefinitions[$entity_type_id] += $field_storage_definitions;
424     }
425     return $this->fieldStorageDefinitions[$entity_type_id];
426   }
427
428   /**
429    * {@inheritdoc}
430    */
431   public function setFieldMap(array $field_map) {
432     $this->fieldMap = $field_map;
433     return $this;
434   }
435
436   /**
437    * {@inheritdoc}
438    */
439   public function getFieldMap() {
440     if (!$this->fieldMap) {
441       // Not prepared, try to load from cache.
442       $cid = 'entity_field_map';
443       if ($cache = $this->cacheGet($cid)) {
444         $this->fieldMap = $cache->data;
445       }
446       else {
447         // The field map is built in two steps. First, add all base fields, by
448         // looping over all fieldable entity types. They always exist for all
449         // bundles, and we do not expect to have so many different entity
450         // types for this to become a bottleneck.
451         foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
452           if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
453             $bundles = array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id));
454             foreach ($this->getBaseFieldDefinitions($entity_type_id) as $field_name => $base_field_definition) {
455               $this->fieldMap[$entity_type_id][$field_name] = [
456                 'type' => $base_field_definition->getType(),
457                 'bundles' => array_combine($bundles, $bundles),
458               ];
459             }
460           }
461         }
462
463         // In the second step, the per-bundle fields are added, based on the
464         // persistent bundle field map stored in a key value collection. This
465         // data is managed in the EntityManager::onFieldDefinitionCreate()
466         // and EntityManager::onFieldDefinitionDelete() methods. Rebuilding this
467         // information in the same way as base fields would not scale, as the
468         // time to query would grow exponentially with more fields and bundles.
469         // A cache would be deleted during cache clears, which is the only time
470         // it is needed, so a key value collection is used.
471         $bundle_field_maps = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->getAll();
472         foreach ($bundle_field_maps as $entity_type_id => $bundle_field_map) {
473           foreach ($bundle_field_map as $field_name => $map_entry) {
474             if (!isset($this->fieldMap[$entity_type_id][$field_name])) {
475               $this->fieldMap[$entity_type_id][$field_name] = $map_entry;
476             }
477             else {
478               $this->fieldMap[$entity_type_id][$field_name]['bundles'] += $map_entry['bundles'];
479             }
480           }
481         }
482
483         $this->cacheSet($cid, $this->fieldMap, Cache::PERMANENT, ['entity_types']);
484       }
485     }
486     return $this->fieldMap;
487   }
488
489   /**
490    * {@inheritdoc}
491    */
492   public function getFieldMapByFieldType($field_type) {
493     if (!isset($this->fieldMapByFieldType[$field_type])) {
494       $filtered_map = [];
495       $map = $this->getFieldMap();
496       foreach ($map as $entity_type => $fields) {
497         foreach ($fields as $field_name => $field_info) {
498           if ($field_info['type'] == $field_type) {
499             $filtered_map[$entity_type][$field_name] = $field_info;
500           }
501         }
502       }
503       $this->fieldMapByFieldType[$field_type] = $filtered_map;
504     }
505     return $this->fieldMapByFieldType[$field_type];
506   }
507
508   /**
509    * Builds field storage definitions for an entity type.
510    *
511    * @param string $entity_type_id
512    *   The entity type ID. Only entity types that implement
513    *   \Drupal\Core\Entity\FieldableEntityInterface are supported
514    *
515    * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
516    *   An array of field storage definitions, keyed by field name.
517    */
518   protected function buildFieldStorageDefinitions($entity_type_id) {
519     $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
520     $field_definitions = [];
521
522     // Retrieve base field definitions from modules.
523     foreach ($this->moduleHandler->getImplementations('entity_field_storage_info') as $module) {
524       $module_definitions = $this->moduleHandler->invoke($module, 'entity_field_storage_info', [$entity_type]);
525       if (!empty($module_definitions)) {
526         // Ensure the provider key actually matches the name of the provider
527         // defining the field.
528         foreach ($module_definitions as $field_name => $definition) {
529           // @todo Remove this check once FieldDefinitionInterface exposes a
530           //  proper provider setter. See https://www.drupal.org/node/2225961.
531           if ($definition instanceof BaseFieldDefinition) {
532             $definition->setProvider($module);
533           }
534           $field_definitions[$field_name] = $definition;
535         }
536       }
537     }
538
539     // Invoke alter hook.
540     $this->moduleHandler->alter('entity_field_storage_info', $field_definitions, $entity_type);
541
542     return $field_definitions;
543   }
544
545   /**
546    * {@inheritdoc}
547    */
548   public function clearCachedFieldDefinitions() {
549     $this->baseFieldDefinitions = [];
550     $this->fieldDefinitions = [];
551     $this->fieldStorageDefinitions = [];
552     $this->fieldMap = [];
553     $this->fieldMapByFieldType = [];
554     $this->entityDisplayRepository->clearDisplayModeInfo();
555     $this->extraFields = [];
556     Cache::invalidateTags(['entity_field_info']);
557     // The typed data manager statically caches prototype objects with injected
558     // definitions, clear those as well.
559     $this->typedDataManager->clearCachedDefinitions();
560   }
561
562   /**
563    * {@inheritdoc}
564    */
565   public function useCaches($use_caches = FALSE) {
566     $this->useCaches = $use_caches;
567     if (!$use_caches) {
568       $this->fieldDefinitions = [];
569       $this->baseFieldDefinitions = [];
570       $this->fieldStorageDefinitions = [];
571     }
572   }
573
574   /**
575    * {@inheritdoc}
576    */
577   public function getExtraFields($entity_type_id, $bundle) {
578     // Read from the "static" cache.
579     if (isset($this->extraFields[$entity_type_id][$bundle])) {
580       return $this->extraFields[$entity_type_id][$bundle];
581     }
582
583     // Read from the persistent cache. Since hook_entity_extra_field_info() and
584     // hook_entity_extra_field_info_alter() might contain t() calls, we cache
585     // per language.
586     $cache_id = 'entity_bundle_extra_fields:' . $entity_type_id . ':' . $bundle . ':' . $this->languageManager->getCurrentLanguage()->getId();
587     $cached = $this->cacheGet($cache_id);
588     if ($cached) {
589       $this->extraFields[$entity_type_id][$bundle] = $cached->data;
590       return $this->extraFields[$entity_type_id][$bundle];
591     }
592
593     $extra = $this->moduleHandler->invokeAll('entity_extra_field_info');
594     $this->moduleHandler->alter('entity_extra_field_info', $extra);
595     $info = isset($extra[$entity_type_id][$bundle]) ? $extra[$entity_type_id][$bundle] : [];
596     $info += [
597       'form' => [],
598       'display' => [],
599     ];
600
601     // Store in the 'static' and persistent caches.
602     $this->extraFields[$entity_type_id][$bundle] = $info;
603     $this->cacheSet($cache_id, $info, Cache::PERMANENT, [
604       'entity_field_info',
605     ]);
606
607     return $this->extraFields[$entity_type_id][$bundle];
608   }
609
610 }