diff --git a/core/core.services.yml b/core/core.services.yml index 9bf8562461ecab532a5bf2e84c8d8e5599ac8d9c..a83af3460cc52fc724d6c5fc269fb1a3273b00af 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -496,10 +496,37 @@ services: arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state'] entity.manager: class: Drupal\Core\Entity\EntityManager - arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@language_manager', '@string_translation', '@class_resolver', '@typed_data_manager', '@keyvalue', '@event_dispatcher'] parent: container.trait + # @todo Remove this tag in https://www.drupal.org/node/2549143. tags: - { name: plugin_manager_cache_clear } + entity_type.manager: + class: Drupal\Core\Entity\EntityTypeManager + arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@string_translation', '@class_resolver'] + parent: container.trait + tags: + - { name: plugin_manager_cache_clear } + entity_type.repository: + class: Drupal\Core\Entity\EntityTypeRepository + arguments: ['@entity_type.manager'] + entity_type.bundle.info: + class: Drupal\Core\Entity\EntityTypeBundleInfo + arguments: ['@entity_type.manager', '@language_manager', '@module_handler', '@typed_data_manager', '@cache.discovery'] + entity.repository: + class: Drupal\Core\Entity\EntityRepository + arguments: ['@entity_type.manager', '@language_manager'] + entity_display.repository: + class: Drupal\Core\Entity\EntityDisplayRepository + arguments: ['@entity_type.manager', '@module_handler', '@cache.discovery', '@language_manager'] + entity_field.manager: + class: Drupal\Core\Entity\EntityFieldManager + arguments: ['@entity_type.manager', '@entity_type.bundle.info', '@entity_display.repository', '@typed_data_manager', '@language_manager', '@keyvalue', '@module_handler', '@cache.discovery'] + entity_type.listener: + class: Drupal\Core\Entity\EntityTypeListener + arguments: ['@entity_type.manager', '@entity_field.manager', '@event_dispatcher', '@entity.last_installed_schema.repository'] + entity_bundle.listener: + class: Drupal\Core\Entity\EntityBundleListener + arguments: ['@entity_type.manager', '@entity_type.bundle.info', '@entity_field.manager', '@module_handler'] entity_route_subscriber: class: Drupal\Core\EventSubscriber\EntityRouteProviderSubscriber arguments: ['@entity.manager'] @@ -508,6 +535,15 @@ services: entity.definition_update_manager: class: Drupal\Core\Entity\EntityDefinitionUpdateManager arguments: ['@entity.manager'] + entity.last_installed_schema.repository: + class: Drupal\Core\Entity\EntityLastInstalledSchemaRepository + arguments: ['@keyvalue'] + field_storage_definition.listener: + class: Drupal\Core\Field\FieldStorageDefinitionListener + arguments: ['@entity_type.manager', '@event_dispatcher', '@entity.last_installed_schema.repository', '@entity_field.manager'] + field_definition.listener: + class: Drupal\Core\Field\FieldDefinitionListener + arguments: ['@entity_type.manager', '@entity_field.manager', '@keyvalue', '@cache.discovery'] entity.form_builder: class: Drupal\Core\Entity\EntityFormBuilder arguments: ['@entity.manager', '@form_builder'] diff --git a/core/lib/Drupal/Core/Cache/UseCacheBackendTrait.php b/core/lib/Drupal/Core/Cache/UseCacheBackendTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..98742d82a00ab011d21f6d637b8715ee029b39ca --- /dev/null +++ b/core/lib/Drupal/Core/Cache/UseCacheBackendTrait.php @@ -0,0 +1,80 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Cache\UseCacheBackendTrait. + */ + +namespace Drupal\Core\Cache; + +/** + * Provides methods to use a cache backend while respecting a 'use caches' flag. + */ +trait UseCacheBackendTrait { + + /** + * Cache backend instance. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cacheBackend; + + /** + * Flag whether caches should be used or skipped. + * + * @var bool + */ + protected $useCaches = TRUE; + + /** + * Fetches from the cache backend, respecting the use caches flag. + * + * @param string $cid + * The cache ID of the data to retrieve. + * + * @return object|false + * The cache item or FALSE on failure. + * + * @see \Drupal\Core\Cache\CacheBackendInterface::get() + */ + protected function cacheGet($cid) { + if ($this->useCaches && $this->cacheBackend) { + return $this->cacheBackend->get($cid); + } + return FALSE; + } + + /** + * Stores data in the persistent cache, respecting the use caches flag. + * + * @param string $cid + * The cache ID of the data to store. + * @param mixed $data + * The data to store in the cache. + * Some storage engines only allow objects up to a maximum of 1MB in size to + * be stored by default. When caching large arrays or similar, take care to + * ensure $data does not exceed this size. + * @param int $expire + * One of the following values: + * - CacheBackendInterface::CACHE_PERMANENT: Indicates that the item should + * not be removed unless it is deleted explicitly. + * - A Unix timestamp: Indicates that the item will be considered invalid + * after this time, i.e. it will not be returned by get() unless + * $allow_invalid has been set to TRUE. When the item has expired, it may + * be permanently deleted by the garbage collector at any time. + * @param array $tags + * An array of tags to be stored with the cache item. These should normally + * identify objects used to build the cache item, which should trigger + * cache invalidation when updated. For example if a cached item represents + * a node, both the node ID and the author's user ID might be passed in as + * tags. For example array('node' => array(123), 'user' => array(92)). + * + * @see \Drupal\Core\Cache\CacheBackendInterface::set() + */ + protected function cacheSet($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { + if ($this->cacheBackend && $this->useCaches) { + $this->cacheBackend->set($cid, $data, $expire, $tags); + } + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityBundleListener.php b/core/lib/Drupal/Core/Entity/EntityBundleListener.php new file mode 100644 index 0000000000000000000000000000000000000000..52d9f696f66a7c1458330bb4b8a21dc8ae9f85f9 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityBundleListener.php @@ -0,0 +1,93 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityBundleListener. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Core\Extension\ModuleHandlerInterface; + +/** + * Reacts to entity bundle CRUD on behalf of the Entity system. + */ +class EntityBundleListener implements EntityBundleListenerInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The entity type bundle info. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $entityTypeBundleInfo; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * Constructs a new EntityBundleListener. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle info. + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager + * The entity field manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityFieldManagerInterface $entity_field_manager, ModuleHandlerInterface $module_handler) { + $this->entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + $this->entityFieldManager = $entity_field_manager; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function onBundleCreate($bundle, $entity_type_id) { + $this->entityTypeBundleInfo->clearCachedBundles(); + // Notify the entity storage. + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if ($storage instanceof EntityBundleListenerInterface) { + $storage->onBundleCreate($bundle, $entity_type_id); + } + // Invoke hook_entity_bundle_create() hook. + $this->moduleHandler->invokeAll('entity_bundle_create', [$entity_type_id, $bundle]); + } + + /** + * {@inheritdoc} + */ + public function onBundleDelete($bundle, $entity_type_id) { + $this->entityTypeBundleInfo->clearCachedBundles(); + // Notify the entity storage. + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if ($storage instanceof EntityBundleListenerInterface) { + $storage->onBundleDelete($bundle, $entity_type_id); + } + // Invoke hook_entity_bundle_delete() hook. + $this->moduleHandler->invokeAll('entity_bundle_delete', [$entity_type_id, $bundle]); + $this->entityFieldManager->clearCachedFieldDefinitions(); + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayRepository.php b/core/lib/Drupal/Core/Entity/EntityDisplayRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..4c9bb8c81168e435d24c3d2ccf9c5882b6dd3d31 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityDisplayRepository.php @@ -0,0 +1,251 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityDisplayRepository. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Cache\UseCacheBackendTrait; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Provides a repository for entity display objects (view modes and form modes). + */ +class EntityDisplayRepository implements EntityDisplayRepositoryInterface { + + use UseCacheBackendTrait; + use StringTranslationTrait; + + /** + * Static cache of display modes information. + * + * @var array + */ + protected $displayModeInfo = []; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * Constructs a new EntityDisplayRepository. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager) { + $this->entityTypeManager = $entity_type_manager; + $this->moduleHandler = $module_handler; + $this->cacheBackend = $cache_backend; + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function getAllViewModes() { + return $this->getAllDisplayModesByEntityType('view_mode'); + } + + /** + * {@inheritdoc} + */ + public function getViewModes($entity_type_id) { + return $this->getDisplayModesByEntityType('view_mode', $entity_type_id); + } + + /** + * {@inheritdoc} + */ + public function getAllFormModes() { + return $this->getAllDisplayModesByEntityType('form_mode'); + } + + /** + * {@inheritdoc} + */ + public function getFormModes($entity_type_id) { + return $this->getDisplayModesByEntityType('form_mode', $entity_type_id); + } + + /** + * Gets the entity display mode info for all entity types. + * + * @param string $display_type + * The display type to be retrieved. It can be "view_mode" or "form_mode". + * + * @return array + * The display mode info for all entity types. + */ + protected function getAllDisplayModesByEntityType($display_type) { + if (!isset($this->displayModeInfo[$display_type])) { + $key = 'entity_' . $display_type . '_info'; + $entity_type_id = 'entity_' . $display_type; + $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_INTERFACE)->getId(); + if ($cache = $this->cacheGet("$key:$langcode")) { + $this->displayModeInfo[$display_type] = $cache->data; + } + else { + $this->displayModeInfo[$display_type] = []; + foreach ($this->entityTypeManager->getStorage($entity_type_id)->loadMultiple() as $display_mode) { + list($display_mode_entity_type, $display_mode_name) = explode('.', $display_mode->id(), 2); + $this->displayModeInfo[$display_type][$display_mode_entity_type][$display_mode_name] = $display_mode->toArray(); + } + $this->moduleHandler->alter($key, $this->displayModeInfo[$display_type]); + $this->cacheSet("$key:$langcode", $this->displayModeInfo[$display_type], CacheBackendInterface::CACHE_PERMANENT, ['entity_types', 'entity_field_info']); + } + } + + return $this->displayModeInfo[$display_type]; + } + + /** + * Gets the entity display mode info for a specific entity type. + * + * @param string $display_type + * The display type to be retrieved. It can be "view_mode" or "form_mode". + * @param string $entity_type_id + * The entity type whose display mode info should be returned. + * + * @return array + * The display mode info for a specific entity type. + */ + protected function getDisplayModesByEntityType($display_type, $entity_type_id) { + if (isset($this->displayModeInfo[$display_type][$entity_type_id])) { + return $this->displayModeInfo[$display_type][$entity_type_id]; + } + else { + $display_modes = $this->getAllDisplayModesByEntityType($display_type); + if (isset($display_modes[$entity_type_id])) { + return $display_modes[$entity_type_id]; + } + } + return []; + } + + /** + * {@inheritdoc} + */ + public function getViewModeOptions($entity_type) { + return $this->getDisplayModeOptions('view_mode', $entity_type); + } + + /** + * {@inheritdoc} + */ + public function getFormModeOptions($entity_type_id) { + return $this->getDisplayModeOptions('form_mode', $entity_type_id); + } + + /** + * {@inheritdoc} + */ + public function getViewModeOptionsByBundle($entity_type_id, $bundle) { + return $this->getDisplayModeOptionsByBundle('view_mode', $entity_type_id, $bundle); + } + + /** + * {@inheritdoc} + */ + public function getFormModeOptionsByBundle($entity_type_id, $bundle) { + return $this->getDisplayModeOptionsByBundle('form_mode', $entity_type_id, $bundle); + } + + /** + * Gets an array of display mode options. + * + * @param string $display_type + * The display type to be retrieved. It can be "view_mode" or "form_mode". + * @param string $entity_type_id + * The entity type whose display mode options should be returned. + * + * @return array + * An array of display mode labels, keyed by the display mode ID. + */ + protected function getDisplayModeOptions($display_type, $entity_type_id) { + $options = array('default' => t('Default')); + foreach ($this->getDisplayModesByEntityType($display_type, $entity_type_id) as $mode => $settings) { + $options[$mode] = $settings['label']; + } + return $options; + } + + /** + * Returns an array of enabled display mode options by bundle. + * + * @param $display_type + * The display type to be retrieved. It can be "view_mode" or "form_mode". + * @param string $entity_type_id + * The entity type whose display mode options should be returned. + * @param string $bundle + * The name of the bundle. + * + * @return array + * An array of display mode labels, keyed by the display mode ID. + */ + protected function getDisplayModeOptionsByBundle($display_type, $entity_type_id, $bundle) { + // Collect all the entity's display modes. + $options = $this->getDisplayModeOptions($display_type, $entity_type_id); + + // Filter out modes for which the entity display is disabled + // (or non-existent). + $load_ids = array(); + // Get the list of available entity displays for the current bundle. + foreach (array_keys($options) as $mode) { + $load_ids[] = $entity_type_id . '.' . $bundle . '.' . $mode; + } + + // Load the corresponding displays. + $displays = $this->entityTypeManager + ->getStorage($display_type == 'form_mode' ? 'entity_form_display' : 'entity_view_display') + ->loadMultiple($load_ids); + + // Unset the display modes that are not active or do not exist. + foreach (array_keys($options) as $mode) { + $display_id = $entity_type_id . '.' . $bundle . '.' . $mode; + if (!isset($displays[$display_id]) || !$displays[$display_id]->status()) { + unset($options[$mode]); + } + } + + return $options; + } + + /** + * {@inheritdoc} + */ + public function clearDisplayModeInfo() { + $this->displayModeInfo = []; + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayRepositoryInterface.php b/core/lib/Drupal/Core/Entity/EntityDisplayRepositoryInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..358ab789b82079dd30f87eecb7b1f5b5c42d4ba3 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityDisplayRepositoryInterface.php @@ -0,0 +1,108 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityDisplayRepositoryInterface. + */ + +namespace Drupal\Core\Entity; + +/** + * Provides an interface for an entity display repository. + */ +interface EntityDisplayRepositoryInterface { + + /** + * Gets the entity view mode info for all entity types. + * + * @return array + * The view mode info for all entity types. + */ + public function getAllViewModes(); + + /** + * Gets the entity view mode info for a specific entity type. + * + * @param string $entity_type_id + * The entity type whose view mode info should be returned. + * + * @return array + * The view mode info for a specific entity type. + */ + public function getViewModes($entity_type_id); + + /** + * Gets the entity form mode info for all entity types. + * + * @return array + * The form mode info for all entity types. + */ + public function getAllFormModes(); + + /** + * Gets the entity form mode info for a specific entity type. + * + * @param string $entity_type_id + * The entity type whose form mode info should be returned. + * + * @return array + * The form mode info for a specific entity type. + */ + public function getFormModes($entity_type_id); + + /** + * Gets an array of view mode options. + * + * @param string $entity_type_id + * The entity type whose view mode options should be returned. + * + * @return array + * An array of view mode labels, keyed by the display mode ID. + */ + public function getViewModeOptions($entity_type_id); + + /** + * Gets an array of form mode options. + * + * @param string $entity_type_id + * The entity type whose form mode options should be returned. + * + * @return array + * An array of form mode labels, keyed by the display mode ID. + */ + public function getFormModeOptions($entity_type_id); + + /** + * Returns an array of enabled view mode options by bundle. + * + * @param string $entity_type_id + * The entity type whose view mode options should be returned. + * @param string $bundle + * The name of the bundle. + * + * @return array + * An array of view mode labels, keyed by the display mode ID. + */ + public function getViewModeOptionsByBundle($entity_type_id, $bundle); + + /** + * Returns an array of enabled form mode options by bundle. + * + * @param string $entity_type_id + * The entity type whose form mode options should be returned. + * @param string $bundle + * The name of the bundle. + * + * @return array + * An array of form mode labels, keyed by the display mode ID. + */ + public function getFormModeOptionsByBundle($entity_type_id, $bundle); + + /** + * Clears the gathered display mode info. + * + * @return $this + */ + public function clearDisplayModeInfo(); + +} diff --git a/core/lib/Drupal/Core/Entity/EntityFieldManager.php b/core/lib/Drupal/Core/Entity/EntityFieldManager.php new file mode 100644 index 0000000000000000000000000000000000000000..065a750283a4196b85ba742f0f67242190036877 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityFieldManager.php @@ -0,0 +1,601 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityFieldManager. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Cache\UseCacheBackendTrait; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\TypedData\TypedDataManagerInterface; + +/** + * Manages the discovery of entity fields. + * + * This includes field definitions, base field definitions, and field storage + * definitions. + */ +class EntityFieldManager implements EntityFieldManagerInterface { + + use UseCacheBackendTrait; + use StringTranslationTrait; + + /** + * Extra fields by bundle. + * + * @var array + */ + protected $extraFields = []; + + /** + * Static cache of base field definitions. + * + * @var array + */ + protected $baseFieldDefinitions; + + /** + * Static cache of field definitions per bundle and entity type. + * + * @var array + */ + protected $fieldDefinitions; + + /** + * Static cache of field storage definitions per entity type. + * + * Elements of the array: + * - $entity_type_id: \Drupal\Core\Field\BaseFieldDefinition[] + * + * @var array + */ + protected $fieldStorageDefinitions; + + /** + * An array keyed by entity type. Each value is an array whose keys are + * field names and whose value is an array with two entries: + * - type: The field type. + * - bundles: The bundles in which the field appears. + * + * @return array + */ + protected $fieldMap = []; + + /** + * An array keyed by field type. Each value is an array whose key are entity + * types including arrays in the same form that $fieldMap. + * + * It helps access the mapping between types and fields by the field type. + * + * @var array + */ + protected $fieldMapByFieldType = []; + + /** + * The typed data manager. + * + * @var \Drupal\Core\TypedData\TypedDataManagerInterface + */ + protected $typedDataManager; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * The key-value factory. + * + * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface + */ + protected $keyValueFactory; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The entity type bundle info. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $entityTypeBundleInfo; + + /** + * The entity display repository. + * + * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface + */ + protected $entityDisplayRepository; + + /** + * Constructs a new EntityFieldManager. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle info. + * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository + * The entity display repository. + * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager + * The typed data manager. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory + * The key-value factory. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend. + */ + 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) { + $this->entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + $this->entityDisplayRepository = $entity_display_repository; + + $this->typedDataManager = $typed_data_manager; + $this->languageManager = $language_manager; + $this->keyValueFactory = $key_value_factory; + $this->moduleHandler = $module_handler; + $this->cacheBackend = $cache_backend; + } + + /** + * {@inheritdoc} + */ + public function getBaseFieldDefinitions($entity_type_id) { + // Check the static cache. + if (!isset($this->baseFieldDefinitions[$entity_type_id])) { + // Not prepared, try to load from cache. + $cid = 'entity_base_field_definitions:' . $entity_type_id . ':' . $this->languageManager->getCurrentLanguage()->getId(); + if ($cache = $this->cacheGet($cid)) { + $this->baseFieldDefinitions[$entity_type_id] = $cache->data; + } + else { + // Rebuild the definitions and put it into the cache. + $this->baseFieldDefinitions[$entity_type_id] = $this->buildBaseFieldDefinitions($entity_type_id); + $this->cacheSet($cid, $this->baseFieldDefinitions[$entity_type_id], Cache::PERMANENT, ['entity_types', 'entity_field_info']); + } + } + return $this->baseFieldDefinitions[$entity_type_id]; + } + + /** + * Builds base field definitions for an entity type. + * + * @param string $entity_type_id + * The entity type ID. Only entity types that implement + * \Drupal\Core\Entity\FieldableEntityInterface are supported. + * + * @return \Drupal\Core\Field\FieldDefinitionInterface[] + * An array of field definitions, keyed by field name. + * + * @throws \LogicException + * Thrown if a config entity type is given or if one of the entity keys is + * flagged as translatable. + */ + protected function buildBaseFieldDefinitions($entity_type_id) { + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + $class = $entity_type->getClass(); + $keys = array_filter($entity_type->getKeys()); + + // Fail with an exception for non-fieldable entity types. + if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) { + throw new \LogicException("Getting the base fields is not supported for entity type {$entity_type->getLabel()}."); + } + + // Retrieve base field definitions. + /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $base_field_definitions */ + $base_field_definitions = $class::baseFieldDefinitions($entity_type); + + // Make sure translatable entity types are correctly defined. + if ($entity_type->isTranslatable()) { + // The langcode field should always be translatable if the entity type is. + if (isset($keys['langcode']) && isset($base_field_definitions[$keys['langcode']])) { + $base_field_definitions[$keys['langcode']]->setTranslatable(TRUE); + } + // A default_langcode field should always be defined. + if (!isset($base_field_definitions[$keys['default_langcode']])) { + $base_field_definitions[$keys['default_langcode']] = BaseFieldDefinition::create('boolean') + ->setLabel($this->t('Default translation')) + ->setDescription($this->t('A flag indicating whether this is the default translation.')) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE) + ->setDefaultValue(TRUE); + } + } + + // Assign base field definitions the entity type provider. + $provider = $entity_type->getProvider(); + foreach ($base_field_definitions as $definition) { + // @todo Remove this check once FieldDefinitionInterface exposes a proper + // provider setter. See https://www.drupal.org/node/2225961. + if ($definition instanceof BaseFieldDefinition) { + $definition->setProvider($provider); + } + } + + // Retrieve base field definitions from modules. + foreach ($this->moduleHandler->getImplementations('entity_base_field_info') as $module) { + $module_definitions = $this->moduleHandler->invoke($module, 'entity_base_field_info', [$entity_type]); + if (!empty($module_definitions)) { + // Ensure the provider key actually matches the name of the provider + // defining the field. + foreach ($module_definitions as $field_name => $definition) { + // @todo Remove this check once FieldDefinitionInterface exposes a + // proper provider setter. See https://www.drupal.org/node/2225961. + if ($definition instanceof BaseFieldDefinition && $definition->getProvider() == NULL) { + $definition->setProvider($module); + } + $base_field_definitions[$field_name] = $definition; + } + } + } + + // Automatically set the field name, target entity type and bundle + // for non-configurable fields. + foreach ($base_field_definitions as $field_name => $base_field_definition) { + if ($base_field_definition instanceof BaseFieldDefinition) { + $base_field_definition->setName($field_name); + $base_field_definition->setTargetEntityTypeId($entity_type_id); + $base_field_definition->setTargetBundle(NULL); + } + } + + // Invoke alter hook. + $this->moduleHandler->alter('entity_base_field_info', $base_field_definitions, $entity_type); + + // Ensure defined entity keys are there and have proper revisionable and + // translatable values. + foreach (array_intersect_key($keys, array_flip(['id', 'revision', 'uuid', 'bundle'])) as $key => $field_name) { + if (!isset($base_field_definitions[$field_name])) { + throw new \LogicException("The $field_name field definition does not exist and it is used as $key entity key."); + } + if ($base_field_definitions[$field_name]->isRevisionable()) { + throw new \LogicException("The {$base_field_definitions[$field_name]->getLabel()} field cannot be revisionable as it is used as $key entity key."); + } + if ($base_field_definitions[$field_name]->isTranslatable()) { + throw new \LogicException("The {$base_field_definitions[$field_name]->getLabel()} field cannot be translatable as it is used as $key entity key."); + } + } + + // Make sure translatable entity types define the "langcode" field properly. + if ($entity_type->isTranslatable() && (!isset($keys['langcode']) || !isset($base_field_definitions[$keys['langcode']]) || !$base_field_definitions[$keys['langcode']]->isTranslatable())) { + throw new \LogicException("The {$entity_type->getLabel()} entity type cannot be translatable as it does not define a translatable \"langcode\" field."); + } + + return $base_field_definitions; + } + + /** + * {@inheritdoc} + */ + public function getFieldDefinitions($entity_type_id, $bundle) { + if (!isset($this->fieldDefinitions[$entity_type_id][$bundle])) { + $base_field_definitions = $this->getBaseFieldDefinitions($entity_type_id); + // Not prepared, try to load from cache. + $cid = 'entity_bundle_field_definitions:' . $entity_type_id . ':' . $bundle . ':' . $this->languageManager->getCurrentLanguage()->getId(); + if ($cache = $this->cacheGet($cid)) { + $bundle_field_definitions = $cache->data; + } + else { + // Rebuild the definitions and put it into the cache. + $bundle_field_definitions = $this->buildBundleFieldDefinitions($entity_type_id, $bundle, $base_field_definitions); + $this->cacheSet($cid, $bundle_field_definitions, Cache::PERMANENT, ['entity_types', 'entity_field_info']); + } + // Field definitions consist of the bundle specific overrides and the + // base fields, merge them together. Use array_replace() to replace base + // fields with by bundle overrides and keep them in order, append + // additional by bundle fields. + $this->fieldDefinitions[$entity_type_id][$bundle] = array_replace($base_field_definitions, $bundle_field_definitions); + } + return $this->fieldDefinitions[$entity_type_id][$bundle]; + } + + /** + * Builds field definitions for a specific bundle within an entity type. + * + * @param string $entity_type_id + * The entity type ID. Only entity types that implement + * \Drupal\Core\Entity\FieldableEntityInterface are supported. + * @param string $bundle + * The bundle. + * @param \Drupal\Core\Field\FieldDefinitionInterface[] $base_field_definitions + * The list of base field definitions. + * + * @return \Drupal\Core\Field\FieldDefinitionInterface[] + * An array of bundle field definitions, keyed by field name. Does + * not include base fields. + */ + protected function buildBundleFieldDefinitions($entity_type_id, $bundle, array $base_field_definitions) { + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + $class = $entity_type->getClass(); + + // Allow the entity class to provide bundle fields and bundle-specific + // overrides of base fields. + $bundle_field_definitions = $class::bundleFieldDefinitions($entity_type, $bundle, $base_field_definitions); + + // Load base field overrides from configuration. These take precedence over + // base field overrides returned above. + $base_field_override_ids = array_map(function($field_name) use ($entity_type_id, $bundle) { + return $entity_type_id . '.' . $bundle . '.' . $field_name; + }, array_keys($base_field_definitions)); + $base_field_overrides = $this->entityTypeManager->getStorage('base_field_override')->loadMultiple($base_field_override_ids); + foreach ($base_field_overrides as $base_field_override) { + /** @var \Drupal\Core\Field\Entity\BaseFieldOverride $base_field_override */ + $field_name = $base_field_override->getName(); + $bundle_field_definitions[$field_name] = $base_field_override; + } + + $provider = $entity_type->getProvider(); + foreach ($bundle_field_definitions as $definition) { + // @todo Remove this check once FieldDefinitionInterface exposes a proper + // provider setter. See https://www.drupal.org/node/2225961. + if ($definition instanceof BaseFieldDefinition) { + $definition->setProvider($provider); + } + } + + // Retrieve base field definitions from modules. + foreach ($this->moduleHandler->getImplementations('entity_bundle_field_info') as $module) { + $module_definitions = $this->moduleHandler->invoke($module, 'entity_bundle_field_info', [$entity_type, $bundle, $base_field_definitions]); + if (!empty($module_definitions)) { + // Ensure the provider key actually matches the name of the provider + // defining the field. + foreach ($module_definitions as $field_name => $definition) { + // @todo Remove this check once FieldDefinitionInterface exposes a + // proper provider setter. See https://www.drupal.org/node/2225961. + if ($definition instanceof BaseFieldDefinition) { + $definition->setProvider($module); + } + $bundle_field_definitions[$field_name] = $definition; + } + } + } + + // Automatically set the field name, target entity type and bundle + // for non-configurable fields. + foreach ($bundle_field_definitions as $field_name => $field_definition) { + if ($field_definition instanceof BaseFieldDefinition) { + $field_definition->setName($field_name); + $field_definition->setTargetEntityTypeId($entity_type_id); + $field_definition->setTargetBundle($bundle); + } + } + + // Invoke 'per bundle' alter hook. + $this->moduleHandler->alter('entity_bundle_field_info', $bundle_field_definitions, $entity_type, $bundle); + + return $bundle_field_definitions; + } + + /** + * {@inheritdoc} + */ + public function getFieldStorageDefinitions($entity_type_id) { + if (!isset($this->fieldStorageDefinitions[$entity_type_id])) { + $this->fieldStorageDefinitions[$entity_type_id] = []; + // Add all non-computed base fields. + foreach ($this->getBaseFieldDefinitions($entity_type_id) as $field_name => $definition) { + if (!$definition->isComputed()) { + $this->fieldStorageDefinitions[$entity_type_id][$field_name] = $definition; + } + } + // Not prepared, try to load from cache. + $cid = 'entity_field_storage_definitions:' . $entity_type_id . ':' . $this->languageManager->getCurrentLanguage()->getId(); + if ($cache = $this->cacheGet($cid)) { + $field_storage_definitions = $cache->data; + } + else { + // Rebuild the definitions and put it into the cache. + $field_storage_definitions = $this->buildFieldStorageDefinitions($entity_type_id); + $this->cacheSet($cid, $field_storage_definitions, Cache::PERMANENT, ['entity_types', 'entity_field_info']); + } + $this->fieldStorageDefinitions[$entity_type_id] += $field_storage_definitions; + } + return $this->fieldStorageDefinitions[$entity_type_id]; + } + + /** + * {@inheritdoc} + */ + public function setFieldMap(array $field_map) { + $this->fieldMap = $field_map; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFieldMap() { + if (!$this->fieldMap) { + // Not prepared, try to load from cache. + $cid = 'entity_field_map'; + if ($cache = $this->cacheGet($cid)) { + $this->fieldMap = $cache->data; + } + else { + // The field map is built in two steps. First, add all base fields, by + // looping over all fieldable entity types. They always exist for all + // bundles, and we do not expect to have so many different entity + // types for this to become a bottleneck. + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + if ($entity_type->isSubclassOf(FieldableEntityInterface::class)) { + $bundles = array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id)); + foreach ($this->getBaseFieldDefinitions($entity_type_id) as $field_name => $base_field_definition) { + $this->fieldMap[$entity_type_id][$field_name] = [ + 'type' => $base_field_definition->getType(), + 'bundles' => array_combine($bundles, $bundles), + ]; + } + } + } + + // In the second step, the per-bundle fields are added, based on the + // persistent bundle field map stored in a key value collection. This + // data is managed in the EntityManager::onFieldDefinitionCreate() + // and EntityManager::onFieldDefinitionDelete() methods. Rebuilding this + // information in the same way as base fields would not scale, as the + // time to query would grow exponentially with more fields and bundles. + // A cache would be deleted during cache clears, which is the only time + // it is needed, so a key value collection is used. + $bundle_field_maps = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->getAll(); + foreach ($bundle_field_maps as $entity_type_id => $bundle_field_map) { + foreach ($bundle_field_map as $field_name => $map_entry) { + if (!isset($this->fieldMap[$entity_type_id][$field_name])) { + $this->fieldMap[$entity_type_id][$field_name] = $map_entry; + } + else { + $this->fieldMap[$entity_type_id][$field_name]['bundles'] += $map_entry['bundles']; + } + } + } + + $this->cacheSet($cid, $this->fieldMap, Cache::PERMANENT, ['entity_types']); + } + } + return $this->fieldMap; + } + + /** + * {@inheritdoc} + */ + public function getFieldMapByFieldType($field_type) { + if (!isset($this->fieldMapByFieldType[$field_type])) { + $filtered_map = []; + $map = $this->getFieldMap(); + foreach ($map as $entity_type => $fields) { + foreach ($fields as $field_name => $field_info) { + if ($field_info['type'] == $field_type) { + $filtered_map[$entity_type][$field_name] = $field_info; + } + } + } + $this->fieldMapByFieldType[$field_type] = $filtered_map; + } + return $this->fieldMapByFieldType[$field_type]; + } + + /** + * Builds field storage definitions for an entity type. + * + * @param string $entity_type_id + * The entity type ID. Only entity types that implement + * \Drupal\Core\Entity\FieldableEntityInterface are supported + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] + * An array of field storage definitions, keyed by field name. + */ + protected function buildFieldStorageDefinitions($entity_type_id) { + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + $field_definitions = []; + + // Retrieve base field definitions from modules. + foreach ($this->moduleHandler->getImplementations('entity_field_storage_info') as $module) { + $module_definitions = $this->moduleHandler->invoke($module, 'entity_field_storage_info', [$entity_type]); + if (!empty($module_definitions)) { + // Ensure the provider key actually matches the name of the provider + // defining the field. + foreach ($module_definitions as $field_name => $definition) { + // @todo Remove this check once FieldDefinitionInterface exposes a + // proper provider setter. See https://www.drupal.org/node/2225961. + if ($definition instanceof BaseFieldDefinition) { + $definition->setProvider($module); + } + $field_definitions[$field_name] = $definition; + } + } + } + + // Invoke alter hook. + $this->moduleHandler->alter('entity_field_storage_info', $field_definitions, $entity_type); + + return $field_definitions; + } + + /** + * {@inheritdoc} + */ + public function clearCachedFieldDefinitions() { + $this->baseFieldDefinitions = []; + $this->fieldDefinitions = []; + $this->fieldStorageDefinitions = []; + $this->fieldMap = []; + $this->fieldMapByFieldType = []; + $this->entityDisplayRepository->clearDisplayModeInfo(); + $this->extraFields = []; + Cache::invalidateTags(['entity_field_info']); + // The typed data manager statically caches prototype objects with injected + // definitions, clear those as well. + $this->typedDataManager->clearCachedDefinitions(); + } + + /** + * {@inheritdoc} + */ + public function useCaches($use_caches = FALSE) { + $this->useCaches = $use_caches; + if (!$use_caches) { + $this->fieldDefinitions = []; + $this->baseFieldDefinitions = []; + $this->fieldStorageDefinitions = []; + } + } + + /** + * {@inheritdoc} + */ + public function getExtraFields($entity_type_id, $bundle) { + // Read from the "static" cache. + if (isset($this->extraFields[$entity_type_id][$bundle])) { + return $this->extraFields[$entity_type_id][$bundle]; + } + + // Read from the persistent cache. Since hook_entity_extra_field_info() and + // hook_entity_extra_field_info_alter() might contain t() calls, we cache + // per language. + $cache_id = 'entity_bundle_extra_fields:' . $entity_type_id . ':' . $bundle . ':' . $this->languageManager->getCurrentLanguage()->getId(); + $cached = $this->cacheGet($cache_id); + if ($cached) { + $this->extraFields[$entity_type_id][$bundle] = $cached->data; + return $this->extraFields[$entity_type_id][$bundle]; + } + + $extra = $this->moduleHandler->invokeAll('entity_extra_field_info'); + $this->moduleHandler->alter('entity_extra_field_info', $extra); + $info = isset($extra[$entity_type_id][$bundle]) ? $extra[$entity_type_id][$bundle] : []; + $info += [ + 'form' => [], + 'display' => [], + ]; + + // Store in the 'static' and persistent caches. + $this->extraFields[$entity_type_id][$bundle] = $info; + $this->cacheSet($cache_id, $info, Cache::PERMANENT, [ + 'entity_field_info', + ]); + + return $this->extraFields[$entity_type_id][$bundle]; + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityFieldManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityFieldManagerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..e6a228cff5cf009bff64d4660bacdec578b13449 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityFieldManagerInterface.php @@ -0,0 +1,150 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityFieldManagerInterface. + */ + +namespace Drupal\Core\Entity; + +/** + * Provides an interface for an entity field manager. + */ +interface EntityFieldManagerInterface { + + /** + * Gets the base field definitions for a content entity type. + * + * Only fields that are not specific to a given bundle or set of bundles are + * returned. This excludes configurable fields, as they are always attached + * to a specific bundle. + * + * @param string $entity_type_id + * The entity type ID. Only entity types that implement + * \Drupal\Core\Entity\FieldableEntityInterface are supported. + * + * @return \Drupal\Core\Field\FieldDefinitionInterface[] + * The array of base field definitions for the entity type, keyed by field + * name. + * + * @throws \LogicException + * Thrown if one of the entity keys is flagged as translatable. + */ + public function getBaseFieldDefinitions($entity_type_id); + + /** + * Gets the field definitions for a specific bundle. + * + * @param string $entity_type_id + * The entity type ID. Only entity types that implement + * \Drupal\Core\Entity\FieldableEntityInterface are supported. + * @param string $bundle + * The bundle. + * + * @return \Drupal\Core\Field\FieldDefinitionInterface[] + * The array of field definitions for the bundle, keyed by field name. + */ + public function getFieldDefinitions($entity_type_id, $bundle); + + /** + * Gets the field storage definitions for a content entity type. + * + * This returns all field storage definitions for base fields and bundle + * fields of an entity type. Note that field storage definitions of a base + * field equal the full base field definition (i.e. they implement + * FieldDefinitionInterface), while the storage definitions for bundle fields + * may implement FieldStorageDefinitionInterface only. + * + * @param string $entity_type_id + * The entity type ID. Only content entities are supported. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] + * The array of field storage definitions for the entity type, keyed by + * field name. + * + * @see \Drupal\Core\Field\FieldStorageDefinitionInterface + */ + public function getFieldStorageDefinitions($entity_type_id); + + /** + * Gets a lightweight map of fields across bundles. + * + * @return array + * An array keyed by entity type. Each value is an array which keys are + * field names and value is an array with two entries: + * - type: The field type. + * - bundles: The bundles in which the field appears. + */ + public function getFieldMap(); + + /** + * Sets a lightweight map of fields across bundles. + * + * @param array[] $field_map + * See the return value of self::getFieldMap(). + * + * @return $this + */ + public function setFieldMap(array $field_map); + + /** + * Gets a lightweight map of fields across bundles filtered by field type. + * + * @param string $field_type + * The field type to filter by. + * + * @return array + * An array keyed by entity type. Each value is an array which keys are + * field names and value is an array with two entries: + * - type: The field type. + * - bundles: The bundles in which the field appears. + */ + public function getFieldMapByFieldType($field_type); + + /** + * Clears static and persistent field definition caches. + */ + public function clearCachedFieldDefinitions(); + + /** + * Disable the use of caches. + * + * @param bool $use_caches + * FALSE to not use any caches. + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. + * + * @todo Remove in https://www.drupal.org/node/2549143. + */ + public function useCaches($use_caches = FALSE); + + /** + * Gets the "extra fields" for a bundle. + * + * @param string $entity_type_id + * The entity type ID. + * @param string $bundle + * The bundle name. + * + * @return array + * A nested array of 'pseudo-field' elements. Each list is nested within the + * following keys: entity type, bundle name, context (either 'form' or + * 'display'). The keys are the name of the elements as appearing in the + * renderable array (either the entity form or the displayed entity). The + * value is an associative array: + * - label: The human readable name of the element. Make sure you sanitize + * this appropriately. + * - description: A short description of the element contents. + * - weight: The default weight of the element. + * - visible: (optional) The default visibility of the element. Defaults to + * TRUE. + * - edit: (optional) String containing markup (normally a link) used as the + * element's 'edit' operation in the administration interface. Only for + * 'form' context. + * - delete: (optional) String containing markup (normally a link) used as the + * element's 'delete' operation in the administration interface. Only for + * 'form' context. + */ + public function getExtraFields($entity_type_id, $bundle); + +} diff --git a/core/lib/Drupal/Core/Entity/EntityForm.php b/core/lib/Drupal/Core/Entity/EntityForm.php index 5450e25fc3c43511fe0266a5d40285539f8a0079..8d919485272fe7779ab23f1d98983b77808a3e2f 100644 --- a/core/lib/Drupal/Core/Entity/EntityForm.php +++ b/core/lib/Drupal/Core/Entity/EntityForm.php @@ -41,9 +41,18 @@ class EntityForm extends FormBase implements EntityFormInterface { * The entity manager. * * @var \Drupal\Core\Entity\EntityManagerInterface + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ protected $entityManager; + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + /** * The entity being used by this form. * @@ -404,4 +413,12 @@ public function setEntityManager(EntityManagerInterface $entity_manager) { return $this; } + /** + * {@inheritdoc} + */ + public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + return $this; + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityFormInterface.php b/core/lib/Drupal/Core/Entity/EntityFormInterface.php index 8ae1a9142b9826e97d177ff3a0cdb305432cf24d..9946a3d76b45b714472460fdc365ef37965bc904 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityFormInterface.php @@ -137,7 +137,21 @@ public function setModuleHandler(ModuleHandlerInterface $module_handler); * The entity manager. * * @return $this + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. + * + * @todo Remove this set call in https://www.drupal.org/node/2603542. */ public function setEntityManager(EntityManagerInterface $entity_manager); + /** + * Sets the entity type manager for this form. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * + * @return $this + */ + public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager); + } diff --git a/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepository.php b/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..a715e17e344501dbd0c106772a4f18cb12febf5d --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepository.php @@ -0,0 +1,97 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityLastInstalledSchemaRepository. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; + +/** + * Provides a repository for installed entity definitions. + */ +class EntityLastInstalledSchemaRepository implements EntityLastInstalledSchemaRepositoryInterface { + + /** + * The key-value factory. + * + * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface + */ + protected $keyValueFactory; + + /** + * Constructs a new EntityLastInstalledSchemaRepository. + * + * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory + * The key-value factory. + */ + public function __construct(KeyValueFactoryInterface $key_value_factory) { + $this->keyValueFactory = $key_value_factory; + } + + /** + * {@inheritdoc} + */ + public function getLastInstalledDefinition($entity_type_id) { + return $this->keyValueFactory->get('entity.definitions.installed')->get($entity_type_id . '.entity_type'); + } + + /** + * {@inheritdoc} + */ + public function setLastInstalledDefinition(EntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + $this->keyValueFactory->get('entity.definitions.installed')->set($entity_type_id . '.entity_type', $entity_type); + return $this; + } + + /** + * {@inheritdoc} + */ + public function deleteLastInstalledDefinition($entity_type_id) { + $this->keyValueFactory->get('entity.definitions.installed')->delete($entity_type_id . '.entity_type'); + // Clean up field storage definitions as well. Even if the entity type + // isn't currently fieldable, there might be legacy definitions or an + // empty array stored from when it was. + $this->keyValueFactory->get('entity.definitions.installed')->delete($entity_type_id . '.field_storage_definitions'); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLastInstalledFieldStorageDefinitions($entity_type_id) { + return $this->keyValueFactory->get('entity.definitions.installed')->get($entity_type_id . '.field_storage_definitions', []); + } + + /** + * {@inheritdoc} + */ + public function setLastInstalledFieldStorageDefinitions($entity_type_id, array $storage_definitions) { + $this->keyValueFactory->get('entity.definitions.installed')->set($entity_type_id . '.field_storage_definitions', $storage_definitions); + } + + /** + * {@inheritdoc} + */ + public function setLastInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { + $entity_type_id = $storage_definition->getTargetEntityTypeId(); + $definitions = $this->getLastInstalledFieldStorageDefinitions($entity_type_id); + $definitions[$storage_definition->getName()] = $storage_definition; + $this->setLastInstalledFieldStorageDefinitions($entity_type_id, $definitions); + } + + /** + * {@inheritdoc} + */ + public function deleteLastInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { + $entity_type_id = $storage_definition->getTargetEntityTypeId(); + $definitions = $this->getLastInstalledFieldStorageDefinitions($entity_type_id); + unset($definitions[$storage_definition->getName()]); + $this->setLastInstalledFieldStorageDefinitions($entity_type_id, $definitions); + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepositoryInterface.php b/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepositoryInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..919cea604183c2109d9d4e8b2ef9a33914be0130 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityLastInstalledSchemaRepositoryInterface.php @@ -0,0 +1,130 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Core\Field\FieldStorageDefinitionInterface; + +/** + * Provides an interface for an installed entity definition repository. + */ +interface EntityLastInstalledSchemaRepositoryInterface { + + /** + * Gets the entity type definition in its most recently installed state. + * + * During the application lifetime, entity type definitions can change. For + * example, updated code can be deployed. The getDefinition() method will + * always return the definition as determined by the current codebase. This + * method, however, returns what the definition was when the last time that + * one of the \Drupal\Core\Entity\EntityTypeListenerInterface events was last + * fired and completed successfully. In other words, the definition that + * the entity type's handlers have incorporated into the application state. + * For example, if the entity type's storage handler is SQL-based, the + * definition for which database tables were created. + * + * Application management code can check if getDefinition() differs from + * getLastInstalledDefinition() and decide whether to: + * - Invoke the appropriate \Drupal\Core\Entity\EntityTypeListenerInterface + * event so that handlers react to the new definition. + * - Raise a warning that the application state is incompatible with the + * codebase. + * - Perform some other action. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return \Drupal\Core\Entity\EntityTypeInterface|null + * The installed entity type definition, or NULL if the entity type has + * not yet been installed via onEntityTypeCreate(). + * + * @see \Drupal\Core\Entity\EntityTypeListenerInterface + */ + public function getLastInstalledDefinition($entity_type_id); + + /** + * Stores the entity type definition in the application state. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * + * @return $this + */ + public function setLastInstalledDefinition(EntityTypeInterface $entity_type); + + /** + * Deletes the entity type definition from the application state. + * + * @param string $entity_type_id + * The entity type definition identifier. + * + * @return $this + */ + public function deleteLastInstalledDefinition($entity_type_id); + + /** + * Gets the entity type's most recently installed field storage definitions. + * + * During the application lifetime, field storage definitions can change. For + * example, updated code can be deployed. The getFieldStorageDefinitions() + * method will always return the definitions as determined by the current + * codebase. This method, however, returns what the definitions were when the + * last time that one of the + * \Drupal\Core\Field\FieldStorageDefinitionListenerInterface events was last + * fired and completed successfully. In other words, the definitions that + * the entity type's handlers have incorporated into the application state. + * For example, if the entity type's storage handler is SQL-based, the + * definitions for which database tables were created. + * + * Application management code can check if getFieldStorageDefinitions() + * differs from getLastInstalledFieldStorageDefinitions() and decide whether + * to: + * - Invoke the appropriate + * \Drupal\Core\Field\FieldStorageDefinitionListenerInterface + * events so that handlers react to the new definitions. + * - Raise a warning that the application state is incompatible with the + * codebase. + * - Perform some other action. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] + * The array of installed field storage definitions for the entity type, + * keyed by field name. + * + * @see \Drupal\Core\Entity\EntityTypeListenerInterface + */ + public function getLastInstalledFieldStorageDefinitions($entity_type_id); + + /** + * Stores the entity type's field storage definitions in the application state. + * + * @param string $entity_type_id + * The entity type identifier. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions + * An array of field storage definitions. + */ + public function setLastInstalledFieldStorageDefinitions($entity_type_id, array $storage_definitions); + + /** + * Stores the field storage definition in the application state. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + */ + public function setLastInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition); + + /** + * Deletes the field storage definition from the application state. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + */ + public function deleteLastInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition); + +} diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index 185ca8b3a0d0254052f7cf38ce16316eb2972c53..d65594d05a805b771c8d87c63fc9e3cb89b92bbc 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -7,1468 +7,512 @@ namespace Drupal\Core\Entity; -use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; -use Drupal\Component\Plugin\Exception\PluginNotFoundException; -use Drupal\Core\Cache\Cache; -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Config\Entity\ConfigEntityType; -use Drupal\Core\DependencyInjection\ClassResolverInterface; -use Drupal\Core\Entity\Exception\AmbiguousEntityClassException; -use Drupal\Core\Entity\Exception\InvalidLinkTemplateException; -use Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\FieldStorageDefinitionEvent; -use Drupal\Core\Field\FieldStorageDefinitionEvents; use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\Core\Field\FieldStorageDefinitionListenerInterface; -use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; -use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Core\Plugin\DefaultPluginManager; -use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\Core\StringTranslation\TranslationInterface; -use Drupal\Core\TypedData\TranslatableInterface; -use Drupal\Core\TypedData\TypedDataManagerInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** - * Manages entity type plugin definitions. + * Provides a wrapper around many other services relating to entities. * - * Each entity type definition array is set in the entity type's - * annotation and altered by hook_entity_type_alter(). + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. * - * The defaults for the plugin definition are provided in - * \Drupal\Core\Entity\EntityManagerInterface::defaults. - * - * @see \Drupal\Core\Entity\Annotation\EntityType - * @see \Drupal\Core\Entity\EntityInterface - * @see \Drupal\Core\Entity\EntityTypeInterface - * @see hook_entity_type_alter() + * @todo Enforce the deprecation of each method once + * https://www.drupal.org/node/2578361 is in. */ -class EntityManager extends DefaultPluginManager implements EntityManagerInterface, ContainerAwareInterface { +class EntityManager implements EntityManagerInterface, ContainerAwareInterface { use ContainerAwareTrait; - use StringTranslationTrait; - - /** - * Extra fields by bundle. - * - * @var array - */ - protected $extraFields = array(); - - /** - * Contains instantiated handlers keyed by handler type and entity type. - * - * @var array - */ - protected $handlers = array(); - - /** - * Static cache of base field definitions. - * - * @var array - */ - protected $baseFieldDefinitions; - - /** - * Static cache of field definitions per bundle and entity type. - * - * @var array - */ - protected $fieldDefinitions; - - /** - * Static cache of field storage definitions per entity type. - * - * Elements of the array: - * - $entity_type_id: \Drupal\Core\Field\BaseFieldDefinition[] - * - * @var array - */ - protected $fieldStorageDefinitions; - - /** - * The string translationManager. - * - * @var \Drupal\Core\StringTranslation\TranslationInterface - */ - protected $translationManager; - - /** - * The class resolver. - * - * @var \Drupal\Core\DependencyInjection\ClassResolverInterface - */ - protected $classResolver; - - /** - * The typed data manager. - * - * @var \Drupal\Core\TypedData\TypedDataManagerInterface - */ - protected $typedDataManager; - - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - - /** - * The keyvalue factory. - * - * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface - */ - protected $keyValueFactory; - - /** - * The event dispatcher. - * - * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface - */ - protected $eventDispatcher; - - /** - * Static cache of bundle information. - * - * @var array - */ - protected $bundleInfo; - - /** - * Static cache of display modes information. - * - * @var array - */ - protected $displayModeInfo = array(); - - /** - * An array keyed by entity type. Each value is an array whose keys are - * field names and whose value is an array with two entries: - * - type: The field type. - * - bundles: The bundles in which the field appears. - * - * @return array - */ - protected $fieldMap = array(); - - /** - * An array keyed by field type. Each value is an array whose key are entity - * types including arrays in the same form that $fieldMap. - * - * It helps access the mapping between types and fields by the field type. - * - * @var array - */ - protected $fieldMapByFieldType = array(); - - /** - * Contains cached mappings of class names to entity types. - * - * @var array - */ - protected $classNameEntityTypeMap = array(); - - /** - * Constructs a new Entity plugin manager. - * - * @param \Traversable $namespaces - * An object that implements \Traversable which contains the root paths - * keyed by the corresponding namespace to look for plugin implementations, - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler. - * @param \Drupal\Core\Cache\CacheBackendInterface $cache - * The cache backend to use. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. - * @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager - * The string translationManager. - * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver - * The class resolver. - * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager - * The typed data manager. - * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory - * The keyvalue factory. - * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher - * The event dispatcher. - */ - public function __construct(\Traversable $namespaces, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, TranslationInterface $translation_manager, ClassResolverInterface $class_resolver, TypedDataManagerInterface $typed_data_manager, KeyValueFactoryInterface $key_value_factory, EventDispatcherInterface $event_dispatcher) { - parent::__construct('Entity', $namespaces, $module_handler, 'Drupal\Core\Entity\EntityInterface'); - - $this->setCacheBackend($cache, 'entity_type', array('entity_types')); - $this->alterInfo('entity_type'); - - $this->discovery = new AnnotatedClassDiscovery('Entity', $namespaces, 'Drupal\Core\Entity\Annotation\EntityType'); - $this->languageManager = $language_manager; - $this->translationManager = $translation_manager; - $this->classResolver = $class_resolver; - $this->typedDataManager = $typed_data_manager; - $this->keyValueFactory = $key_value_factory; - $this->eventDispatcher = $event_dispatcher; - } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function clearCachedDefinitions() { - parent::clearCachedDefinitions(); - $this->clearCachedBundles(); - $this->clearCachedFieldDefinitions(); - $this->classNameEntityTypeMap = array(); - $this->handlers = array(); - } - - /** - * {@inheritdoc} - */ - public function processDefinition(&$definition, $plugin_id) { - /** @var \Drupal\Core\Entity\EntityTypeInterface $definition */ - parent::processDefinition($definition, $plugin_id); - - // All link templates must have a leading slash. - foreach ((array) $definition->getLinkTemplates() as $link_relation_name => $link_template) { - if ($link_template[0] != '/') { - throw new InvalidLinkTemplateException("Link template '$link_relation_name' for entity type '$plugin_id' must start with a leading slash, the current link template is '$link_template'"); - } - } - } - - /** - * {@inheritdoc} - */ - protected function findDefinitions() { - $definitions = $this->getDiscovery()->getDefinitions(); + $this->container->get('entity_type.manager')->clearCachedDefinitions(); - // Directly call the hook implementations to pass the definitions to them - // by reference, so new entity types can be added. - foreach ($this->moduleHandler->getImplementations('entity_type_build') as $module) { - $function = $module . '_' . 'entity_type_build'; - $function($definitions); - } - foreach ($definitions as $plugin_id => $definition) { - $this->processDefinition($definition, $plugin_id); - } - if ($this->alterHook) { - $this->moduleHandler->alter($this->alterHook, $definitions); - } - return $definitions; + // @todo None of these are plugin managers, and they should not co-opt + // this method for managing its caches. Remove in + // https://www.drupal.org/node/2549143. + $this->container->get('entity_type.bundle.info')->clearCachedBundles(); + $this->container->get('entity_field.manager')->clearCachedFieldDefinitions(); + $this->container->get('entity_type.repository')->clearCachedDefinitions(); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getDefinition($entity_type_id, $exception_on_invalid = TRUE) { - if (($entity_type = parent::getDefinition($entity_type_id, FALSE)) && class_exists($entity_type->getClass())) { - return $entity_type; - } - elseif (!$exception_on_invalid) { - return NULL; - } - - throw new PluginNotFoundException($entity_type_id, sprintf('The "%s" entity type does not exist.', $entity_type_id)); + return $this->container->get('entity_type.manager')->getDefinition($entity_type_id, $exception_on_invalid); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function hasHandler($entity_type, $handler_type) { - if ($definition = $this->getDefinition($entity_type, FALSE)) { - return $definition->hasHandlerClass($handler_type); - } - return FALSE; + return $this->container->get('entity_type.manager')->hasHandler($entity_type, $handler_type); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getStorage($entity_type) { - return $this->getHandler($entity_type, 'storage'); + return $this->container->get('entity_type.manager')->getStorage($entity_type); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getListBuilder($entity_type) { - return $this->getHandler($entity_type, 'list_builder'); + return $this->container->get('entity_type.manager')->getListBuilder($entity_type); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getFormObject($entity_type, $operation) { - if (!isset($this->handlers['form'][$operation][$entity_type])) { - if (!$class = $this->getDefinition($entity_type, TRUE)->getFormClass($operation)) { - throw new InvalidPluginDefinitionException($entity_type, sprintf('The "%s" entity type did not specify a "%s" form class.', $entity_type, $operation)); - } - - $form_object = $this->classResolver->getInstanceFromDefinition($class); - - $form_object - ->setStringTranslation($this->translationManager) - ->setModuleHandler($this->moduleHandler) - ->setEntityManager($this) - ->setOperation($operation); - $this->handlers['form'][$operation][$entity_type] = $form_object; - } - return $this->handlers['form'][$operation][$entity_type]; + return $this->container->get('entity_type.manager')->getFormObject($entity_type, $operation); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getRouteProviders($entity_type) { - if (!isset($this->handlers['route_provider'][$entity_type])) { - $route_provider_classes = $this->getDefinition($entity_type, TRUE)->getRouteProviderClasses(); - - foreach ($route_provider_classes as $type => $class) { - $this->handlers['route_provider'][$entity_type][$type] = $this->createHandlerInstance($class, $this->getDefinition($entity_type)); - } - } - return isset($this->handlers['route_provider'][$entity_type]) ? $this->handlers['route_provider'][$entity_type] : []; + return $this->container->get('entity_type.manager')->getRouteProviders($entity_type); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getViewBuilder($entity_type) { - return $this->getHandler($entity_type, 'view_builder'); + return $this->container->get('entity_type.manager')->getViewBuilder($entity_type); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getAccessControlHandler($entity_type) { - return $this->getHandler($entity_type, 'access'); + return $this->container->get('entity_type.manager')->getAccessControlHandler($entity_type); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getHandler($entity_type, $handler_type) { - if (!isset($this->handlers[$handler_type][$entity_type])) { - $definition = $this->getDefinition($entity_type); - $class = $definition->getHandlerClass($handler_type); - if (!$class) { - throw new InvalidPluginDefinitionException($entity_type, sprintf('The "%s" entity type did not specify a %s handler.', $entity_type, $handler_type)); - } - $this->handlers[$handler_type][$entity_type] = $this->createHandlerInstance($class, $definition); - } - return $this->handlers[$handler_type][$entity_type]; + return $this->container->get('entity_type.manager')->getHandler($entity_type, $handler_type); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function createHandlerInstance($class, EntityTypeInterface $definition = null) { - if (is_subclass_of($class, 'Drupal\Core\Entity\EntityHandlerInterface')) { - $handler = $class::createInstance($this->container, $definition); - } - else { - $handler = new $class($definition); - } - if (method_exists($handler, 'setModuleHandler')) { - $handler->setModuleHandler($this->moduleHandler); - } - if (method_exists($handler, 'setStringTranslation')) { - $handler->setStringTranslation($this->translationManager); - } - return $handler; + return $this->container->get('entity_type.manager')->createHandlerInstance($class, $definition); } /** * {@inheritdoc} - */ - public function getBaseFieldDefinitions($entity_type_id) { - // Check the static cache. - if (!isset($this->baseFieldDefinitions[$entity_type_id])) { - // Not prepared, try to load from cache. - $cid = 'entity_base_field_definitions:' . $entity_type_id . ':' . $this->languageManager->getCurrentLanguage()->getId(); - if ($cache = $this->cacheGet($cid)) { - $this->baseFieldDefinitions[$entity_type_id] = $cache->data; - } - else { - // Rebuild the definitions and put it into the cache. - $this->baseFieldDefinitions[$entity_type_id] = $this->buildBaseFieldDefinitions($entity_type_id); - $this->cacheSet($cid, $this->baseFieldDefinitions[$entity_type_id], Cache::PERMANENT, array('entity_types', 'entity_field_info')); - } - } - return $this->baseFieldDefinitions[$entity_type_id]; - } - - /** - * Builds base field definitions for an entity type. * - * @param string $entity_type_id - * The entity type ID. Only entity types that implement - * \Drupal\Core\Entity\FieldableEntityInterface are supported. - * - * @return \Drupal\Core\Field\FieldDefinitionInterface[] - * An array of field definitions, keyed by field name. - * - * @throws \LogicException - * Thrown if a config entity type is given or if one of the entity keys is - * flagged as translatable. + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - protected function buildBaseFieldDefinitions($entity_type_id) { - $entity_type = $this->getDefinition($entity_type_id); - $class = $entity_type->getClass(); - $keys = array_filter($entity_type->getKeys()); - - // Fail with an exception for non-fieldable entity types. - if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) { - throw new \LogicException("Getting the base fields is not supported for entity type {$entity_type->getLabel()}."); - } - - // Retrieve base field definitions. - /** @var FieldStorageDefinitionInterface[] $base_field_definitions */ - $base_field_definitions = $class::baseFieldDefinitions($entity_type); - - // Make sure translatable entity types are correctly defined. - if ($entity_type->isTranslatable()) { - // The langcode field should always be translatable if the entity type is. - if (isset($keys['langcode']) && isset($base_field_definitions[$keys['langcode']])) { - $base_field_definitions[$keys['langcode']]->setTranslatable(TRUE); - } - // A default_langcode field should always be defined. - if (!isset($base_field_definitions[$keys['default_langcode']])) { - $base_field_definitions[$keys['default_langcode']] = BaseFieldDefinition::create('boolean') - ->setLabel($this->t('Default translation')) - ->setDescription($this->t('A flag indicating whether this is the default translation.')) - ->setTranslatable(TRUE) - ->setRevisionable(TRUE) - ->setDefaultValue(TRUE); - } - } - - // Assign base field definitions the entity type provider. - $provider = $entity_type->getProvider(); - foreach ($base_field_definitions as $definition) { - // @todo Remove this check once FieldDefinitionInterface exposes a proper - // provider setter. See https://www.drupal.org/node/2225961. - if ($definition instanceof BaseFieldDefinition) { - $definition->setProvider($provider); - } - } - - // Retrieve base field definitions from modules. - foreach ($this->moduleHandler->getImplementations('entity_base_field_info') as $module) { - $module_definitions = $this->moduleHandler->invoke($module, 'entity_base_field_info', array($entity_type)); - if (!empty($module_definitions)) { - // Ensure the provider key actually matches the name of the provider - // defining the field. - foreach ($module_definitions as $field_name => $definition) { - // @todo Remove this check once FieldDefinitionInterface exposes a - // proper provider setter. See https://www.drupal.org/node/2225961. - if ($definition instanceof BaseFieldDefinition && $definition->getProvider() == NULL) { - $definition->setProvider($module); - } - $base_field_definitions[$field_name] = $definition; - } - } - } - - // Automatically set the field name, target entity type and bundle - // for non-configurable fields. - foreach ($base_field_definitions as $field_name => $base_field_definition) { - if ($base_field_definition instanceof BaseFieldDefinition) { - $base_field_definition->setName($field_name); - $base_field_definition->setTargetEntityTypeId($entity_type_id); - $base_field_definition->setTargetBundle(NULL); - } - } - - // Invoke alter hook. - $this->moduleHandler->alter('entity_base_field_info', $base_field_definitions, $entity_type); - - // Ensure defined entity keys are there and have proper revisionable and - // translatable values. - foreach (array_intersect_key($keys, array_flip(['id', 'revision', 'uuid', 'bundle'])) as $key => $field_name) { - if (!isset($base_field_definitions[$field_name])) { - throw new \LogicException("The $field_name field definition does not exist and it is used as $key entity key."); - } - if ($base_field_definitions[$field_name]->isRevisionable()) { - throw new \LogicException("The {$base_field_definitions[$field_name]->getLabel()} field cannot be revisionable as it is used as $key entity key."); - } - if ($base_field_definitions[$field_name]->isTranslatable()) { - throw new \LogicException("The {$base_field_definitions[$field_name]->getLabel()} field cannot be translatable as it is used as $key entity key."); - } - } - - // Make sure translatable entity types define the "langcode" field properly. - if ($entity_type->isTranslatable() && (!isset($keys['langcode']) || !isset($base_field_definitions[$keys['langcode']]) || !$base_field_definitions[$keys['langcode']]->isTranslatable())) { - throw new \LogicException("The {$entity_type->getLabel()} entity type cannot be translatable as it does not define a translatable \"langcode\" field."); - } - - return $base_field_definitions; + public function getBaseFieldDefinitions($entity_type_id) { + return $this->container->get('entity_field.manager')->getBaseFieldDefinitions($entity_type_id); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getFieldDefinitions($entity_type_id, $bundle) { - if (!isset($this->fieldDefinitions[$entity_type_id][$bundle])) { - $base_field_definitions = $this->getBaseFieldDefinitions($entity_type_id); - // Not prepared, try to load from cache. - $cid = 'entity_bundle_field_definitions:' . $entity_type_id . ':' . $bundle . ':' . $this->languageManager->getCurrentLanguage()->getId(); - if ($cache = $this->cacheGet($cid)) { - $bundle_field_definitions = $cache->data; - } - else { - // Rebuild the definitions and put it into the cache. - $bundle_field_definitions = $this->buildBundleFieldDefinitions($entity_type_id, $bundle, $base_field_definitions); - $this->cacheSet($cid, $bundle_field_definitions, Cache::PERMANENT, array('entity_types', 'entity_field_info')); - } - // Field definitions consist of the bundle specific overrides and the - // base fields, merge them together. Use array_replace() to replace base - // fields with by bundle overrides and keep them in order, append - // additional by bundle fields. - $this->fieldDefinitions[$entity_type_id][$bundle] = array_replace($base_field_definitions, $bundle_field_definitions); - } - return $this->fieldDefinitions[$entity_type_id][$bundle]; + return $this->container->get('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle); } /** - * Builds field definitions for a specific bundle within an entity type. - * - * @param string $entity_type_id - * The entity type ID. Only entity types that implement - * \Drupal\Core\Entity\FieldableEntityInterface are supported. - * @param string $bundle - * The bundle. - * @param \Drupal\Core\Field\FieldDefinitionInterface[] $base_field_definitions - * The list of base field definitions. + * {@inheritdoc} * - * @return \Drupal\Core\Field\FieldDefinitionInterface[] - * An array of bundle field definitions, keyed by field name. Does - * not include base fields. + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - protected function buildBundleFieldDefinitions($entity_type_id, $bundle, array $base_field_definitions) { - $entity_type = $this->getDefinition($entity_type_id); - $class = $entity_type->getClass(); - - // Allow the entity class to provide bundle fields and bundle-specific - // overrides of base fields. - $bundle_field_definitions = $class::bundleFieldDefinitions($entity_type, $bundle, $base_field_definitions); - - // Load base field overrides from configuration. These take precedence over - // base field overrides returned above. - $base_field_override_ids = array_map(function($field_name) use ($entity_type_id, $bundle) { - return $entity_type_id . '.' . $bundle . '.' . $field_name; - }, array_keys($base_field_definitions)); - $base_field_overrides = $this->getStorage('base_field_override')->loadMultiple($base_field_override_ids); - foreach ($base_field_overrides as $base_field_override) { - /** @var \Drupal\Core\Field\Entity\BaseFieldOverride $base_field_override */ - $field_name = $base_field_override->getName(); - $bundle_field_definitions[$field_name] = $base_field_override; - } - - $provider = $entity_type->getProvider(); - foreach ($bundle_field_definitions as $definition) { - // @todo Remove this check once FieldDefinitionInterface exposes a proper - // provider setter. See https://www.drupal.org/node/2225961. - if ($definition instanceof BaseFieldDefinition) { - $definition->setProvider($provider); - } - } - - // Retrieve base field definitions from modules. - foreach ($this->moduleHandler->getImplementations('entity_bundle_field_info') as $module) { - $module_definitions = $this->moduleHandler->invoke($module, 'entity_bundle_field_info', array($entity_type, $bundle, $base_field_definitions)); - if (!empty($module_definitions)) { - // Ensure the provider key actually matches the name of the provider - // defining the field. - foreach ($module_definitions as $field_name => $definition) { - // @todo Remove this check once FieldDefinitionInterface exposes a - // proper provider setter. See https://www.drupal.org/node/2225961. - if ($definition instanceof BaseFieldDefinition) { - $definition->setProvider($module); - } - $bundle_field_definitions[$field_name] = $definition; - } - } - } - - // Automatically set the field name, target entity type and bundle - // for non-configurable fields. - foreach ($bundle_field_definitions as $field_name => $field_definition) { - if ($field_definition instanceof BaseFieldDefinition) { - $field_definition->setName($field_name); - $field_definition->setTargetEntityTypeId($entity_type_id); - $field_definition->setTargetBundle($bundle); - } - } - - // Invoke 'per bundle' alter hook. - $this->moduleHandler->alter('entity_bundle_field_info', $bundle_field_definitions, $entity_type, $bundle); - - return $bundle_field_definitions; + public function getFieldStorageDefinitions($entity_type_id) { + return $this->container->get('entity_field.manager')->getFieldStorageDefinitions($entity_type_id); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - public function getFieldStorageDefinitions($entity_type_id) { - if (!isset($this->fieldStorageDefinitions[$entity_type_id])) { - $this->fieldStorageDefinitions[$entity_type_id] = array(); - // Add all non-computed base fields. - foreach ($this->getBaseFieldDefinitions($entity_type_id) as $field_name => $definition) { - if (!$definition->isComputed()) { - $this->fieldStorageDefinitions[$entity_type_id][$field_name] = $definition; - } - } - // Not prepared, try to load from cache. - $cid = 'entity_field_storage_definitions:' . $entity_type_id . ':' . $this->languageManager->getCurrentLanguage()->getId(); - if ($cache = $this->cacheGet($cid)) { - $field_storage_definitions = $cache->data; - } - else { - // Rebuild the definitions and put it into the cache. - $field_storage_definitions = $this->buildFieldStorageDefinitions($entity_type_id); - $this->cacheSet($cid, $field_storage_definitions, Cache::PERMANENT, array('entity_types', 'entity_field_info')); - } - $this->fieldStorageDefinitions[$entity_type_id] += $field_storage_definitions; - } - return $this->fieldStorageDefinitions[$entity_type_id]; + public function setFieldMap(array $field_map) { + return $this->container->get('entity_field.manager')->setFieldMap($field_map); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getFieldMap() { - if (!$this->fieldMap) { - // Not prepared, try to load from cache. - $cid = 'entity_field_map'; - if ($cache = $this->cacheGet($cid)) { - $this->fieldMap = $cache->data; - } - else { - // The field map is built in two steps. First, add all base fields, by - // looping over all fieldable entity types. They always exist for all - // bundles, and we do not expect to have so many different entity - // types for this to become a bottleneck. - foreach ($this->getDefinitions() as $entity_type_id => $entity_type) { - if ($entity_type->isSubclassOf(FieldableEntityInterface::class)) { - $bundles = array_keys($this->getBundleInfo($entity_type_id)); - foreach ($this->getBaseFieldDefinitions($entity_type_id) as $field_name => $base_field_definition) { - $this->fieldMap[$entity_type_id][$field_name] = [ - 'type' => $base_field_definition->getType(), - 'bundles' => array_combine($bundles, $bundles), - ]; - } - } - } - - // In the second step, the per-bundle fields are added, based on the - // persistent bundle field map stored in a key value collection. This - // data is managed in the EntityManager::onFieldDefinitionCreate() - // and EntityManager::onFieldDefinitionDelete() methods. Rebuilding this - // information in the same way as base fields would not scale, as the - // time to query would grow exponentially with more fields and bundles. - // A cache would be deleted during cache clears, which is the only time - // it is needed, so a key value collection is used. - $bundle_field_maps = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->getAll(); - foreach ($bundle_field_maps as $entity_type_id => $bundle_field_map) { - foreach ($bundle_field_map as $field_name => $map_entry) { - if (!isset($this->fieldMap[$entity_type_id][$field_name])) { - $this->fieldMap[$entity_type_id][$field_name] = $map_entry; - } - else { - $this->fieldMap[$entity_type_id][$field_name]['bundles'] += $map_entry['bundles']; - } - } - } - - $this->cacheSet($cid, $this->fieldMap, Cache::PERMANENT, array('entity_types')); - } - } - return $this->fieldMap; + return $this->container->get('entity_field.manager')->getFieldMap(); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getFieldMapByFieldType($field_type) { - if (!isset($this->fieldMapByFieldType[$field_type])) { - $filtered_map = array(); - $map = $this->getFieldMap(); - foreach ($map as $entity_type => $fields) { - foreach ($fields as $field_name => $field_info) { - if ($field_info['type'] == $field_type) { - $filtered_map[$entity_type][$field_name] = $field_info; - } - } - } - $this->fieldMapByFieldType[$field_type] = $filtered_map; - } - return $this->fieldMapByFieldType[$field_type]; + return $this->container->get('entity_field.manager')->getFieldMapByFieldType($field_type); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) { - $entity_type_id = $field_definition->getTargetEntityTypeId(); - $bundle = $field_definition->getTargetBundle(); - $field_name = $field_definition->getName(); - - // Notify the storage about the new field. - $this->getStorage($entity_type_id)->onFieldDefinitionCreate($field_definition); - - // Update the bundle field map key value collection, add the new field. - $bundle_field_map = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->get($entity_type_id); - if (!isset($bundle_field_map[$field_name])) { - // This field did not exist yet, initialize it with the type and empty - // bundle list. - $bundle_field_map[$field_name] = [ - 'type' => $field_definition->getType(), - 'bundles' => [], - ]; - } - $bundle_field_map[$field_name]['bundles'][$bundle] = $bundle; - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->set($entity_type_id, $bundle_field_map); - - // Delete the cache entry. - $this->cacheBackend->delete('entity_field_map'); - - // If the field map is initialized, update it as well, so that calls to it - // do not have to rebuild it again. - if ($this->fieldMap) { - if (!isset($this->fieldMap[$entity_type_id][$field_name])) { - // This field did not exist yet, initialize it with the type and empty - // bundle list. - $this->fieldMap[$entity_type_id][$field_name] = [ - 'type' => $field_definition->getType(), - 'bundles' => [], - ]; - } - $this->fieldMap[$entity_type_id][$field_name]['bundles'][$bundle] = $bundle; - } + $this->container->get('field_definition.listener')->onFieldDefinitionCreate($field_definition); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) { - // Notify the storage about the updated field. - $this->getStorage($field_definition->getTargetEntityTypeId())->onFieldDefinitionUpdate($field_definition, $original); + $this->container->get('field_definition.listener')->onFieldDefinitionUpdate($field_definition, $original); } /** * {@inheritdoc} - */ - public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) { - $entity_type_id = $field_definition->getTargetEntityTypeId(); - $bundle = $field_definition->getTargetBundle(); - $field_name = $field_definition->getName(); - - // Notify the storage about the field deletion. - $this->getStorage($entity_type_id)->onFieldDefinitionDelete($field_definition); - - // Unset the bundle from the bundle field map key value collection. - $bundle_field_map = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->get($entity_type_id); - unset($bundle_field_map[$field_name]['bundles'][$bundle]); - if (empty($bundle_field_map[$field_name]['bundles'])) { - // If there are no bundles left, remove the field from the map. - unset($bundle_field_map[$field_name]); - } - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->set($entity_type_id, $bundle_field_map); - - // Delete the cache entry. - $this->cacheBackend->delete('entity_field_map'); - - // If the field map is initialized, update it as well, so that calls to it - // do not have to rebuild it again. - if ($this->fieldMap) { - unset($this->fieldMap[$entity_type_id][$field_name]['bundles'][$bundle]); - if (empty($this->fieldMap[$entity_type_id][$field_name]['bundles'])) { - unset($this->fieldMap[$entity_type_id][$field_name]); - } - } - } - - /** - * Builds field storage definitions for an entity type. * - * @param string $entity_type_id - * The entity type ID. Only entity types that implement - * \Drupal\Core\Entity\FieldableEntityInterface are supported - * - * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] - * An array of field storage definitions, keyed by field name. + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - protected function buildFieldStorageDefinitions($entity_type_id) { - $entity_type = $this->getDefinition($entity_type_id); - $field_definitions = array(); - - // Retrieve base field definitions from modules. - foreach ($this->moduleHandler->getImplementations('entity_field_storage_info') as $module) { - $module_definitions = $this->moduleHandler->invoke($module, 'entity_field_storage_info', array($entity_type)); - if (!empty($module_definitions)) { - // Ensure the provider key actually matches the name of the provider - // defining the field. - foreach ($module_definitions as $field_name => $definition) { - // @todo Remove this check once FieldDefinitionInterface exposes a - // proper provider setter. See https://www.drupal.org/node/2225961. - if ($definition instanceof BaseFieldDefinition) { - $definition->setProvider($module); - } - $field_definitions[$field_name] = $definition; - } - } - } - - // Invoke alter hook. - $this->moduleHandler->alter('entity_field_storage_info', $field_definitions, $entity_type); - - return $field_definitions; + public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) { + $this->container->get('field_definition.listener')->onFieldDefinitionDelete($field_definition); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function clearCachedFieldDefinitions() { - $this->baseFieldDefinitions = array(); - $this->fieldDefinitions = array(); - $this->fieldStorageDefinitions = array(); - $this->fieldMap = array(); - $this->fieldMapByFieldType = array(); - $this->displayModeInfo = array(); - $this->extraFields = array(); - Cache::invalidateTags(array('entity_field_info')); - // The typed data manager statically caches prototype objects with injected - // definitions, clear those as well. - $this->typedDataManager->clearCachedDefinitions(); + $this->container->get('entity_field.manager')->clearCachedFieldDefinitions(); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function clearCachedBundles() { - $this->bundleInfo = array(); - Cache::invalidateTags(array('entity_bundles')); - // Entity bundles are exposed as data types, clear that cache too. - $this->typedDataManager->clearCachedDefinitions(); + $this->container->get('entity_type.bundle.info')->clearCachedBundles(); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getBundleInfo($entity_type) { - $bundle_info = $this->getAllBundleInfo(); - return isset($bundle_info[$entity_type]) ? $bundle_info[$entity_type] : array(); + return $this->container->get('entity_type.bundle.info')->getBundleInfo($entity_type); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getAllBundleInfo() { - if (empty($this->bundleInfo)) { - $langcode = $this->languageManager->getCurrentLanguage()->getId(); - if ($cache = $this->cacheGet("entity_bundle_info:$langcode")) { - $this->bundleInfo = $cache->data; - } - else { - $this->bundleInfo = $this->moduleHandler->invokeAll('entity_bundle_info'); - // First look for entity types that act as bundles for others, load them - // and add them as bundles. - foreach ($this->getDefinitions() as $type => $entity_type) { - if ($entity_type->getBundleOf()) { - foreach ($this->getStorage($type)->loadMultiple() as $entity) { - $this->bundleInfo[$entity_type->getBundleOf()][$entity->id()]['label'] = $entity->label(); - } - } - } - foreach ($this->getDefinitions() as $type => $entity_type) { - // If no bundles are provided, use the entity type name and label. - if (!isset($this->bundleInfo[$type])) { - $this->bundleInfo[$type][$type]['label'] = $entity_type->getLabel(); - } - } - $this->moduleHandler->alter('entity_bundle_info', $this->bundleInfo); - $this->cacheSet("entity_bundle_info:$langcode", $this->bundleInfo, Cache::PERMANENT, array('entity_types', 'entity_bundles')); - } - } - - return $this->bundleInfo; + return $this->container->get('entity_type.bundle.info')->getAllBundleInfo(); } /** * {@inheritdoc} */ public function getExtraFields($entity_type_id, $bundle) { - // Read from the "static" cache. - if (isset($this->extraFields[$entity_type_id][$bundle])) { - return $this->extraFields[$entity_type_id][$bundle]; - } - - // Read from the persistent cache. Since hook_entity_extra_field_info() and - // hook_entity_extra_field_info_alter() might contain t() calls, we cache - // per language. - $cache_id = 'entity_bundle_extra_fields:' . $entity_type_id . ':' . $bundle . ':' . $this->languageManager->getCurrentLanguage()->getId(); - $cached = $this->cacheGet($cache_id); - if ($cached) { - $this->extraFields[$entity_type_id][$bundle] = $cached->data; - return $this->extraFields[$entity_type_id][$bundle]; - } - - $extra = $this->moduleHandler->invokeAll('entity_extra_field_info'); - $this->moduleHandler->alter('entity_extra_field_info', $extra); - $info = isset($extra[$entity_type_id][$bundle]) ? $extra[$entity_type_id][$bundle] : array(); - $info += array( - 'form' => array(), - 'display' => array(), - ); - - // Store in the 'static' and persistent caches. - $this->extraFields[$entity_type_id][$bundle] = $info; - $this->cacheSet($cache_id, $info, Cache::PERMANENT, array( - 'entity_field_info', - )); - - return $this->extraFields[$entity_type_id][$bundle]; + return $this->container->get('entity_field.manager')->getExtraFields($entity_type_id, $bundle); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getEntityTypeLabels($group = FALSE) { - $options = array(); - $definitions = $this->getDefinitions(); - - foreach ($definitions as $entity_type_id => $definition) { - if ($group) { - $options[(string) $definition->getGroupLabel()][$entity_type_id] = $definition->getLabel(); - } - else { - $options[$entity_type_id] = $definition->getLabel(); - } - } - - if ($group) { - foreach ($options as &$group_options) { - // Sort the list alphabetically by group label. - array_multisort($group_options, SORT_ASC, SORT_NATURAL); - } - - // Make sure that the 'Content' group is situated at the top. - $content = $this->t('Content', array(), array('context' => 'Entity type group')); - $options = array((string) $content => $options[(string) $content]) + $options; - } - - return $options; + return $this->container->get('entity_type.repository')->getEntityTypeLabels($group); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getTranslationFromContext(EntityInterface $entity, $langcode = NULL, $context = array()) { - $translation = $entity; - - if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) { - if (empty($langcode)) { - $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); - $entity->addCacheContexts(['languages:' . LanguageInterface::TYPE_CONTENT]); - } - - // Retrieve language fallback candidates to perform the entity language - // negotiation, unless the current translation is already the desired one. - if ($entity->language()->getId() != $langcode) { - $context['data'] = $entity; - $context += array('operation' => 'entity_view', 'langcode' => $langcode); - $candidates = $this->languageManager->getFallbackCandidates($context); - - // Ensure the default language has the proper language code. - $default_language = $entity->getUntranslated()->language(); - $candidates[$default_language->getId()] = LanguageInterface::LANGCODE_DEFAULT; - - // Return the most fitting entity translation. - foreach ($candidates as $candidate) { - if ($entity->hasTranslation($candidate)) { - $translation = $entity->getTranslation($candidate); - break; - } - } - } - } - - return $translation; + return $this->container->get('entity.repository')->getTranslationFromContext($entity, $langcode, $context); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getAllViewModes() { - return $this->getAllDisplayModesByEntityType('view_mode'); + return $this->container->get('entity_display.repository')->getAllViewModes(); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getViewModes($entity_type_id) { - return $this->getDisplayModesByEntityType('view_mode', $entity_type_id); + return $this->container->get('entity_display.repository')->getViewModes($entity_type_id); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getAllFormModes() { - return $this->getAllDisplayModesByEntityType('form_mode'); + return $this->container->get('entity_display.repository')->getAllFormModes(); } /** * {@inheritdoc} - */ - public function getFormModes($entity_type_id) { - return $this->getDisplayModesByEntityType('form_mode', $entity_type_id); - } - - /** - * Gets the entity display mode info for all entity types. - * - * @param string $display_type - * The display type to be retrieved. It can be "view_mode" or "form_mode". - * - * @return array - * The display mode info for all entity types. - */ - protected function getAllDisplayModesByEntityType($display_type) { - if (!isset($this->displayModeInfo[$display_type])) { - $key = 'entity_' . $display_type . '_info'; - $entity_type_id = 'entity_' . $display_type; - $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_INTERFACE)->getId(); - if ($cache = $this->cacheGet("$key:$langcode")) { - $this->displayModeInfo[$display_type] = $cache->data; - } - else { - $this->displayModeInfo[$display_type] = array(); - foreach ($this->getStorage($entity_type_id)->loadMultiple() as $display_mode) { - list($display_mode_entity_type, $display_mode_name) = explode('.', $display_mode->id(), 2); - $this->displayModeInfo[$display_type][$display_mode_entity_type][$display_mode_name] = $display_mode->toArray(); - } - $this->moduleHandler->alter($key, $this->displayModeInfo[$display_type]); - $this->cacheSet("$key:$langcode", $this->displayModeInfo[$display_type], CacheBackendInterface::CACHE_PERMANENT, array('entity_types', 'entity_field_info')); - } - } - - return $this->displayModeInfo[$display_type]; - } - - /** - * Gets the entity display mode info for a specific entity type. * - * @param string $display_type - * The display type to be retrieved. It can be "view_mode" or "form_mode". - * @param string $entity_type_id - * The entity type whose display mode info should be returned. - * - * @return array - * The display mode info for a specific entity type. + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - protected function getDisplayModesByEntityType($display_type, $entity_type_id) { - if (isset($this->displayModeInfo[$display_type][$entity_type_id])) { - return $this->displayModeInfo[$display_type][$entity_type_id]; - } - else { - $display_modes = $this->getAllDisplayModesByEntityType($display_type); - if (isset($display_modes[$entity_type_id])) { - return $display_modes[$entity_type_id]; - } - } - return array(); + public function getFormModes($entity_type_id) { + return $this->container->get('entity_display.repository')->getFormModes($entity_type_id); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getViewModeOptions($entity_type_id) { - return $this->getDisplayModeOptions('view_mode', $entity_type_id); + return $this->container->get('entity_display.repository')->getViewModeOptions($entity_type_id); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getFormModeOptions($entity_type_id) { - return $this->getDisplayModeOptions('form_mode', $entity_type_id); + return $this->container->get('entity_display.repository')->getFormModeOptions($entity_type_id); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getViewModeOptionsByBundle($entity_type_id, $bundle) { - return $this->getDisplayModeOptionsByBundle('view_mode', $entity_type_id, $bundle); + return $this->container->get('entity_display.repository')->getViewModeOptionsByBundle($entity_type_id, $bundle); } /** * {@inheritdoc} - */ - public function getFormModeOptionsByBundle($entity_type_id, $bundle) { - return $this->getDisplayModeOptionsByBundle('form_mode', $entity_type_id, $bundle); - } - - /** - * Gets an array of display mode options. * - * @param string $display_type - * The display type to be retrieved. It can be "view_mode" or "form_mode". - * @param string $entity_type_id - * The entity type whose display mode options should be returned. - * - * @return array - * An array of display mode labels, keyed by the display mode ID. + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - protected function getDisplayModeOptions($display_type, $entity_type_id) { - $options = array('default' => t('Default')); - foreach ($this->getDisplayModesByEntityType($display_type, $entity_type_id) as $mode => $settings) { - $options[$mode] = $settings['label']; - } - return $options; + public function getFormModeOptionsByBundle($entity_type_id, $bundle) { + return $this->container->get('entity_display.repository')->getFormModeOptionsByBundle($entity_type_id, $bundle); } /** - * Returns an array of enabled display mode options by bundle. - * - * @param $display_type - * The display type to be retrieved. It can be "view_mode" or "form_mode". - * @param string $entity_type_id - * The entity type whose display mode options should be returned. - * @param string $bundle - * The name of the bundle. + * {@inheritdoc} * - * @return array - * An array of display mode labels, keyed by the display mode ID. + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - protected function getDisplayModeOptionsByBundle($display_type, $entity_type_id, $bundle) { - // Collect all the entity's display modes. - $options = $this->getDisplayModeOptions($display_type, $entity_type_id); - - // Filter out modes for which the entity display is disabled - // (or non-existent). - $load_ids = array(); - // Get the list of available entity displays for the current bundle. - foreach (array_keys($options) as $mode) { - $load_ids[] = $entity_type_id . '.' . $bundle . '.' . $mode; - } - - // Load the corresponding displays. - $displays = $this->getStorage($display_type == 'form_mode' ? 'entity_form_display' : 'entity_view_display') - ->loadMultiple($load_ids); - - // Unset the display modes that are not active or do not exist. - foreach (array_keys($options) as $mode) { - $display_id = $entity_type_id . '.' . $bundle . '.' . $mode; - if (!isset($displays[$display_id]) || !$displays[$display_id]->status()) { - unset($options[$mode]); - } - } - - return $options; + public function clearDisplayModeInfo() { + $this->container->get('entity_display.repository')->clearDisplayModeInfo(); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function loadEntityByUuid($entity_type_id, $uuid) { - $entity_type = $this->getDefinition($entity_type_id); - - if (!$uuid_key = $entity_type->getKey('uuid')) { - throw new EntityStorageException("Entity type $entity_type_id does not support UUIDs."); - } - - $entities = $this->getStorage($entity_type_id)->loadByProperties(array($uuid_key => $uuid)); - - return reset($entities); + return $this->container->get('entity.repository')->loadEntityByUuid($entity_type_id, $uuid); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function loadEntityByConfigTarget($entity_type_id, $target) { - $entity_type = $this->getDefinition($entity_type_id); - - // For configuration entities, the config target is given by the entity ID. - // @todo Consider adding a method to allow entity types to indicate the - // target identifier key rather than hard-coding this check. Issue: - // https://www.drupal.org/node/2412983. - if ($entity_type instanceof ConfigEntityType) { - $entity = $this->getStorage($entity_type_id)->load($target); - } - - // For content entities, the config target is given by the UUID. - else { - $entity = $this->loadEntityByUuid($entity_type_id, $target); - } - - return $entity; + return $this->container->get('entity.repository')->loadEntityByConfigTarget($entity_type_id, $target); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getEntityTypeFromClass($class_name) { - - // Check the already calculated classes first. - if (isset($this->classNameEntityTypeMap[$class_name])) { - return $this->classNameEntityTypeMap[$class_name]; - } - - $same_class = 0; - $entity_type_id = NULL; - foreach ($this->getDefinitions() as $entity_type) { - if ($entity_type->getOriginalClass() == $class_name || $entity_type->getClass() == $class_name) { - $entity_type_id = $entity_type->id(); - if ($same_class++) { - throw new AmbiguousEntityClassException($class_name); - } - } - } - - // Return the matching entity type ID if there is one. - if ($entity_type_id) { - $this->classNameEntityTypeMap[$class_name] = $entity_type_id; - return $entity_type_id; - } - - throw new NoCorrespondingEntityClassException($class_name); + return $this->container->get('entity_type.repository')->getEntityTypeFromClass($class_name); } /** * {@inheritdoc} */ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { - $entity_type_id = $entity_type->id(); - - // @todo Forward this to all interested handlers, not only storage, once - // iterating handlers is possible: https://www.drupal.org/node/2332857. - $storage = $this->getStorage($entity_type_id); - if ($storage instanceof EntityTypeListenerInterface) { - $storage->onEntityTypeCreate($entity_type); - } - - $this->eventDispatcher->dispatch(EntityTypeEvents::CREATE, new EntityTypeEvent($entity_type)); - - $this->setLastInstalledDefinition($entity_type); - if ($entity_type->isSubclassOf(FieldableEntityInterface::class)) { - $this->setLastInstalledFieldStorageDefinitions($entity_type_id, $this->getFieldStorageDefinitions($entity_type_id)); - } + $this->container->get('entity_type.listener')->onEntityTypeCreate($entity_type); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) { - $entity_type_id = $entity_type->id(); - - // @todo Forward this to all interested handlers, not only storage, once - // iterating handlers is possible: https://www.drupal.org/node/2332857. - $storage = $this->getStorage($entity_type_id); - if ($storage instanceof EntityTypeListenerInterface) { - $storage->onEntityTypeUpdate($entity_type, $original); - } - - $this->eventDispatcher->dispatch(EntityTypeEvents::UPDATE, new EntityTypeEvent($entity_type, $original)); - - $this->setLastInstalledDefinition($entity_type); + $this->container->get('entity_type.listener')->onEntityTypeUpdate($entity_type, $original); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { - $entity_type_id = $entity_type->id(); - - // @todo Forward this to all interested handlers, not only storage, once - // iterating handlers is possible: https://www.drupal.org/node/2332857. - $storage = $this->getStorage($entity_type_id); - if ($storage instanceof EntityTypeListenerInterface) { - $storage->onEntityTypeDelete($entity_type); - } - - $this->eventDispatcher->dispatch(EntityTypeEvents::DELETE, new EntityTypeEvent($entity_type)); - - $this->deleteLastInstalledDefinition($entity_type_id); + $this->container->get('entity_type.listener')->onEntityTypeDelete($entity_type); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { - $entity_type_id = $storage_definition->getTargetEntityTypeId(); - - // @todo Forward this to all interested handlers, not only storage, once - // iterating handlers is possible: https://www.drupal.org/node/2332857. - $storage = $this->getStorage($entity_type_id); - if ($storage instanceof FieldStorageDefinitionListenerInterface) { - $storage->onFieldStorageDefinitionCreate($storage_definition); - } - - $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::CREATE, new FieldStorageDefinitionEvent($storage_definition)); - - $this->setLastInstalledFieldStorageDefinition($storage_definition); - $this->clearCachedFieldDefinitions(); + $this->container->get('field_storage_definition.listener')->onFieldStorageDefinitionCreate($storage_definition); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { - $entity_type_id = $storage_definition->getTargetEntityTypeId(); - - // @todo Forward this to all interested handlers, not only storage, once - // iterating handlers is possible: https://www.drupal.org/node/2332857. - $storage = $this->getStorage($entity_type_id); - if ($storage instanceof FieldStorageDefinitionListenerInterface) { - $storage->onFieldStorageDefinitionUpdate($storage_definition, $original); - } - - $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::UPDATE, new FieldStorageDefinitionEvent($storage_definition, $original)); - - $this->setLastInstalledFieldStorageDefinition($storage_definition); - $this->clearCachedFieldDefinitions(); + $this->container->get('field_storage_definition.listener')->onFieldStorageDefinitionUpdate($storage_definition, $original); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { - $entity_type_id = $storage_definition->getTargetEntityTypeId(); - - // @todo Forward this to all interested handlers, not only storage, once - // iterating handlers is possible: https://www.drupal.org/node/2332857. - $storage = $this->getStorage($entity_type_id); - if ($storage instanceof FieldStorageDefinitionListenerInterface) { - $storage->onFieldStorageDefinitionDelete($storage_definition); - } - - $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::DELETE, new FieldStorageDefinitionEvent($storage_definition)); - - $this->deleteLastInstalledFieldStorageDefinition($storage_definition); - $this->clearCachedFieldDefinitions(); + $this->container->get('field_storage_definition.listener')->onFieldStorageDefinitionDelete($storage_definition); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function onBundleCreate($bundle, $entity_type_id) { - $this->clearCachedBundles(); - // Notify the entity storage. - $storage = $this->getStorage($entity_type_id); - if ($storage instanceof EntityBundleListenerInterface) { - $storage->onBundleCreate($bundle, $entity_type_id); - } - // Invoke hook_entity_bundle_create() hook. - $this->moduleHandler->invokeAll('entity_bundle_create', array($entity_type_id, $bundle)); + $this->container->get('entity_bundle.listener')->onBundleCreate($bundle, $entity_type_id); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function onBundleDelete($bundle, $entity_type_id) { - $this->clearCachedBundles(); - // Notify the entity storage. - $storage = $this->getStorage($entity_type_id); - if ($storage instanceof EntityBundleListenerInterface) { - $storage->onBundleDelete($bundle, $entity_type_id); - } - // Invoke hook_entity_bundle_delete() hook. - $this->moduleHandler->invokeAll('entity_bundle_delete', array($entity_type_id, $bundle)); - $this->clearCachedFieldDefinitions(); + $this->container->get('entity_bundle.listener')->onBundleDelete($bundle, $entity_type_id); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getLastInstalledDefinition($entity_type_id) { - return $this->keyValueFactory->get('entity.definitions.installed')->get($entity_type_id . '.entity_type'); + return $this->container->get('entity.last_installed_schema.repository')->getLastInstalledDefinition($entity_type_id); } /** * {@inheritdoc} */ public function useCaches($use_caches = FALSE) { - parent::useCaches($use_caches); - if (!$use_caches) { - $this->handlers = []; - $this->fieldDefinitions = []; - $this->baseFieldDefinitions = []; - $this->fieldStorageDefinitions = []; - } - } + $this->container->get('entity_type.manager')->useCaches($use_caches); - /** - * Stores the entity type definition in the application state. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type definition. - */ - protected function setLastInstalledDefinition(EntityTypeInterface $entity_type) { - $entity_type_id = $entity_type->id(); - $this->keyValueFactory->get('entity.definitions.installed')->set($entity_type_id . '.entity_type', $entity_type); + // @todo EntityFieldManager is not a plugin manager, and should not co-opt + // this method for managing its caches. + $this->container->get('entity_field.manager')->useCaches($use_caches); } /** - * Deletes the entity type definition from the application state. + * {@inheritdoc} * - * @param string $entity_type_id - * The entity type definition identifier. + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - protected function deleteLastInstalledDefinition($entity_type_id) { - $this->keyValueFactory->get('entity.definitions.installed')->delete($entity_type_id . '.entity_type'); - // Clean up field storage definitions as well. Even if the entity type - // isn't currently fieldable, there might be legacy definitions or an - // empty array stored from when it was. - $this->keyValueFactory->get('entity.definitions.installed')->delete($entity_type_id . '.field_storage_definitions'); + public function getLastInstalledFieldStorageDefinitions($entity_type_id) { + return $this->container->get('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions($entity_type_id); } /** * {@inheritdoc} + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - public function getLastInstalledFieldStorageDefinitions($entity_type_id) { - return $this->keyValueFactory->get('entity.definitions.installed')->get($entity_type_id . '.field_storage_definitions', array()); + public function getDefinitions() { + return $this->container->get('entity_type.manager')->getDefinitions(); } /** - * Stores the entity type's field storage definitions in the application state. + * {@inheritdoc} * - * @param string $entity_type_id - * The entity type identifier. - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions - * An array of field storage definitions. + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - protected function setLastInstalledFieldStorageDefinitions($entity_type_id, array $storage_definitions) { - $this->keyValueFactory->get('entity.definitions.installed')->set($entity_type_id . '.field_storage_definitions', $storage_definitions); + public function hasDefinition($plugin_id) { + return $this->container->get('entity_type.manager')->hasDefinition($plugin_id); } /** - * Stores the field storage definition in the application state. + * {@inheritdoc} * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - protected function setLastInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { - $entity_type_id = $storage_definition->getTargetEntityTypeId(); - $definitions = $this->getLastInstalledFieldStorageDefinitions($entity_type_id); - $definitions[$storage_definition->getName()] = $storage_definition; - $this->setLastInstalledFieldStorageDefinitions($entity_type_id, $definitions); + public function createInstance($plugin_id, array $configuration = []) { + return $this->container->get('entity_type.manager')->createInstance($plugin_id, $configuration); } /** - * Deletes the field storage definition from the application state. + * {@inheritdoc} * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field storage definition. + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - protected function deleteLastInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { - $entity_type_id = $storage_definition->getTargetEntityTypeId(); - $definitions = $this->getLastInstalledFieldStorageDefinitions($entity_type_id); - unset($definitions[$storage_definition->getName()]); - $this->setLastInstalledFieldStorageDefinitions($entity_type_id, $definitions); + public function getInstance(array $options) { + return $this->container->get('entity_type.manager')->getInstance($options); } } diff --git a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php index edce82c7642a4778ba0b9345df4929d21fd06c0d..f837f7dfee68c3ecf84925e00ada6662caced7e9 100644 --- a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php @@ -7,534 +7,28 @@ namespace Drupal\Core\Entity; -use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface; -use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Field\FieldDefinitionListenerInterface; use Drupal\Core\Field\FieldStorageDefinitionListenerInterface; /** * Provides an interface for entity type managers. + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ -interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListenerInterface, EntityBundleListenerInterface, FieldStorageDefinitionListenerInterface, FieldDefinitionListenerInterface, CachedDiscoveryInterface { +interface EntityManagerInterface extends EntityTypeListenerInterface, EntityBundleListenerInterface, FieldStorageDefinitionListenerInterface, FieldDefinitionListenerInterface, EntityTypeManagerInterface, EntityTypeRepositoryInterface, EntityTypeBundleInfoInterface, EntityDisplayRepositoryInterface, EntityFieldManagerInterface, EntityRepositoryInterface { /** - * Builds a list of entity type labels suitable for a Form API options list. + * @see \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface::getLastInstalledDefinition() * - * @param bool $group - * (optional) Whether to group entity types by plugin group (e.g. 'content', - * 'config'). Defaults to FALSE. - * - * @return array - * An array of entity type labels, keyed by entity type name. - */ - public function getEntityTypeLabels($group = FALSE); - - /** - * Gets the base field definitions for a content entity type. - * - * Only fields that are not specific to a given bundle or set of bundles are - * returned. This excludes configurable fields, as they are always attached - * to a specific bundle. - * - * @param string $entity_type_id - * The entity type ID. Only entity types that implement - * \Drupal\Core\Entity\FieldableEntityInterface are supported. - * - * @return \Drupal\Core\Field\FieldDefinitionInterface[] - * The array of base field definitions for the entity type, keyed by field - * name. - * - * @throws \LogicException - * Thrown if one of the entity keys is flagged as translatable. - */ - public function getBaseFieldDefinitions($entity_type_id); - - /** - * Gets the field definitions for a specific bundle. - * - * @param string $entity_type_id - * The entity type ID. Only entity types that implement - * \Drupal\Core\Entity\FieldableEntityInterface are supported. - * @param string $bundle - * The bundle. - * - * @return \Drupal\Core\Field\FieldDefinitionInterface[] - * The array of field definitions for the bundle, keyed by field name. - */ - public function getFieldDefinitions($entity_type_id, $bundle); - - /** - * Gets the field storage definitions for a content entity type. - * - * This returns all field storage definitions for base fields and bundle - * fields of an entity type. Note that field storage definitions of a base - * field equal the full base field definition (i.e. they implement - * FieldDefinitionInterface), while the storage definitions for bundle fields - * may implement FieldStorageDefinitionInterface only. - * - * @param string $entity_type_id - * The entity type ID. Only content entities are supported. - * - * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] - * The array of field storage definitions for the entity type, keyed by - * field name. - * - * @see \Drupal\Core\Field\FieldStorageDefinitionInterface - */ - public function getFieldStorageDefinitions($entity_type_id); - - /** - * Gets the entity type's most recently installed field storage definitions. - * - * During the application lifetime, field storage definitions can change. For - * example, updated code can be deployed. The getFieldStorageDefinitions() - * method will always return the definitions as determined by the current - * codebase. This method, however, returns what the definitions were when the - * last time that one of the - * \Drupal\Core\Field\FieldStorageDefinitionListenerInterface events was last - * fired and completed successfully. In other words, the definitions that - * the entity type's handlers have incorporated into the application state. - * For example, if the entity type's storage handler is SQL-based, the - * definitions for which database tables were created. - * - * Application management code can check if getFieldStorageDefinitions() - * differs from getLastInstalledFieldStorageDefinitions() and decide whether - * to: - * - Invoke the appropriate - * \Drupal\Core\Field\FieldStorageDefinitionListenerInterface - * events so that handlers react to the new definitions. - * - Raise a warning that the application state is incompatible with the - * codebase. - * - Perform some other action. - * - * @param string $entity_type_id - * The entity type ID. - * - * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] - * The array of installed field storage definitions for the entity type, - * keyed by field name. - * - * @see \Drupal\Core\Entity\EntityTypeListenerInterface - */ - public function getLastInstalledFieldStorageDefinitions($entity_type_id); - - /** - * Gets a lightweight map of fields across bundles. - * - * @return array - * An array keyed by entity type. Each value is an array which keys are - * field names and value is an array with two entries: - * - type: The field type. - * - bundles: The bundles in which the field appears. - */ - public function getFieldMap(); - - /** - * Gets a lightweight map of fields across bundles filtered by field type. - * - * @param string $field_type - * The field type to filter by. - * - * @return array - * An array keyed by entity type. Each value is an array which keys are - * field names and value is an array with two entries: - * - type: The field type. - * - bundles: The bundles in which the field appears. - */ - public function getFieldMapByFieldType($field_type); - - /** - * Creates a new access control handler instance. - * - * @param string $entity_type - * The entity type for this access control handler. - * - * @return \Drupal\Core\Entity\EntityAccessControlHandlerInterface. - * A access control handler instance. - */ - public function getAccessControlHandler($entity_type); - - /** - * Creates a new storage instance. - * - * @param string $entity_type - * The entity type for this storage. - * - * @return \Drupal\Core\Entity\EntityStorageInterface - * A storage instance. - * - * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - */ - public function getStorage($entity_type); - - /** - * Get the bundle info of all entity types. - * - * @return array - * An array of all bundle information. - */ - public function getAllBundleInfo(); - - /** - * {@inheritdoc} - */ - public function clearCachedDefinitions(); - - /** - * Clears static and persistent field definition caches. - */ - public function clearCachedFieldDefinitions(); - - /** - * Clears static and persistent bundles. - */ - public function clearCachedBundles(); - - /** - * Creates a new view builder instance. - * - * @param string $entity_type - * The entity type for this view builder. - * - * @return \Drupal\Core\Entity\EntityViewBuilderInterface. - * A view builder instance. - */ - public function getViewBuilder($entity_type); - - /** - * Creates a new entity list builder. - * - * @param string $entity_type - * The entity type for this list builder. - * - * @return \Drupal\Core\Entity\EntityListBuilderInterface - * An entity list builder instance. - */ - public function getListBuilder($entity_type); - - /** - * Creates a new form instance. - * - * @param string $entity_type - * The entity type for this form. - * @param string $operation - * The name of the operation to use, e.g., 'default'. - * - * @return \Drupal\Core\Entity\EntityFormInterface - * A form instance. - */ - public function getFormObject($entity_type, $operation); - - /** - * Gets all route provider instances. - * - * @param string $entity_type - * The entity type for this route providers. - * - * @return \Drupal\Core\Entity\Routing\EntityRouteProviderInterface[] - */ - public function getRouteProviders($entity_type); - - /** - * Checks whether a certain entity type has a certain handler. - * - * @param string $entity_type - * The name of the entity type. - * @param string $handler_type - * The name of the handler. - * - * @return bool - * Returns TRUE if the entity type has the handler, else FALSE. - */ - public function hasHandler($entity_type, $handler_type); - - /** - * Creates a new handler instance for a entity type and handler type. - * - * @param string $entity_type - * The entity type for this handler. - * @param string $handler_type - * The handler type to create an instance for. - * - * @return object - * A handler instance. - * - * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - */ - public function getHandler($entity_type, $handler_type); - - /** - * Creates new handler instance. - * - * Usually \Drupal\Core\Entity\EntityManagerInterface::getHandler() is - * preferred since that method has additional checking that the class exists - * and has static caches. - * - * @param mixed $class - * The handler class to instantiate. - * @param \Drupal\Core\Entity\EntityTypeInterface $definition - * The entity type definition. - * - * @return object - * A handler instance. - */ - public function createHandlerInstance($class, EntityTypeInterface $definition = null); - - /** - * Gets the bundle info of an entity type. - * - * @param string $entity_type - * The entity type. - * - * @return array - * Returns the bundle information for the specified entity type. - */ - public function getBundleInfo($entity_type); - - /** - * Gets the "extra fields" for a bundle. - * - * @param string $entity_type_id - * The entity type ID. - * @param string $bundle - * The bundle name. - * - * @return array - * A nested array of 'pseudo-field' elements. Each list is nested within the - * following keys: entity type, bundle name, context (either 'form' or - * 'display'). The keys are the name of the elements as appearing in the - * renderable array (either the entity form or the displayed entity). The - * value is an associative array: - * - label: The human readable name of the element. Make sure you sanitize - * this appropriately. - * - description: A short description of the element contents. - * - weight: The default weight of the element. - * - visible: (optional) The default visibility of the element. Defaults to - * TRUE. - * - edit: (optional) String containing markup (normally a link) used as the - * element's 'edit' operation in the administration interface. Only for - * 'form' context. - * - delete: (optional) String containing markup (normally a link) used as the - * element's 'delete' operation in the administration interface. Only for - * 'form' context. - */ - public function getExtraFields($entity_type_id, $bundle); - - /** - * Gets the entity translation to be used in the given context. - * - * This will check whether a translation for the desired language is available - * and if not, it will fall back to the most appropriate translation based on - * the provided context. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity whose translation will be returned. - * @param string $langcode - * (optional) The language of the current context. Defaults to the current - * content language. - * @param array $context - * (optional) An associative array of arbitrary data that can be useful to - * determine the proper fallback sequence. - * - * @return \Drupal\Core\Entity\EntityInterface - * An entity object for the translated data. - * - * @see \Drupal\Core\Language\LanguageManagerInterface::getFallbackCandidates() - */ - public function getTranslationFromContext(EntityInterface $entity, $langcode = NULL, $context = array()); - - /** - * {@inheritdoc} - * - * @return \Drupal\Core\Entity\EntityTypeInterface|null - */ - public function getDefinition($entity_type_id, $exception_on_invalid = TRUE); - - /** - * Gets the entity type definition in its most recently installed state. - * - * During the application lifetime, entity type definitions can change. For - * example, updated code can be deployed. The getDefinition() method will - * always return the definition as determined by the current codebase. This - * method, however, returns what the definition was when the last time that - * one of the \Drupal\Core\Entity\EntityTypeListenerInterface events was last - * fired and completed successfully. In other words, the definition that - * the entity type's handlers have incorporated into the application state. - * For example, if the entity type's storage handler is SQL-based, the - * definition for which database tables were created. - * - * Application management code can check if getDefinition() differs from - * getLastInstalledDefinition() and decide whether to: - * - Invoke the appropriate \Drupal\Core\Entity\EntityTypeListenerInterface - * event so that handlers react to the new definition. - * - Raise a warning that the application state is incompatible with the - * codebase. - * - Perform some other action. - * - * @param string $entity_type_id - * The entity type ID. - * - * @return \Drupal\Core\Entity\EntityTypeInterface|null - * The installed entity type definition, or NULL if the entity type has - * not yet been installed via onEntityTypeCreate(). - * - * @see \Drupal\Core\Entity\EntityTypeListenerInterface + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ public function getLastInstalledDefinition($entity_type_id); /** - * {@inheritdoc} - * - * @return \Drupal\Core\Entity\EntityTypeInterface[] - */ - public function getDefinitions(); - - /** - * Gets the entity view mode info for all entity types. - * - * @return array - * The view mode info for all entity types. - */ - public function getAllViewModes(); - - /** - * Gets the entity view mode info for a specific entity type. - * - * @param string $entity_type_id - * The entity type whose view mode info should be returned. - * - * @return array - * The view mode info for a specific entity type. - */ - public function getViewModes($entity_type_id); - - /** - * Gets the entity form mode info for all entity types. - * - * @return array - * The form mode info for all entity types. - */ - public function getAllFormModes(); - - /** - * Gets the entity form mode info for a specific entity type. - * - * @param string $entity_type_id - * The entity type whose form mode info should be returned. - * - * @return array - * The form mode info for a specific entity type. - */ - public function getFormModes($entity_type_id); - - /** - * Gets an array of view mode options. - * - * @param string $entity_type_id - * The entity type whose view mode options should be returned. - * - * @return array - * An array of view mode labels, keyed by the display mode ID. - */ - public function getViewModeOptions($entity_type_id); - - /** - * Gets an array of form mode options. - * - * @param string $entity_type_id - * The entity type whose form mode options should be returned. - * - * @return array - * An array of form mode labels, keyed by the display mode ID. - */ - public function getFormModeOptions($entity_type_id); - - /** - * Returns an array of enabled view mode options by bundle. - * - * @param string $entity_type_id - * The entity type whose view mode options should be returned. - * @param string $bundle - * The name of the bundle. - * - * @return array - * An array of view mode labels, keyed by the display mode ID. - */ - public function getViewModeOptionsByBundle($entity_type_id, $bundle); - - /** - * Returns an array of enabled form mode options by bundle. - * - * @param string $entity_type_id - * The entity type whose form mode options should be returned. - * @param string $bundle - * The name of the bundle. + * @see \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface::getLastInstalledFieldStorageDefinitions() * - * @return array - * An array of form mode labels, keyed by the display mode ID. + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. */ - public function getFormModeOptionsByBundle($entity_type_id, $bundle); - - /** - * Loads an entity by UUID. - * - * Note that some entity types may not support UUIDs. - * - * @param string $entity_type_id - * The entity type ID to load from. - * @param string $uuid - * The UUID of the entity to load. - * - * @return \Drupal\Core\Entity\EntityInterface|null - * The entity object, or NULL if there is no entity with the given UUID. - * - * @throws \Drupal\Core\Entity\EntityStorageException - * Thrown in case the requested entity type does not support UUIDs. - */ - public function loadEntityByUuid($entity_type_id, $uuid); - - /** - * Loads an entity by the config target identifier. - * - * @param string $entity_type_id - * The entity type ID to load from. - * @param string $target - * The configuration target to load, as returned from - * \Drupal\Core\Entity\EntityInterface::getConfigTarget(). - * - * @return \Drupal\Core\Entity\EntityInterface|null - * The entity object, or NULL if there is no entity with the given config - * target identifier. - * - * @throws \Drupal\Core\Entity\EntityStorageException - * Thrown if the target identifier is a UUID but the entity type does not - * support UUIDs. - * - * @see \Drupal\Core\Entity\EntityInterface::getConfigTarget() - */ - public function loadEntityByConfigTarget($entity_type_id, $target); - - /** - * Gets the entity type ID based on the class that is called on. - * - * Compares the class this is called on against the known entity classes - * and returns the entity type ID of a direct match or a subclass as fallback, - * to support entity type definitions that were altered. - * - * @param string $class_name - * Class name to use for searching the entity type ID. - * - * @return string - * The entity type ID. - * - * @throws \Drupal\Core\Entity\Exception\AmbiguousEntityClassException - * Thrown when multiple subclasses correspond to the called class. - * @throws \Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException - * Thrown when no entity class corresponds to the called class. - * - * @see \Drupal\Core\Entity\Entity::load() - * @see \Drupal\Core\Entity\Entity::loadMultiple() - */ - public function getEntityTypeFromClass($class_name); + public function getLastInstalledFieldStorageDefinitions($entity_type_id); } diff --git a/core/lib/Drupal/Core/Entity/EntityRepository.php b/core/lib/Drupal/Core/Entity/EntityRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..58381d1a2691abbabf3b1689ef09ce120bd51e6f --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityRepository.php @@ -0,0 +1,120 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityRepository. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\TypedData\TranslatableInterface; + +/** + * Provides several mechanisms for retrieving entities. + */ +class EntityRepository implements EntityRepositoryInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * Constructs a new EntityRepository. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager) { + $this->entityTypeManager = $entity_type_manager; + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function loadEntityByUuid($entity_type_id, $uuid) { + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + + if (!$uuid_key = $entity_type->getKey('uuid')) { + throw new EntityStorageException("Entity type $entity_type_id does not support UUIDs."); + } + + $entities = $this->entityTypeManager->getStorage($entity_type_id)->loadByProperties([$uuid_key => $uuid]); + + return reset($entities); + } + + /** + * {@inheritdoc} + */ + public function loadEntityByConfigTarget($entity_type_id, $target) { + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + + // For configuration entities, the config target is given by the entity ID. + // @todo Consider adding a method to allow entity types to indicate the + // target identifier key rather than hard-coding this check. Issue: + // https://www.drupal.org/node/2412983. + if ($entity_type instanceof ConfigEntityTypeInterface) { + $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($target); + } + + // For content entities, the config target is given by the UUID. + else { + $entity = $this->loadEntityByUuid($entity_type_id, $target); + } + + return $entity; + } + + /** + * {@inheritdoc} + */ + public function getTranslationFromContext(EntityInterface $entity, $langcode = NULL, $context = array()) { + $translation = $entity; + + if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) { + if (empty($langcode)) { + $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); + $entity->addCacheContexts(['languages:' . LanguageInterface::TYPE_CONTENT]); + } + + // Retrieve language fallback candidates to perform the entity language + // negotiation, unless the current translation is already the desired one. + if ($entity->language()->getId() != $langcode) { + $context['data'] = $entity; + $context += array('operation' => 'entity_view', 'langcode' => $langcode); + $candidates = $this->languageManager->getFallbackCandidates($context); + + // Ensure the default language has the proper language code. + $default_language = $entity->getUntranslated()->language(); + $candidates[$default_language->getId()] = LanguageInterface::LANGCODE_DEFAULT; + + // Return the most fitting entity translation. + foreach ($candidates as $candidate) { + if ($entity->hasTranslation($candidate)) { + $translation = $entity->getTranslation($candidate); + break; + } + } + } + } + + return $translation; + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityRepositoryInterface.php b/core/lib/Drupal/Core/Entity/EntityRepositoryInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..0946ee9495ee69fa2d67d5f55b622a38d75898f1 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityRepositoryInterface.php @@ -0,0 +1,77 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityRepositoryInterface. + */ + +namespace Drupal\Core\Entity; + +/** + * Provides an interface for an entity repository. + */ +interface EntityRepositoryInterface { + + /** + * Loads an entity by UUID. + * + * Note that some entity types may not support UUIDs. + * + * @param string $entity_type_id + * The entity type ID to load from. + * @param string $uuid + * The UUID of the entity to load. + * + * @return \Drupal\Core\Entity\EntityInterface|null + * The entity object, or NULL if there is no entity with the given UUID. + * + * @throws \Drupal\Core\Entity\EntityStorageException + * Thrown in case the requested entity type does not support UUIDs. + */ + public function loadEntityByUuid($entity_type_id, $uuid); + + /** + * Loads an entity by the config target identifier. + * + * @param string $entity_type_id + * The entity type ID to load from. + * @param string $target + * The configuration target to load, as returned from + * \Drupal\Core\Entity\EntityInterface::getConfigTarget(). + * + * @return \Drupal\Core\Entity\EntityInterface|null + * The entity object, or NULL if there is no entity with the given config + * target identifier. + * + * @throws \Drupal\Core\Entity\EntityStorageException + * Thrown if the target identifier is a UUID but the entity type does not + * support UUIDs. + * + * @see \Drupal\Core\Entity\EntityInterface::getConfigTarget() + */ + public function loadEntityByConfigTarget($entity_type_id, $target); + + /** + * Gets the entity translation to be used in the given context. + * + * This will check whether a translation for the desired language is available + * and if not, it will fall back to the most appropriate translation based on + * the provided context. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity whose translation will be returned. + * @param string $langcode + * (optional) The language of the current context. Defaults to the current + * content language. + * @param array $context + * (optional) An associative array of arbitrary data that can be useful to + * determine the proper fallback sequence. + * + * @return \Drupal\Core\Entity\EntityInterface + * An entity object for the translated data. + * + * @see \Drupal\Core\Language\LanguageManagerInterface::getFallbackCandidates() + */ + public function getTranslationFromContext(EntityInterface $entity, $langcode = NULL, $context = array()); + +} diff --git a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php new file mode 100644 index 0000000000000000000000000000000000000000..1e37789c842650aa245ad36fab9306d8014c2d6c --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php @@ -0,0 +1,133 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityTypeBundleInfo. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Cache\UseCacheBackendTrait; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\TypedData\TypedDataManagerInterface; + +/** + * Provides discovery and retrieval of entity type bundles. + */ +class EntityTypeBundleInfo implements EntityTypeBundleInfoInterface { + + use UseCacheBackendTrait; + + /** + * Static cache of bundle information. + * + * @var array + */ + protected $bundleInfo; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The typed data manager. + * + * @var \Drupal\Core\TypedData\TypedDataManagerInterface + */ + protected $typedDataManager; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a new EntityTypeBundleInfo. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager + * The typed data manager. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, TypedDataManagerInterface $typed_data_manager, CacheBackendInterface $cache_backend) { + $this->entityTypeManager = $entity_type_manager; + $this->languageManager = $language_manager; + $this->moduleHandler = $module_handler; + $this->typedDataManager = $typed_data_manager; + $this->cacheBackend = $cache_backend; + } + + /** + * {@inheritdoc} + */ + public function getBundleInfo($entity_type) { + $bundle_info = $this->getAllBundleInfo(); + return isset($bundle_info[$entity_type]) ? $bundle_info[$entity_type] : []; + } + + /** + * {@inheritdoc} + */ + public function getAllBundleInfo() { + if (empty($this->bundleInfo)) { + $langcode = $this->languageManager->getCurrentLanguage()->getId(); + if ($cache = $this->cacheGet("entity_bundle_info:$langcode")) { + $this->bundleInfo = $cache->data; + } + else { + $this->bundleInfo = $this->moduleHandler->invokeAll('entity_bundle_info'); + // First look for entity types that act as bundles for others, load them + // and add them as bundles. + foreach ($this->entityTypeManager->getDefinitions() as $type => $entity_type) { + if ($entity_type->getBundleOf()) { + foreach ($this->entityTypeManager->getStorage($type)->loadMultiple() as $entity) { + $this->bundleInfo[$entity_type->getBundleOf()][$entity->id()]['label'] = $entity->label(); + } + } + } + foreach ($this->entityTypeManager->getDefinitions() as $type => $entity_type) { + // If no bundles are provided, use the entity type name and label. + if (!isset($this->bundleInfo[$type])) { + $this->bundleInfo[$type][$type]['label'] = $entity_type->getLabel(); + } + } + $this->moduleHandler->alter('entity_bundle_info', $this->bundleInfo); + $this->cacheSet("entity_bundle_info:$langcode", $this->bundleInfo, Cache::PERMANENT, ['entity_types', 'entity_bundles']); + } + } + + return $this->bundleInfo; + } + + /** + * {@inheritdoc} + */ + public function clearCachedBundles() { + $this->bundleInfo = []; + Cache::invalidateTags(['entity_bundles']); + // Entity bundles are exposed as data types, clear that cache too. + $this->typedDataManager->clearCachedDefinitions(); + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..40b0c1feb55d6cbfbb0615a32a0467d707c8ff1e --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php @@ -0,0 +1,39 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityTypeBundleInfoInterface. + */ + +namespace Drupal\Core\Entity; + +/** + * Provides an interface for an entity type bundle info. + */ +interface EntityTypeBundleInfoInterface { + + /** + * Get the bundle info of all entity types. + * + * @return array + * An array of all bundle information. + */ + public function getAllBundleInfo(); + + /** + * Gets the bundle info of an entity type. + * + * @param string $entity_type + * The entity type. + * + * @return array + * Returns the bundle information for the specified entity type. + */ + public function getBundleInfo($entity_type); + + /** + * Clears static and persistent bundles. + */ + public function clearCachedBundles(); + +} diff --git a/core/lib/Drupal/Core/Entity/EntityTypeListener.php b/core/lib/Drupal/Core/Entity/EntityTypeListener.php new file mode 100644 index 0000000000000000000000000000000000000000..90e164c9e5fb9100d5d359444c79a780ab5a3cc0 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityTypeListener.php @@ -0,0 +1,123 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityTypeListener. + */ + +namespace Drupal\Core\Entity; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +/** + * Reacts to entity type CRUD on behalf of the Entity system. + * + * @see \Drupal\Core\Entity\EntityTypeEvents + */ +class EntityTypeListener implements EntityTypeListenerInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + + /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * The entity last installed schema repository. + * + * @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface + */ + protected $entityLastInstalledSchemaRepository; + + /** + * Constructs a new EntityTypeListener. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager + * The entity field manager. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher + * The event dispatcher. + * @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository + * The entity last installed schema repository. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, EventDispatcherInterface $event_dispatcher, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository) { + $this->entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->eventDispatcher = $event_dispatcher; + $this->entityLastInstalledSchemaRepository = $entity_last_installed_schema_repository; + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeCreate(EntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + + // @todo Forward this to all interested handlers, not only storage, once + // iterating handlers is possible: https://www.drupal.org/node/2332857. + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if ($storage instanceof EntityTypeListenerInterface) { + $storage->onEntityTypeCreate($entity_type); + } + + $this->eventDispatcher->dispatch(EntityTypeEvents::CREATE, new EntityTypeEvent($entity_type)); + + $this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type); + if ($entity_type->isSubclassOf(FieldableEntityInterface::class)) { + $this->entityLastInstalledSchemaRepository->setLastInstalledFieldStorageDefinitions($entity_type_id, $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id)); + } + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + $entity_type_id = $entity_type->id(); + + // @todo Forward this to all interested handlers, not only storage, once + // iterating handlers is possible: https://www.drupal.org/node/2332857. + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if ($storage instanceof EntityTypeListenerInterface) { + $storage->onEntityTypeUpdate($entity_type, $original); + } + + $this->eventDispatcher->dispatch(EntityTypeEvents::UPDATE, new EntityTypeEvent($entity_type, $original)); + + $this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type); + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDelete(EntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + + // @todo Forward this to all interested handlers, not only storage, once + // iterating handlers is possible: https://www.drupal.org/node/2332857. + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if ($storage instanceof EntityTypeListenerInterface) { + $storage->onEntityTypeDelete($entity_type); + } + + $this->eventDispatcher->dispatch(EntityTypeEvents::DELETE, new EntityTypeEvent($entity_type)); + + $this->entityLastInstalledSchemaRepository->deleteLastInstalledDefinition($entity_type_id); + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityTypeManager.php b/core/lib/Drupal/Core/Entity/EntityTypeManager.php new file mode 100644 index 0000000000000000000000000000000000000000..3a24e0e86f72d7d73b879162196640ba180bf210 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityTypeManager.php @@ -0,0 +1,266 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityTypeManager. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\DependencyInjection\ClassResolverInterface; +use Drupal\Core\Entity\Exception\InvalidLinkTemplateException; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery; +use Drupal\Core\StringTranslation\TranslationInterface; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerAwareTrait; + +/** + * Manages entity type plugin definitions. + * + * Each entity type definition array is set in the entity type's + * annotation and altered by hook_entity_type_alter(). + * + * @see \Drupal\Core\Entity\Annotation\EntityType + * @see \Drupal\Core\Entity\EntityInterface + * @see \Drupal\Core\Entity\EntityTypeInterface + * @see hook_entity_type_alter() + */ +class EntityTypeManager extends DefaultPluginManager implements EntityTypeManagerInterface, ContainerAwareInterface { + + use ContainerAwareTrait; + + /** + * Contains instantiated handlers keyed by handler type and entity type. + * + * @var array + */ + protected $handlers = []; + + /** + * The string translation service. + * + * @var \Drupal\Core\StringTranslation\TranslationInterface + */ + protected $stringTranslation; + + /** + * The class resolver. + * + * @var \Drupal\Core\DependencyInjection\ClassResolverInterface + */ + protected $classResolver; + + /** + * Constructs a new Entity plugin manager. + * + * @param \Traversable $namespaces + * An object that implements \Traversable which contains the root paths + * keyed by the corresponding namespace to look for plugin implementations, + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * The cache backend to use. + * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation + * The string translation. + * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver + * The class resolver. + */ + public function __construct(\Traversable $namespaces, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, TranslationInterface $string_translation, ClassResolverInterface $class_resolver) { + parent::__construct('Entity', $namespaces, $module_handler, 'Drupal\Core\Entity\EntityInterface'); + + $this->setCacheBackend($cache, 'entity_type', ['entity_types']); + $this->alterInfo('entity_type'); + + $this->discovery = new AnnotatedClassDiscovery('Entity', $namespaces, 'Drupal\Core\Entity\Annotation\EntityType'); + $this->stringTranslation = $string_translation; + $this->classResolver = $class_resolver; + } + + /** + * {@inheritdoc} + */ + public function processDefinition(&$definition, $plugin_id) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $definition */ + parent::processDefinition($definition, $plugin_id); + + // All link templates must have a leading slash. + foreach ((array) $definition->getLinkTemplates() as $link_relation_name => $link_template) { + if ($link_template[0] != '/') { + throw new InvalidLinkTemplateException("Link template '$link_relation_name' for entity type '$plugin_id' must start with a leading slash, the current link template is '$link_template'"); + } + } + } + + /** + * {@inheritdoc} + */ + protected function findDefinitions() { + $definitions = $this->getDiscovery()->getDefinitions(); + + // Directly call the hook implementations to pass the definitions to them + // by reference, so new entity types can be added. + foreach ($this->moduleHandler->getImplementations('entity_type_build') as $module) { + $function = $module . '_' . 'entity_type_build'; + $function($definitions); + } + foreach ($definitions as $plugin_id => $definition) { + $this->processDefinition($definition, $plugin_id); + } + $this->alterDefinitions($definitions); + + return $definitions; + } + + /** + * {@inheritdoc} + */ + public function getDefinition($entity_type_id, $exception_on_invalid = TRUE) { + if (($entity_type = parent::getDefinition($entity_type_id, FALSE)) && class_exists($entity_type->getClass())) { + return $entity_type; + } + elseif (!$exception_on_invalid) { + return NULL; + } + + throw new PluginNotFoundException($entity_type_id, sprintf('The "%s" entity type does not exist.', $entity_type_id)); + } + + /** + * {@inheritdoc} + */ + public function clearCachedDefinitions() { + parent::clearCachedDefinitions(); + $this->handlers = []; + } + + /** + * {@inheritdoc} + */ + public function useCaches($use_caches = FALSE) { + parent::useCaches($use_caches); + if (!$use_caches) { + $this->handlers = []; + } + } + + /** + * {@inheritdoc} + */ + public function hasHandler($entity_type, $handler_type) { + if ($definition = $this->getDefinition($entity_type, FALSE)) { + return $definition->hasHandlerClass($handler_type); + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getStorage($entity_type) { + return $this->getHandler($entity_type, 'storage'); + } + + /** + * {@inheritdoc} + */ + public function getListBuilder($entity_type) { + return $this->getHandler($entity_type, 'list_builder'); + } + + /** + * {@inheritdoc} + */ + public function getFormObject($entity_type, $operation) { + if (!isset($this->handlers['form'][$operation][$entity_type])) { + if (!$class = $this->getDefinition($entity_type, TRUE)->getFormClass($operation)) { + throw new InvalidPluginDefinitionException($entity_type, sprintf('The "%s" entity type did not specify a "%s" form class.', $entity_type, $operation)); + } + + $form_object = $this->classResolver->getInstanceFromDefinition($class); + + $form_object + ->setStringTranslation($this->stringTranslation) + ->setModuleHandler($this->moduleHandler) + ->setEntityTypeManager($this) + ->setOperation($operation) + // The entity manager cannot be injected due to a circular dependency. + // @todo Remove this set call in https://www.drupal.org/node/2603542. + ->setEntityManager(\Drupal::entityManager()); + $this->handlers['form'][$operation][$entity_type] = $form_object; + } + + return $this->handlers['form'][$operation][$entity_type]; + } + + /** + * {@inheritdoc} + */ + public function getRouteProviders($entity_type) { + if (!isset($this->handlers['route_provider'][$entity_type])) { + $route_provider_classes = $this->getDefinition($entity_type, TRUE)->getRouteProviderClasses(); + + foreach ($route_provider_classes as $type => $class) { + $this->handlers['route_provider'][$entity_type][$type] = $this->createHandlerInstance($class, $this->getDefinition($entity_type)); + } + } + + return isset($this->handlers['route_provider'][$entity_type]) ? $this->handlers['route_provider'][$entity_type] : []; + } + + /** + * {@inheritdoc} + */ + public function getViewBuilder($entity_type) { + return $this->getHandler($entity_type, 'view_builder'); + } + + /** + * {@inheritdoc} + */ + public function getAccessControlHandler($entity_type) { + return $this->getHandler($entity_type, 'access'); + } + + /** + * {@inheritdoc} + */ + public function getHandler($entity_type, $handler_type) { + if (!isset($this->handlers[$handler_type][$entity_type])) { + $definition = $this->getDefinition($entity_type); + $class = $definition->getHandlerClass($handler_type); + if (!$class) { + throw new InvalidPluginDefinitionException($entity_type, sprintf('The "%s" entity type did not specify a %s handler.', $entity_type, $handler_type)); + } + $this->handlers[$handler_type][$entity_type] = $this->createHandlerInstance($class, $definition); + } + + return $this->handlers[$handler_type][$entity_type]; + } + + /** + * {@inheritdoc} + */ + public function createHandlerInstance($class, EntityTypeInterface $definition = NULL) { + if (is_subclass_of($class, 'Drupal\Core\Entity\EntityHandlerInterface')) { + $handler = $class::createInstance($this->container, $definition); + } + else { + $handler = new $class($definition); + } + if (method_exists($handler, 'setModuleHandler')) { + $handler->setModuleHandler($this->moduleHandler); + } + if (method_exists($handler, 'setStringTranslation')) { + $handler->setStringTranslation($this->stringTranslation); + } + + return $handler; + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..c8242f4241145658a3c941077c3ac4b84e9ce68f --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityTypeManagerInterface.php @@ -0,0 +1,146 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityTypeManagerInterface. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface; +use Drupal\Component\Plugin\PluginManagerInterface; + +/** + * Provides an interface for entity type managers. + */ +interface EntityTypeManagerInterface extends PluginManagerInterface, CachedDiscoveryInterface { + + /** + * Creates a new access control handler instance. + * + * @param string $entity_type + * The entity type for this access control handler. + * + * @return \Drupal\Core\Entity\EntityAccessControlHandlerInterface. + * A access control handler instance. + */ + public function getAccessControlHandler($entity_type); + + /** + * Creates a new storage instance. + * + * @param string $entity_type + * The entity type for this storage. + * + * @return \Drupal\Core\Entity\EntityStorageInterface + * A storage instance. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + public function getStorage($entity_type); + + /** + * Creates a new view builder instance. + * + * @param string $entity_type + * The entity type for this view builder. + * + * @return \Drupal\Core\Entity\EntityViewBuilderInterface. + * A view builder instance. + */ + public function getViewBuilder($entity_type); + + /** + * Creates a new entity list builder. + * + * @param string $entity_type + * The entity type for this list builder. + * + * @return \Drupal\Core\Entity\EntityListBuilderInterface + * An entity list builder instance. + */ + public function getListBuilder($entity_type); + + /** + * Creates a new form instance. + * + * @param string $entity_type + * The entity type for this form. + * @param string $operation + * The name of the operation to use, e.g., 'default'. + * + * @return \Drupal\Core\Entity\EntityFormInterface + * A form instance. + */ + public function getFormObject($entity_type, $operation); + + /** + * Gets all route provider instances. + * + * @param string $entity_type + * The entity type for this route providers. + * + * @return \Drupal\Core\Entity\Routing\EntityRouteProviderInterface[] + */ + public function getRouteProviders($entity_type); + + /** + * Checks whether a certain entity type has a certain handler. + * + * @param string $entity_type + * The name of the entity type. + * @param string $handler_type + * The name of the handler. + * + * @return bool + * Returns TRUE if the entity type has the handler, else FALSE. + */ + public function hasHandler($entity_type, $handler_type); + + /** + * Creates a new handler instance for a entity type and handler type. + * + * @param string $entity_type + * The entity type for this handler. + * @param string $handler_type + * The handler type to create an instance for. + * + * @return object + * A handler instance. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + public function getHandler($entity_type, $handler_type); + + /** + * Creates new handler instance. + * + * Usually \Drupal\Core\Entity\EntityManagerInterface::getHandler() is + * preferred since that method has additional checking that the class exists + * and has static caches. + * + * @param mixed $class + * The handler class to instantiate. + * @param \Drupal\Core\Entity\EntityTypeInterface $definition + * The entity type definition. + * + * @return object + * A handler instance. + */ + public function createHandlerInstance($class, EntityTypeInterface $definition = NULL); + + /** + * {@inheritdoc} + * + * @return \Drupal\Core\Entity\EntityTypeInterface|null + */ + public function getDefinition($entity_type_id, $exception_on_invalid = TRUE); + + /** + * {@inheritdoc} + * + * @return \Drupal\Core\Entity\EntityTypeInterface[] + */ + public function getDefinitions(); + +} diff --git a/core/lib/Drupal/Core/Entity/EntityTypeRepository.php b/core/lib/Drupal/Core/Entity/EntityTypeRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..b03ad1184e970af4bb532cdad9adb7ee1ab8d2f1 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityTypeRepository.php @@ -0,0 +1,113 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityTypeRepository. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Core\Entity\Exception\AmbiguousEntityClassException; +use Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Provides helper methods for loading entity types. + * + * @see \Drupal\Core\Entity\EntityTypeManagerInterface + */ +class EntityTypeRepository implements EntityTypeRepositoryInterface { + + use StringTranslationTrait; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Contains cached mappings of class names to entity types. + * + * @var array + */ + protected $classNameEntityTypeMap = []; + + /** + * Constructs a new EntityTypeRepository. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public function getEntityTypeLabels($group = FALSE) { + $options = []; + $definitions = $this->entityTypeManager->getDefinitions(); + + foreach ($definitions as $entity_type_id => $definition) { + if ($group) { + $options[(string) $definition->getGroupLabel()][$entity_type_id] = $definition->getLabel(); + } + else { + $options[$entity_type_id] = $definition->getLabel(); + } + } + + if ($group) { + foreach ($options as &$group_options) { + // Sort the list alphabetically by group label. + array_multisort($group_options, SORT_ASC, SORT_NATURAL); + } + + // Make sure that the 'Content' group is situated at the top. + $content = $this->t('Content', array(), array('context' => 'Entity type group')); + $options = array((string) $content => $options[(string) $content]) + $options; + } + + return $options; + } + + /** + * {@inheritdoc} + */ + public function getEntityTypeFromClass($class_name) { + // Check the already calculated classes first. + if (isset($this->classNameEntityTypeMap[$class_name])) { + return $this->classNameEntityTypeMap[$class_name]; + } + + $same_class = 0; + $entity_type_id = NULL; + foreach ($this->entityTypeManager->getDefinitions() as $entity_type) { + if ($entity_type->getOriginalClass() == $class_name || $entity_type->getClass() == $class_name) { + $entity_type_id = $entity_type->id(); + if ($same_class++) { + throw new AmbiguousEntityClassException($class_name); + } + } + } + + // Return the matching entity type ID if there is one. + if ($entity_type_id) { + $this->classNameEntityTypeMap[$class_name] = $entity_type_id; + return $entity_type_id; + } + + throw new NoCorrespondingEntityClassException($class_name); + } + + /** + * {@inheritdoc} + */ + public function clearCachedDefinitions() { + $this->classNameEntityTypeMap = []; + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityTypeRepositoryInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeRepositoryInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..a4e4e1ec96be15f3bfa7f98a9d95f3ba2a84c5a9 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityTypeRepositoryInterface.php @@ -0,0 +1,59 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityTypeRepositoryInterface. + */ + +namespace Drupal\Core\Entity; + +/** + * Provides an interface for helper methods for loading entity types. + */ +interface EntityTypeRepositoryInterface { + + /** + * Builds a list of entity type labels suitable for a Form API options list. + * + * @param bool $group + * (optional) Whether to group entity types by plugin group (e.g. 'content', + * 'config'). Defaults to FALSE. + * + * @return array + * An array of entity type labels, keyed by entity type name. + */ + public function getEntityTypeLabels($group = FALSE); + + /** + * Gets the entity type ID based on the class that is called on. + * + * Compares the class this is called on against the known entity classes + * and returns the entity type ID of a direct match or a subclass as fallback, + * to support entity type definitions that were altered. + * + * @param string $class_name + * Class name to use for searching the entity type ID. + * + * @return string + * The entity type ID. + * + * @throws \Drupal\Core\Entity\Exception\AmbiguousEntityClassException + * Thrown when multiple subclasses correspond to the called class. + * @throws \Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException + * Thrown when no entity class corresponds to the called class. + * + * @see \Drupal\Core\Entity\Entity::load() + * @see \Drupal\Core\Entity\Entity::loadMultiple() + */ + public function getEntityTypeFromClass($class_name); + + /** + * Clear the static cache. + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. + * + * @todo Remove in https://www.drupal.org/node/2549143. + */ + public function clearCachedDefinitions(); + +} diff --git a/core/lib/Drupal/Core/Field/FieldDefinitionListener.php b/core/lib/Drupal/Core/Field/FieldDefinitionListener.php new file mode 100644 index 0000000000000000000000000000000000000000..bd303ad000ca900f1eb588556c5f321a7e3a6092 --- /dev/null +++ b/core/lib/Drupal/Core/Field/FieldDefinitionListener.php @@ -0,0 +1,152 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Field\FieldDefinitionListener. + */ + +namespace Drupal\Core\Field; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; + +/** + * Reacts to field definition CRUD on behalf of the Entity system. + */ +class FieldDefinitionListener implements FieldDefinitionListenerInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The key-value factory. + * + * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface + */ + protected $keyValueFactory; + + /** + * Cache backend instance. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cacheBackend; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + + /** + * Constructs a new FieldDefinitionListener. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager + * The entity field manager. + * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory + * The key-value factory. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, KeyValueFactoryInterface $key_value_factory, CacheBackendInterface $cache_backend) { + $this->entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->keyValueFactory = $key_value_factory; + $this->cacheBackend = $cache_backend; + } + + /** + * {@inheritdoc} + */ + public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) { + $entity_type_id = $field_definition->getTargetEntityTypeId(); + $bundle = $field_definition->getTargetBundle(); + $field_name = $field_definition->getName(); + + // Notify the storage about the new field. + $this->entityTypeManager->getStorage($entity_type_id)->onFieldDefinitionCreate($field_definition); + + // Update the bundle field map key value collection, add the new field. + $bundle_field_map = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->get($entity_type_id); + if (!isset($bundle_field_map[$field_name])) { + // This field did not exist yet, initialize it with the type and empty + // bundle list. + $bundle_field_map[$field_name] = [ + 'type' => $field_definition->getType(), + 'bundles' => [], + ]; + } + $bundle_field_map[$field_name]['bundles'][$bundle] = $bundle; + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->set($entity_type_id, $bundle_field_map); + + // Delete the cache entry. + $this->cacheBackend->delete('entity_field_map'); + + // If the field map is initialized, update it as well, so that calls to it + // do not have to rebuild it again. + if ($field_map = $this->entityFieldManager->getFieldMap()) { + if (!isset($field_map[$entity_type_id][$field_name])) { + // This field did not exist yet, initialize it with the type and empty + // bundle list. + $field_map[$entity_type_id][$field_name] = [ + 'type' => $field_definition->getType(), + 'bundles' => [], + ]; + } + $field_map[$entity_type_id][$field_name]['bundles'][$bundle] = $bundle; + $this->entityFieldManager->setFieldMap($field_map); + } + } + + /** + * {@inheritdoc} + */ + public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) { + // Notify the storage about the updated field. + $this->entityTypeManager->getStorage($field_definition->getTargetEntityTypeId())->onFieldDefinitionUpdate($field_definition, $original); + } + + /** + * {@inheritdoc} + */ + public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) { + $entity_type_id = $field_definition->getTargetEntityTypeId(); + $bundle = $field_definition->getTargetBundle(); + $field_name = $field_definition->getName(); + + // Notify the storage about the field deletion. + $this->entityTypeManager->getStorage($entity_type_id)->onFieldDefinitionDelete($field_definition); + + // Unset the bundle from the bundle field map key value collection. + $bundle_field_map = $this->keyValueFactory->get('entity.definitions.bundle_field_map')->get($entity_type_id); + unset($bundle_field_map[$field_name]['bundles'][$bundle]); + if (empty($bundle_field_map[$field_name]['bundles'])) { + // If there are no bundles left, remove the field from the map. + unset($bundle_field_map[$field_name]); + } + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->set($entity_type_id, $bundle_field_map); + + // Delete the cache entry. + $this->cacheBackend->delete('entity_field_map'); + + // If the field map is initialized, update it as well, so that calls to it + // do not have to rebuild it again. + if ($field_map = $this->entityFieldManager->getFieldMap()) { + unset($field_map[$entity_type_id][$field_name]['bundles'][$bundle]); + if (empty($field_map[$entity_type_id][$field_name]['bundles'])) { + unset($field_map[$entity_type_id][$field_name]); + } + $this->entityFieldManager->setFieldMap($field_map); + } + } + +} diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php new file mode 100644 index 0000000000000000000000000000000000000000..c6120316a7633e4344f9815a48ab095efdd3354b --- /dev/null +++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php @@ -0,0 +1,126 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Field\FieldStorageDefinitionListener. + */ + +namespace Drupal\Core\Field; + +use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +/** + * Reacts to field storage definition CRUD on behalf of the Entity system. + * + * @see \Drupal\Core\Field\FieldStorageDefinitionEvents + */ +class FieldStorageDefinitionListener implements FieldStorageDefinitionListenerInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * The entity definition manager. + * + * @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface + */ + protected $entityLastInstalledSchemaRepository; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + + /** + * Constructs a new FieldStorageDefinitionListener. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher + * The event dispatcher. + * @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository + * The entity last installed schema repository. + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager + * The entity field manager. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository, EntityFieldManagerInterface $entity_field_manager) { + $this->entityTypeManager = $entity_type_manager; + $this->eventDispatcher = $event_dispatcher; + $this->entityLastInstalledSchemaRepository = $entity_last_installed_schema_repository; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { + $entity_type_id = $storage_definition->getTargetEntityTypeId(); + + // @todo Forward this to all interested handlers, not only storage, once + // iterating handlers is possible: https://www.drupal.org/node/2332857. + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if ($storage instanceof FieldStorageDefinitionListenerInterface) { + $storage->onFieldStorageDefinitionCreate($storage_definition); + } + + $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::CREATE, new FieldStorageDefinitionEvent($storage_definition)); + + $this->entityLastInstalledSchemaRepository->setLastInstalledFieldStorageDefinition($storage_definition); + $this->entityFieldManager->clearCachedFieldDefinitions(); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + $entity_type_id = $storage_definition->getTargetEntityTypeId(); + + // @todo Forward this to all interested handlers, not only storage, once + // iterating handlers is possible: https://www.drupal.org/node/2332857. + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if ($storage instanceof FieldStorageDefinitionListenerInterface) { + $storage->onFieldStorageDefinitionUpdate($storage_definition, $original); + } + + $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::UPDATE, new FieldStorageDefinitionEvent($storage_definition, $original)); + + $this->entityLastInstalledSchemaRepository->setLastInstalledFieldStorageDefinition($storage_definition); + $this->entityFieldManager->clearCachedFieldDefinitions(); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { + $entity_type_id = $storage_definition->getTargetEntityTypeId(); + + // @todo Forward this to all interested handlers, not only storage, once + // iterating handlers is possible: https://www.drupal.org/node/2332857. + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if ($storage instanceof FieldStorageDefinitionListenerInterface) { + $storage->onFieldStorageDefinitionDelete($storage_definition); + } + + $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::DELETE, new FieldStorageDefinitionEvent($storage_definition)); + + $this->entityLastInstalledSchemaRepository->deleteLastInstalledFieldStorageDefinition($storage_definition); + $this->entityFieldManager->clearCachedFieldDefinitions(); + } + +} diff --git a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php index 5b7b86ba2f844cb295dc455ceee135caf691e1c4..558556d2d85b39ec62d1d086d2354376e5b46fd5 100644 --- a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php +++ b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php @@ -8,13 +8,14 @@ namespace Drupal\Core\Plugin; use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Cache\UseCacheBackendTrait; use Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait; use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator; use Drupal\Component\Plugin\PluginManagerBase; use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\Cache; -use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery; use Drupal\Core\Plugin\Factory\ContainerFactory; @@ -27,13 +28,7 @@ class DefaultPluginManager extends PluginManagerBase implements PluginManagerInterface, CachedDiscoveryInterface { use DiscoveryCachedTrait; - - /** - * Cache backend instance. - * - * @var \Drupal\Core\Cache\CacheBackendInterface - */ - protected $cacheBackend; + use UseCacheBackendTrait; /** * The cache key. @@ -80,13 +75,6 @@ class DefaultPluginManager extends PluginManagerBase implements PluginManagerInt */ protected $defaults = array(); - /** - * Flag whether persistent caches should be used. - * - * @var bool - */ - protected $useCaches = TRUE; - /** * The name of the annotation that contains the plugin definition. * @@ -236,30 +224,6 @@ public function useCaches($use_caches = FALSE) { } } - /** - * Fetches from the cache backend, respecting the use caches flag. - * - * @see \Drupal\Core\Cache\CacheBackendInterface::get() - */ - protected function cacheGet($cid) { - if ($this->useCaches && $this->cacheBackend) { - return $this->cacheBackend->get($cid); - } - return FALSE; - } - - /** - * Stores data in the persistent cache, respecting the use caches flag. - * - * @see \Drupal\Core\Cache\CacheBackendInterface::set() - */ - protected function cacheSet($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) { - if ($this->cacheBackend && $this->useCaches) { - $this->cacheBackend->set($cid, $data, $expire, $tags); - } - } - - /** * Performs extra processing on plugin definitions. * diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityFieldManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityFieldManagerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c63cef87b44ad0d0e9040be0edbb378b6dc3b91b --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/EntityFieldManagerTest.php @@ -0,0 +1,824 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Entity\EntityFieldManagerTest. + */ + +namespace Drupal\Tests\Core\Entity; + +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Cache\CacheTagsInvalidatorInterface; +use Drupal\Core\Config\Entity\ConfigEntityStorageInterface; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\ContentEntityTypeInterface; +use Drupal\Core\Entity\EntityDisplayRepositoryInterface; +use Drupal\Core\Entity\EntityFieldManager; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\EntityTypeRepositoryInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreInterface; +use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\Core\TypedData\TypedDataManagerInterface; +use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * @coversDefaultClass \Drupal\Core\Entity\EntityFieldManager + * @group Entity + */ +class EntityFieldManagerTest extends UnitTestCase { + + /** + * The typed data manager. + * + * @var \Drupal\Core\TypedData\TypedDataManagerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $typedDataManager; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $moduleHandler; + + /** + * The cache backend to use. + * + * @var \Drupal\Core\Cache\CacheBackendInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $cacheBackend; + + /** + * The cache tags invalidator. + * + * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $cacheTagsInvalidator; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $languageManager; + + /** + * The keyvalue factory. + * + * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $keyValueFactory; + + /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $eventDispatcher; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $entityTypeManager; + + /** + * The entity type repository. + * + * @var \Drupal\Core\Entity\EntityTypeRepositoryInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $entityTypeRepository; + + /** + * The entity type bundle info. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $entityTypeBundleInfo; + + /** + * The entity display repository. + * + * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $entityDisplayRepository; + + /** + * The entity field manager under test. + * + * @var \Drupal\Core\Entity\EntityFieldManager + */ + protected $entityFieldManager; + + /** + * The dependency injection container. + * + * @var \Symfony\Component\DependencyInjection\ContainerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $container; + + /** + * The entity type definition. + * + * @var \Drupal\Core\Entity\EntityTypeInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $entityType; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + $this->container = $this->prophesize(ContainerInterface::class); + \Drupal::setContainer($this->container->reveal()); + + $this->typedDataManager = $this->prophesize(TypedDataManagerInterface::class); + $this->typedDataManager->getDefinition('field_item:boolean')->willReturn([ + 'class' => BooleanItem::class, + ]); + $this->container->get('typed_data_manager')->willReturn($this->typedDataManager->reveal()); + + $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); + $this->moduleHandler->alter('entity_base_field_info', Argument::type('array'), Argument::any())->willReturn(NULL); + $this->moduleHandler->alter('entity_bundle_field_info', Argument::type('array'), Argument::any(), Argument::type('string'))->willReturn(NULL); + + $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); + $this->cacheTagsInvalidator = $this->prophesize(CacheTagsInvalidatorInterface::class); + + $language = new Language(['id' => 'en']); + $this->languageManager = $this->prophesize(LanguageManagerInterface::class); + $this->languageManager->getCurrentLanguage()->willReturn($language); + $this->languageManager->getLanguages()->willReturn(['en' => (object) ['id' => 'en']]); + + $this->keyValueFactory = $this->prophesize(KeyValueFactoryInterface::class); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityTypeRepository = $this->prophesize(EntityTypeRepositoryInterface::class); + $this->entityTypeBundleInfo = $this->prophesize(EntityTypeBundleInfoInterface::class); + $this->entityDisplayRepository = $this->prophesize(EntityDisplayRepositoryInterface::class); + + $this->entityFieldManager = new TestEntityFieldManager($this->entityTypeManager->reveal(), $this->entityTypeBundleInfo->reveal(), $this->entityDisplayRepository->reveal(), $this->typedDataManager->reveal(), $this->languageManager->reveal(), $this->keyValueFactory->reveal(), $this->moduleHandler->reveal(), $this->cacheBackend->reveal()); + } + + /** + * Sets up the entity type manager to be tested. + * + * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions + * (optional) An array of entity type definitions. + */ + protected function setUpEntityTypeDefinitions($definitions = []) { + $class = $this->getMockClass(EntityInterface::class); + foreach ($definitions as $key => $entity_type) { + // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called + // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must + // always be mocked. + $entity_type->getLinkTemplates()->willReturn([]); + + // Give the entity type a legitimate class to return. + $entity_type->getClass()->willReturn($class); + + $definitions[$key] = $entity_type->reveal(); + } + + $this->entityTypeManager->getDefinition(Argument::type('string')) + ->will(function ($args) use ($definitions) { + if (isset($definitions[$args[0]])) { + return $definitions[$args[0]]; + } + throw new PluginNotFoundException($args[0]); + }); + $this->entityTypeManager->getDefinition(Argument::type('string'), FALSE) + ->will(function ($args) use ($definitions) { + if (isset($definitions[$args[0]])) { + return $definitions[$args[0]]; + } + }); + $this->entityTypeManager->getDefinitions()->willReturn($definitions); + + } + + /** + * Tests the getBaseFieldDefinitions() method. + * + * @covers ::getBaseFieldDefinitions + * @covers ::buildBaseFieldDefinitions + */ + public function testGetBaseFieldDefinitions() { + $field_definition = $this->setUpEntityWithFieldDefinition(); + + $expected = ['id' => $field_definition]; + $this->assertSame($expected, $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type')); + } + + /** + * Tests the getFieldDefinitions() method. + * + * @covers ::getFieldDefinitions + * @covers ::buildBundleFieldDefinitions + */ + public function testGetFieldDefinitions() { + $field_definition = $this->setUpEntityWithFieldDefinition(); + + $expected = ['id' => $field_definition]; + $this->assertSame($expected, $this->entityFieldManager->getFieldDefinitions('test_entity_type', 'test_entity_bundle')); + } + + /** + * Tests the getFieldStorageDefinitions() method. + * + * @covers ::getFieldStorageDefinitions + * @covers ::buildFieldStorageDefinitions + */ + public function testGetFieldStorageDefinitions() { + $field_definition = $this->setUpEntityWithFieldDefinition(TRUE); + $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class); + $field_storage_definition->getName()->willReturn('field_storage'); + + $definitions = ['field_storage' => $field_storage_definition->reveal()]; + + $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([]); + $this->moduleHandler->getImplementations('entity_field_storage_info')->willReturn(['example_module']); + $this->moduleHandler->invoke('example_module', 'entity_field_storage_info', [$this->entityType])->willReturn($definitions); + $this->moduleHandler->alter('entity_field_storage_info', $definitions, $this->entityType)->willReturn(NULL); + + $expected = [ + 'id' => $field_definition, + 'field_storage' => $field_storage_definition->reveal(), + ]; + $this->assertSame($expected, $this->entityFieldManager->getFieldStorageDefinitions('test_entity_type')); + } + + /** + * Tests the getBaseFieldDefinitions() method with a translatable entity type. + * + * @covers ::getBaseFieldDefinitions + * @covers ::buildBaseFieldDefinitions + * + * @dataProvider providerTestGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode + */ + public function testGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode($default_langcode_key) { + $this->setUpEntityWithFieldDefinition(FALSE, 'id', ['langcode' => 'langcode', 'default_langcode' => $default_langcode_key]); + + $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); + $field_definition->isTranslatable()->willReturn(TRUE); + + $entity_class = EntityManagerTestEntity::class; + $entity_class::$baseFieldDefinitions += ['langcode' => $field_definition]; + + $this->entityType->isTranslatable()->willReturn(TRUE); + + $definitions = $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type'); + + $this->assertTrue(isset($definitions[$default_langcode_key])); + } + + /** + * Provides test data for testGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode(). + * + * @return array + * Test data. + */ + public function providerTestGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode() { + return [ + ['default_langcode'], + ['custom_default_langcode_key'], + ]; + } + + /** + * Tests the getBaseFieldDefinitions() method with a translatable entity type. + * + * @covers ::getBaseFieldDefinitions + * @covers ::buildBaseFieldDefinitions + * + * @expectedException \LogicException + * @expectedExceptionMessage The Test entity type cannot be translatable as it does not define a translatable "langcode" field. + * + * @dataProvider providerTestGetBaseFieldDefinitionsTranslatableEntityTypeLangcode + */ + public function testGetBaseFieldDefinitionsTranslatableEntityTypeLangcode($provide_key, $provide_field, $translatable) { + $keys = $provide_key ? ['langcode' => 'langcode'] : []; + $this->setUpEntityWithFieldDefinition(FALSE, 'id', $keys); + + if ($provide_field) { + $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); + $field_definition->isTranslatable()->willReturn($translatable); + if (!$translatable) { + $field_definition->setTranslatable(!$translatable)->shouldBeCalled(); + } + + $entity_class = EntityManagerTestEntity::class; + $entity_class::$baseFieldDefinitions += ['langcode' => $field_definition->reveal()]; + } + + $this->entityType->isTranslatable()->willReturn(TRUE); + $this->entityType->getLabel()->willReturn('Test'); + + $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type'); + } + + /** + * Provides test data for testGetBaseFieldDefinitionsTranslatableEntityTypeLangcode(). + * + * @return array + * Test data. + */ + public function providerTestGetBaseFieldDefinitionsTranslatableEntityTypeLangcode() { + return [ + [FALSE, TRUE, TRUE], + [TRUE, FALSE, TRUE], + [TRUE, TRUE, FALSE], + ]; + } + + /** + * Tests the getBaseFieldDefinitions() method with caching. + * + * @covers ::getBaseFieldDefinitions + */ + public function testGetBaseFieldDefinitionsWithCaching() { + $field_definition = $this->setUpEntityWithFieldDefinition(); + + $expected = ['id' => $field_definition]; + + $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') + ->willReturn(FALSE) + ->shouldBeCalled(); + $this->cacheBackend->set('entity_base_field_definitions:test_entity_type:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) + ->will(function ($args) { + $data = (object) ['data' => $args[1]]; + $this->get('entity_base_field_definitions:test_entity_type:en') + ->willReturn($data) + ->shouldBeCalled(); + }) + ->shouldBeCalled(); + + $this->assertSame($expected, $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type')); + $this->entityFieldManager->testClearEntityFieldInfo(); + $this->assertSame($expected, $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type')); + } + + /** + * Tests the getFieldDefinitions() method with caching. + * + * @covers ::getFieldDefinitions + */ + public function testGetFieldDefinitionsWithCaching() { + $field_definition = $this->setUpEntityWithFieldDefinition(FALSE, 'id'); + + $expected = ['id' => $field_definition]; + + $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') + ->willReturn((object) ['data' => $expected]) + ->shouldBeCalledTimes(2); + $this->cacheBackend->get('entity_bundle_field_definitions:test_entity_type:test_bundle:en') + ->willReturn(FALSE) + ->shouldBeCalledTimes(1); + $this->cacheBackend->set('entity_bundle_field_definitions:test_entity_type:test_bundle:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) + ->will(function ($args) { + $data = (object) ['data' => $args[1]]; + $this->get('entity_bundle_field_definitions:test_entity_type:test_bundle:en') + ->willReturn($data) + ->shouldBeCalled(); + }) + ->shouldBeCalled(); + + $this->assertSame($expected, $this->entityFieldManager->getFieldDefinitions('test_entity_type', 'test_bundle')); + $this->entityFieldManager->testClearEntityFieldInfo(); + $this->assertSame($expected, $this->entityFieldManager->getFieldDefinitions('test_entity_type', 'test_bundle')); + } + + /** + * Tests the getFieldStorageDefinitions() method with caching. + * + * @covers ::getFieldStorageDefinitions + */ + public function testGetFieldStorageDefinitionsWithCaching() { + $field_definition = $this->setUpEntityWithFieldDefinition(TRUE, 'id'); + $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class); + $field_storage_definition->getName()->willReturn('field_storage'); + + $definitions = ['field_storage' => $field_storage_definition->reveal()]; + + $this->moduleHandler->getImplementations('entity_field_storage_info')->willReturn(['example_module']); + $this->moduleHandler->invoke('example_module', 'entity_field_storage_info', [$this->entityType])->willReturn($definitions); + $this->moduleHandler->alter('entity_field_storage_info', $definitions, $this->entityType)->willReturn(NULL); + + $expected = [ + 'id' => $field_definition, + 'field_storage' => $field_storage_definition->reveal(), + ]; + + $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') + ->willReturn((object) ['data' => ['id' => $expected['id']]]) + ->shouldBeCalledTimes(2); + $this->cacheBackend->get('entity_field_storage_definitions:test_entity_type:en')->willReturn(FALSE); + + $this->cacheBackend->set('entity_field_storage_definitions:test_entity_type:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) + ->will(function () use ($expected) { + $this->get('entity_field_storage_definitions:test_entity_type:en') + ->willReturn((object) ['data' => $expected]) + ->shouldBeCalled(); + }) + ->shouldBeCalled(); + + + $this->assertSame($expected, $this->entityFieldManager->getFieldStorageDefinitions('test_entity_type')); + $this->entityFieldManager->testClearEntityFieldInfo(); + $this->assertSame($expected, $this->entityFieldManager->getFieldStorageDefinitions('test_entity_type')); + } + + /** + * Tests the getBaseFieldDefinitions() method with an invalid definition. + * + * @covers ::getBaseFieldDefinitions + * @covers ::buildBaseFieldDefinitions + * + * @expectedException \LogicException + */ + public function testGetBaseFieldDefinitionsInvalidDefinition() { + $this->setUpEntityWithFieldDefinition(FALSE, 'langcode', ['langcode' => 'langcode']); + + $this->entityType->isTranslatable()->willReturn(TRUE); + $this->entityType->getLabel()->willReturn('the_label'); + + $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type'); + } + + /** + * Tests that getFieldDefinitions() method sets the 'provider' definition key. + * + * @covers ::getFieldDefinitions + * @covers ::buildBundleFieldDefinitions + */ + public function testGetFieldDefinitionsProvider() { + $this->setUpEntityWithFieldDefinition(TRUE); + + $module = 'entity_manager_test_module'; + + // @todo Mock FieldDefinitionInterface once it exposes a proper provider + // setter. See https://www.drupal.org/node/2225961. + $field_definition = $this->prophesize(BaseFieldDefinition::class); + + // We expect two calls as the field definition will be returned from both + // base and bundle entity field info hook implementations. + $field_definition->getProvider()->shouldBeCalled(); + $field_definition->setProvider($module)->shouldBeCalledTimes(2); + $field_definition->setName(0)->shouldBeCalledTimes(2); + $field_definition->setTargetEntityTypeId('test_entity_type')->shouldBeCalled(); + $field_definition->setTargetBundle(NULL)->shouldBeCalled(); + $field_definition->setTargetBundle('test_bundle')->shouldBeCalled(); + + $this->moduleHandler->getImplementations(Argument::type('string'))->willReturn([$module]); + $this->moduleHandler->invoke($module, 'entity_base_field_info', [$this->entityType])->willReturn([$field_definition->reveal()]); + $this->moduleHandler->invoke($module, 'entity_bundle_field_info', Argument::type('array'))->willReturn([$field_definition->reveal()]); + + $this->entityFieldManager->getFieldDefinitions('test_entity_type', 'test_bundle'); + } + + /** + * Prepares an entity that defines a field definition. + * + * @param bool $custom_invoke_all + * (optional) Whether the test will set up its own + * ModuleHandlerInterface::invokeAll() implementation. Defaults to FALSE. + * @param string $field_definition_id + * (optional) The ID to use for the field definition. Defaults to 'id'. + * @param array $entity_keys + * (optional) An array of entity keys for the mocked entity type. Defaults + * to an empty array. + * + * @return \Drupal\Core\Field\BaseFieldDefinition|\Prophecy\Prophecy\ProphecyInterface + * A field definition object. + */ + protected function setUpEntityWithFieldDefinition($custom_invoke_all = FALSE, $field_definition_id = 'id', $entity_keys = []) { + $field_type_manager = $this->prophesize(FieldTypePluginManagerInterface::class); + $field_type_manager->getDefaultStorageSettings('boolean')->willReturn([]); + $field_type_manager->getDefaultFieldSettings('boolean')->willReturn([]); + $this->container->get('plugin.manager.field.field_type')->willReturn($field_type_manager->reveal()); + + $string_translation = $this->prophesize(TranslationInterface::class); + $this->container->get('string_translation')->willReturn($string_translation->reveal()); + + $entity_class = EntityManagerTestEntity::class; + + $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); + $entity_class::$baseFieldDefinitions = [ + $field_definition_id => $field_definition->reveal(), + ]; + $entity_class::$bundleFieldDefinitions = []; + + if (!$custom_invoke_all) { + $this->moduleHandler->getImplementations(Argument::cetera())->willReturn([]); + } + + // Mock the base field definition override. + $override_entity_type = $this->prophesize(EntityTypeInterface::class); + + $this->entityType = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $this->entityType, 'base_field_override' => $override_entity_type]); + + $storage = $this->prophesize(ConfigEntityStorageInterface::class); + $storage->loadMultiple(Argument::type('array'))->willReturn([]); + $this->entityTypeManager->getStorage('base_field_override')->willReturn($storage->reveal()); + + $this->entityType->getClass()->willReturn($entity_class); + $this->entityType->getKeys()->willReturn($entity_keys + ['default_langcode' => 'default_langcode']); + $this->entityType->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); + $this->entityType->isTranslatable()->willReturn(FALSE); + $this->entityType->getProvider()->willReturn('the_provider'); + $this->entityType->id()->willReturn('the_entity_id'); + + return $field_definition->reveal(); + } + + /** + * Tests the clearCachedFieldDefinitions() method. + * + * @covers ::clearCachedFieldDefinitions + */ + public function testClearCachedFieldDefinitions() { + $this->setUpEntityTypeDefinitions(); + + $this->cacheTagsInvalidator->invalidateTags(['entity_field_info'])->shouldBeCalled(); + $this->container->get('cache_tags.invalidator')->willReturn($this->cacheTagsInvalidator->reveal())->shouldBeCalled(); + + $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); + + $this->entityFieldManager->clearCachedFieldDefinitions(); + } + + /** + * @covers ::getExtraFields + */ + function testGetExtraFields() { + $this->setUpEntityTypeDefinitions(); + + $entity_type_id = $this->randomMachineName(); + $bundle = $this->randomMachineName(); + $language_code = 'en'; + $hook_bundle_extra_fields = [ + $entity_type_id => [ + $bundle => [ + 'form' => [ + 'foo_extra_field' => [ + 'label' => 'Foo', + ], + ], + ], + ], + ]; + $processed_hook_bundle_extra_fields = $hook_bundle_extra_fields; + $processed_hook_bundle_extra_fields[$entity_type_id][$bundle] += [ + 'display' => [], + ]; + $cache_id = 'entity_bundle_extra_fields:' . $entity_type_id . ':' . $bundle . ':' . $language_code; + + $language = new Language(['id' => $language_code]); + $this->languageManager->getCurrentLanguage() + ->willReturn($language) + ->shouldBeCalledTimes(1); + + $this->cacheBackend->get($cache_id)->shouldBeCalled(); + + $this->moduleHandler->invokeAll('entity_extra_field_info')->willReturn($hook_bundle_extra_fields); + $this->moduleHandler->alter('entity_extra_field_info', $hook_bundle_extra_fields)->shouldBeCalled(); + + $this->cacheBackend->set($cache_id, $processed_hook_bundle_extra_fields[$entity_type_id][$bundle], Cache::PERMANENT, ['entity_field_info'])->shouldBeCalled(); + + $this->assertSame($processed_hook_bundle_extra_fields[$entity_type_id][$bundle], $this->entityFieldManager->getExtraFields($entity_type_id, $bundle)); + } + + /** + * @covers ::getFieldMap + */ + public function testGetFieldMap() { + $this->entityTypeBundleInfo->getBundleInfo('test_entity_type')->willReturn([])->shouldBeCalled(); + + // Set up a content entity type. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_class = EntityManagerTestEntity::class; + + // Define an ID field definition as a base field. + $id_definition = $this->prophesize(FieldDefinitionInterface::class); + $id_definition->getType()->willReturn('integer'); + $base_field_definitions = [ + 'id' => $id_definition->reveal(), + ]; + $entity_class::$baseFieldDefinitions = $base_field_definitions; + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); + $key_value_store->getAll()->willReturn([ + 'test_entity_type' => [ + 'by_bundle' => [ + 'type' => 'string', + 'bundles' => ['second_bundle' => 'second_bundle'], + ], + ], + ]); + + // Set up a non-content entity type. + $non_content_entity_type = $this->prophesize(EntityTypeInterface::class); + + // Mock the base field definition override. + $override_entity_type = $this->prophesize(EntityTypeInterface::class); + + $this->setUpEntityTypeDefinitions([ + 'test_entity_type' => $entity_type, + 'non_fieldable' => $non_content_entity_type, + 'base_field_override' => $override_entity_type, + ]); + + $entity_type->getClass()->willReturn($entity_class); + $entity_type->getKeys()->willReturn(['default_langcode' => 'default_langcode']); + $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); + $entity_type->isTranslatable()->shouldBeCalled(); + $entity_type->getProvider()->shouldBeCalled(); + + $non_content_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); + + $override_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); + + // Set up the entity type bundle info to return two bundles for the + // fieldable entity type. + $this->entityTypeBundleInfo->getBundleInfo('test_entity_type')->willReturn([ + 'first_bundle' => 'first_bundle', + 'second_bundle' => 'second_bundle', + ])->shouldBeCalled(); + $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([]); + + $expected = [ + 'test_entity_type' => [ + 'id' => [ + 'type' => 'integer', + 'bundles' => ['first_bundle' => 'first_bundle', 'second_bundle' => 'second_bundle'], + ], + 'by_bundle' => [ + 'type' => 'string', + 'bundles' => ['second_bundle' => 'second_bundle'], + ], + ] + ]; + $this->assertEquals($expected, $this->entityFieldManager->getFieldMap()); + } + + /** + * @covers ::getFieldMap + */ + public function testGetFieldMapFromCache() { + $expected = [ + 'test_entity_type' => [ + 'id' => [ + 'type' => 'integer', + 'bundles' => ['first_bundle' => 'first_bundle', 'second_bundle' => 'second_bundle'], + ], + 'by_bundle' => [ + 'type' => 'string', + 'bundles' => ['second_bundle' => 'second_bundle'], + ], + ] + ]; + $this->setUpEntityTypeDefinitions(); + $this->cacheBackend->get('entity_field_map')->willReturn((object) ['data' => $expected]); + + // Call the field map twice to make sure the static cache works. + $this->assertEquals($expected, $this->entityFieldManager->getFieldMap()); + $this->assertEquals($expected, $this->entityFieldManager->getFieldMap()); + } + + /** + * @covers ::getFieldMapByFieldType + */ + public function testGetFieldMapByFieldType() { + // Set up a content entity type. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_class = EntityManagerTestEntity::class; + + // Set up the entity type bundle info to return two bundles for the + // fieldable entity type. + $this->entityTypeBundleInfo->getBundleInfo('test_entity_type')->willReturn([ + 'first_bundle' => 'first_bundle', + 'second_bundle' => 'second_bundle', + ])->shouldBeCalled(); + $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([])->shouldBeCalled(); + + // Define an ID field definition as a base field. + $id_definition = $this->prophesize(FieldDefinitionInterface::class); + $id_definition->getType()->willReturn('integer')->shouldBeCalled(); + $base_field_definitions = [ + 'id' => $id_definition->reveal(), + ]; + $entity_class::$baseFieldDefinitions = $base_field_definitions; + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal())->shouldBeCalled(); + $key_value_store->getAll()->willReturn([ + 'test_entity_type' => [ + 'by_bundle' => [ + 'type' => 'string', + 'bundles' => ['second_bundle' => 'second_bundle'], + ], + ], + ])->shouldBeCalled(); + + // Mock the base field definition override. + $override_entity_type = $this->prophesize(EntityTypeInterface::class); + + $this->setUpEntityTypeDefinitions([ + 'test_entity_type' => $entity_type, + 'base_field_override' => $override_entity_type, + ]); + + $entity_type->getClass()->willReturn($entity_class)->shouldBeCalled(); + $entity_type->getKeys()->willReturn(['default_langcode' => 'default_langcode'])->shouldBeCalled(); + $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE)->shouldBeCalled(); + $entity_type->isTranslatable()->shouldBeCalled(); + $entity_type->getProvider()->shouldBeCalled(); + + $override_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE)->shouldBeCalled(); + + $integerFields = $this->entityFieldManager->getFieldMapByFieldType('integer'); + $this->assertCount(1, $integerFields['test_entity_type']); + $this->assertArrayNotHasKey('non_fieldable', $integerFields); + $this->assertArrayHasKey('id', $integerFields['test_entity_type']); + $this->assertArrayNotHasKey('by_bundle', $integerFields['test_entity_type']); + + $stringFields = $this->entityFieldManager->getFieldMapByFieldType('string'); + $this->assertCount(1, $stringFields['test_entity_type']); + $this->assertArrayNotHasKey('non_fieldable', $stringFields); + $this->assertArrayHasKey('by_bundle', $stringFields['test_entity_type']); + $this->assertArrayNotHasKey('id', $stringFields['test_entity_type']); + } + +} + +class TestEntityFieldManager extends EntityFieldManager { + + /** + * Allows the static caches to be cleared. + */ + public function testClearEntityFieldInfo() { + $this->baseFieldDefinitions = []; + $this->fieldDefinitions = []; + $this->fieldStorageDefinitions = []; + } +} + +/** + * Provides a content entity with dummy static method implementations. + */ +abstract class EntityManagerTestEntity implements \Iterator, ContentEntityInterface { + + /** + * The base field definitions. + * + * @var \Drupal\Core\Field\FieldDefinitionInterface[] + */ + public static $baseFieldDefinitions = []; + + /** + * The bundle field definitions. + * + * @var array[] + * Keys are entity type IDs, values are arrays of which the keys are bundle + * names and the values are field definitions. + */ + public static $bundleFieldDefinitions = []; + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + return static::$baseFieldDefinitions; + } + + /** + * {@inheritdoc} + */ + public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { + return isset(static::$bundleFieldDefinitions[$entity_type->id()][$bundle]) ? static::$bundleFieldDefinitions[$entity_type->id()][$bundle] : []; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php index a3f85685535f06ab59e33b0eb7e50fcd6fe99217..ec66c7ab74e4379c7ab198b13c9a0c968b516e02 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php @@ -5,42 +5,16 @@ * Contains \Drupal\Tests\Core\Entity\EntityManagerTest. */ -namespace Drupal\Tests\Core\Entity { +namespace Drupal\Tests\Core\Entity; -use Drupal\Component\Plugin\Discovery\DiscoveryInterface; -use Drupal\Component\Plugin\Exception\PluginNotFoundException; -use Drupal\Core\Cache\Cache; -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Cache\CacheTagsInvalidatorInterface; -use Drupal\Core\Config\Entity\ConfigEntityStorage; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\ContentEntityTypeInterface; -use Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface; -use Drupal\Core\Entity\EntityHandlerBase; -use Drupal\Core\Entity\EntityHandlerInterface; -use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityManager; -use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\Core\Field\FieldTypePluginManagerInterface; -use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem; -use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; -use Drupal\Core\KeyValueStore\KeyValueStoreInterface; -use Drupal\Core\Language\Language; -use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Core\StringTranslation\TranslationInterface; -use Drupal\Core\TypedData\TypedDataManager; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\EntityTypeRepositoryInterface; use Drupal\Tests\UnitTestCase; use Prophecy\Argument; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * @coversDefaultClass \Drupal\Core\Entity\EntityManager @@ -51,79 +25,37 @@ class EntityManagerTest extends UnitTestCase { /** * The entity manager. * - * @var \Drupal\Tests\Core\Entity\TestEntityManager + * @var \Drupal\Core\Entity\EntityManager */ protected $entityManager; /** - * The entity type definition. + * The entity type manager. * - * @var \Drupal\Core\Entity\EntityTypeInterface|\Prophecy\Prophecy\ProphecyInterface + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ProphecyInterface */ - protected $entityType; + protected $entityTypeManager; /** - * The plugin discovery. + * The entity type repository. * - * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface|\Prophecy\Prophecy\ProphecyInterface + * @var \Drupal\Core\Entity\EntityTypeRepositoryInterface|\Prophecy\Prophecy\ProphecyInterface */ - protected $discovery; + protected $entityTypeRepository; /** - * The dependency injection container. + * The entity type bundle info. * - * @var \Symfony\Component\DependencyInjection\ContainerInterface|\Prophecy\Prophecy\ProphecyInterface + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface|\Prophecy\Prophecy\ProphecyInterface */ - protected $container; + protected $entityTypeBundleInfo; /** - * The module handler. + * The entity field manager. * - * @var \Drupal\Core\Extension\ModuleHandlerInterface|\Prophecy\Prophecy\ProphecyInterface + * @var \Drupal\Core\Entity\EntityFieldManagerInterface|\Prophecy\Prophecy\ProphecyInterface */ - protected $moduleHandler; - - /** - * The cache backend to use. - * - * @var \Drupal\Core\Cache\CacheBackendInterface|\Prophecy\Prophecy\ProphecyInterface - */ - protected $cacheBackend; - - /** - * The cache tags invalidator. - * - * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface|\Prophecy\Prophecy\ProphecyInterface - */ - protected $cacheTagsInvalidator; - - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface|\Prophecy\Prophecy\ProphecyInterface - */ - protected $languageManager; - - /** - * The typed data manager. - * - * @var \Drupal\Core\TypedData\TypedDataManager|\Prophecy\Prophecy\ProphecyInterface - */ - protected $typedDataManager; - - /** - * The keyvalue factory. - * - * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface|\Prophecy\Prophecy\ProphecyInterface - */ - protected $keyValueFactory; - - /** - * The event dispatcher. - * - * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface|\Prophecy\Prophecy\ProphecyInterface - */ - protected $eventDispatcher; + protected $entityFieldManager; /** * {@inheritdoc} @@ -131,1594 +63,33 @@ class EntityManagerTest extends UnitTestCase { protected function setUp() { parent::setUp(); - $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); - $this->moduleHandler->getImplementations('entity_type_build')->willReturn([]); - $this->moduleHandler->alter('entity_type', Argument::type('array'))->willReturn(NULL); - $this->moduleHandler->alter('entity_base_field_info', Argument::type('array'), Argument::any())->willReturn(NULL); - $this->moduleHandler->alter('entity_bundle_field_info', Argument::type('array'), Argument::any(), Argument::type('string'))->willReturn(NULL); - - $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); - $this->cacheTagsInvalidator = $this->prophesize(CacheTagsInvalidatorInterface::class); - - $language = new Language(['id' => 'en']); - $this->languageManager = $this->prophesize(LanguageManagerInterface::class); - $this->languageManager->getCurrentLanguage()->willReturn($language); - $this->languageManager->getLanguages()->willReturn(['en' => (object) ['id' => 'en']]); - - $this->typedDataManager = $this->prophesize(TypedDataManager::class); - $this->typedDataManager->getDefinition('field_item:boolean')->willReturn([ - 'class' => BooleanItem::class, - ]); - - $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); - - $this->keyValueFactory = $this->prophesize(KeyValueFactoryInterface::class); - - $this->container = $this->prophesize(ContainerInterface::class); - $this->container->get('cache_tags.invalidator')->willReturn($this->cacheTagsInvalidator->reveal()); - $this->container->get('typed_data_manager')->willReturn($this->typedDataManager->reveal()); - \Drupal::setContainer($this->container->reveal()); - - $this->discovery = $this->prophesize(DiscoveryInterface::class); - $translation_manager = $this->prophesize(TranslationInterface::class); - - $this->entityManager = new TestEntityManager(new \ArrayObject(), $this->moduleHandler->reveal(), $this->cacheBackend->reveal(), $this->languageManager->reveal(), $translation_manager->reveal(), $this->getClassResolverStub(), $this->typedDataManager->reveal(), $this->keyValueFactory->reveal(), $this->eventDispatcher->reveal()); - $this->entityManager->setContainer($this->container->reveal()); - $this->entityManager->setDiscovery($this->discovery->reveal()); - } - - /** - * Sets up the entity manager to be tested. - * - * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions - * (optional) An array of entity type definitions. - */ - protected function setUpEntityManager($definitions = array()) { - $class = $this->getMockClass(EntityInterface::class); - foreach ($definitions as $key => $entity_type) { - // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called - // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must - // always be mocked. - $entity_type->getLinkTemplates()->willReturn([]); - - // Give the entity type a legitimate class to return. - $entity_type->getClass()->willReturn($class); + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityTypeRepository = $this->prophesize(EntityTypeRepositoryInterface::class); + $this->entityTypeBundleInfo = $this->prophesize(EntityTypeBundleInfoInterface::class); + $this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class); - $definitions[$key] = $entity_type->reveal(); - } + $container = new ContainerBuilder(); + $container->set('entity_type.manager', $this->entityTypeManager->reveal()); + $container->set('entity_type.repository', $this->entityTypeRepository->reveal()); + $container->set('entity_type.bundle.info', $this->entityTypeBundleInfo->reveal()); + $container->set('entity_field.manager', $this->entityFieldManager->reveal()); - $this->discovery->getDefinition(Argument::cetera()) - ->will(function ($args) use ($definitions) { - $entity_type_id = $args[0]; - $exception_on_invalid = $args[1]; - if (isset($definitions[$entity_type_id])) { - return $definitions[$entity_type_id]; - } - elseif (!$exception_on_invalid) { - return NULL; - } - else throw new PluginNotFoundException($entity_type_id); - }); - $this->discovery->getDefinitions()->willReturn($definitions); + $this->entityManager = new EntityManager(); + $this->entityManager->setContainer($container); } /** * Tests the clearCachedDefinitions() method. * * @covers ::clearCachedDefinitions - * */ public function testClearCachedDefinitions() { - $this->setUpEntityManager(); - - $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); - - $this->cacheTagsInvalidator->invalidateTags(['entity_types'])->shouldBeCalled(); - $this->cacheTagsInvalidator->invalidateTags(['entity_bundles'])->shouldBeCalled(); - $this->cacheTagsInvalidator->invalidateTags(['entity_field_info'])->shouldBeCalled(); + $this->entityTypeManager->clearCachedDefinitions()->shouldBeCalled(); + $this->entityTypeRepository->clearCachedDefinitions()->shouldBeCalled(); + $this->entityTypeBundleInfo->clearCachedBundles()->shouldBeCalled(); + $this->entityFieldManager->clearCachedFieldDefinitions()->shouldBeCalled(); $this->entityManager->clearCachedDefinitions(); } - /** - * Tests the processDefinition() method. - * - * @covers ::processDefinition - * - * @expectedException \Drupal\Core\Entity\Exception\InvalidLinkTemplateException - * @expectedExceptionMessage Link template 'canonical' for entity type 'apple' must start with a leading slash, the current link template is 'path/to/apple' - */ - public function testProcessDefinition() { - $apple = $this->prophesize(EntityTypeInterface::class); - $this->setUpEntityManager(array('apple' => $apple)); - - $apple->getLinkTemplates()->willReturn(['canonical' => 'path/to/apple']); - - $definition = $apple->reveal(); - $this->entityManager->processDefinition($definition, 'apple'); - } - - /** - * Tests the getDefinition() method. - * - * @covers ::getDefinition - * - * @dataProvider providerTestGetDefinition - */ - public function testGetDefinition($entity_type_id, $expected) { - $entity = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityManager(array( - 'apple' => $entity, - 'banana' => $entity, - )); - - $entity_type = $this->entityManager->getDefinition($entity_type_id, FALSE); - if ($expected) { - $this->assertInstanceOf(EntityTypeInterface::class, $entity_type); - } - else { - $this->assertNull($entity_type); - } - } - - /** - * Provides test data for testGetDefinition(). - * - * @return array - * Test data. - */ - public function providerTestGetDefinition() { - return array( - array('apple', TRUE), - array('banana', TRUE), - array('pear', FALSE), - ); - } - - /** - * Tests the getDefinition() method with an invalid definition. - * - * @covers ::getDefinition - * - * @expectedException \Drupal\Component\Plugin\Exception\PluginNotFoundException - * @expectedExceptionMessage The "pear" entity type does not exist. - */ - public function testGetDefinitionInvalidException() { - $this->setUpEntityManager(); - - $this->entityManager->getDefinition('pear', TRUE); - } - - /** - * Tests the hasHandler() method. - * - * @covers ::hasHandler - * - * @dataProvider providerTestHasHandler - */ - public function testHasHandler($entity_type_id, $expected) { - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->hasHandlerClass('storage')->willReturn(TRUE); - - $banana = $this->prophesize(EntityTypeInterface::class); - $banana->hasHandlerClass('storage')->willReturn(FALSE); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $entity_type = $this->entityManager->hasHandler($entity_type_id, 'storage'); - $this->assertSame($expected, $entity_type); - } - - /** - * Provides test data for testHasHandler(). - * - * @return array - * Test data. - */ - public function providerTestHasHandler() { - return array( - array('apple', TRUE), - array('banana', FALSE), - array('pear', FALSE), - ); - } - - /** - * Tests the getStorage() method. - * - * @covers ::getStorage - */ - public function testGetStorage() { - $class = $this->getTestHandlerClass(); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('storage')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - $this->assertInstanceOf($class, $this->entityManager->getStorage('test_entity_type')); - } - - /** - * Tests the getListBuilder() method. - * - * @covers ::getListBuilder - */ - public function testGetListBuilder() { - $class = $this->getTestHandlerClass(); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('list_builder')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - $this->assertInstanceOf($class, $this->entityManager->getListBuilder('test_entity_type')); - } - - /** - * Tests the getViewBuilder() method. - * - * @covers ::getViewBuilder - */ - public function testGetViewBuilder() { - $class = $this->getTestHandlerClass(); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('view_builder')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - $this->assertInstanceOf($class, $this->entityManager->getViewBuilder('test_entity_type')); - } - - /** - * Tests the getAccessControlHandler() method. - * - * @covers ::getAccessControlHandler - */ - public function testGetAccessControlHandler() { - $class = $this->getTestHandlerClass(); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('access')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - $this->assertInstanceOf($class, $this->entityManager->getAccessControlHandler('test_entity_type')); - } - - /** - * Tests the getFormObject() method. - * - * @covers ::getFormObject - */ - public function testGetFormObject() { - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getFormClass('default')->willReturn(TestEntityForm::class); - - $banana = $this->prophesize(EntityTypeInterface::class); - $banana->getFormClass('default')->willReturn(TestEntityFormInjected::class); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $apple_form = $this->entityManager->getFormObject('apple', 'default'); - $this->assertInstanceOf(TestEntityForm::class, $apple_form); - $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_form); - $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_form); - - $banana_form = $this->entityManager->getFormObject('banana', 'default'); - $this->assertInstanceOf(TestEntityFormInjected::class, $banana_form); - $this->assertAttributeEquals('yellow', 'color', $banana_form); - - } - - /** - * Tests the getFormObject() method with an invalid operation. - * - * @covers ::getFormObject - * - * @expectedException \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - */ - public function testGetFormObjectInvalidOperation() { - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getFormClass('edit')->willReturn(''); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - $this->entityManager->getFormObject('test_entity_type', 'edit'); - } - - /** - * Tests the getHandler() method. - * - * @covers ::getHandler - */ - public function testGetHandler() { - $class = $this->getTestHandlerClass(); - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getHandlerClass('storage')->willReturn($class); - - $this->setUpEntityManager(array( - 'apple' => $apple, - )); - - $apple_controller = $this->entityManager->getHandler('apple', 'storage'); - $this->assertInstanceOf($class, $apple_controller); - $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_controller); - $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_controller); - } - - /** - * Tests the getHandler() method when no controller is defined. - * - * @covers ::getHandler - * - * @expectedException \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - */ - public function testGetHandlerMissingHandler() { - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('storage')->willReturn(''); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - $this->entityManager->getHandler('test_entity_type', 'storage'); - } - - /** - * Tests the getBaseFieldDefinitions() method. - * - * @covers ::getBaseFieldDefinitions - * @covers ::buildBaseFieldDefinitions - */ - public function testGetBaseFieldDefinitions() { - $field_definition = $this->setUpEntityWithFieldDefinition(); - - $expected = array('id' => $field_definition); - $this->assertSame($expected, $this->entityManager->getBaseFieldDefinitions('test_entity_type')); - } - - /** - * Tests the getFieldDefinitions() method. - * - * @covers ::getFieldDefinitions - * @covers ::buildBundleFieldDefinitions - */ - public function testGetFieldDefinitions() { - $field_definition = $this->setUpEntityWithFieldDefinition(); - - $expected = array('id' => $field_definition); - $this->assertSame($expected, $this->entityManager->getFieldDefinitions('test_entity_type', 'test_entity_bundle')); - } - - /** - * Tests the getFieldStorageDefinitions() method. - * - * @covers ::getFieldStorageDefinitions - * @covers ::buildFieldStorageDefinitions - */ - public function testGetFieldStorageDefinitions() { - $field_definition = $this->setUpEntityWithFieldDefinition(TRUE); - $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class); - $field_storage_definition->getName()->willReturn('field_storage'); - - $definitions = ['field_storage' => $field_storage_definition->reveal()]; - - $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([]); - $this->moduleHandler->getImplementations('entity_field_storage_info')->willReturn(['example_module']); - $this->moduleHandler->invoke('example_module', 'entity_field_storage_info', [$this->entityType])->willReturn($definitions); - $this->moduleHandler->alter('entity_field_storage_info', $definitions, $this->entityType)->willReturn(NULL); - - $expected = array( - 'id' => $field_definition, - 'field_storage' => $field_storage_definition->reveal(), - ); - $this->assertSame($expected, $this->entityManager->getFieldStorageDefinitions('test_entity_type')); - } - - /** - * Tests the getBaseFieldDefinitions() method with a translatable entity type. - * - * @covers ::getBaseFieldDefinitions - * @covers ::buildBaseFieldDefinitions - * - * @dataProvider providerTestGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode - */ - public function testGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode($default_langcode_key) { - $this->setUpEntityWithFieldDefinition(FALSE, 'id', array('langcode' => 'langcode', 'default_langcode' => $default_langcode_key)); - - $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); - $field_definition->isTranslatable()->willReturn(TRUE); - - $entity_class = EntityManagerTestEntity::class; - $entity_class::$baseFieldDefinitions += array('langcode' => $field_definition); - - $this->entityType->isTranslatable()->willReturn(TRUE); - - $definitions = $this->entityManager->getBaseFieldDefinitions('test_entity_type'); - - $this->assertTrue(isset($definitions[$default_langcode_key])); - } - - /** - * Provides test data for testGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode(). - * - * @return array - * Test data. - */ - public function providerTestGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode() { - return [ - ['default_langcode'], - ['custom_default_langcode_key'], - ]; - } - - /** - * Tests the getBaseFieldDefinitions() method with a translatable entity type. - * - * @covers ::getBaseFieldDefinitions - * @covers ::buildBaseFieldDefinitions - * - * @expectedException \LogicException - * @expectedExceptionMessage The Test entity type cannot be translatable as it does not define a translatable "langcode" field. - * - * @dataProvider providerTestGetBaseFieldDefinitionsTranslatableEntityTypeLangcode - */ - public function testGetBaseFieldDefinitionsTranslatableEntityTypeLangcode($provide_key, $provide_field, $translatable) { - $keys = $provide_key ? array('langcode' => 'langcode') : array(); - $this->setUpEntityWithFieldDefinition(FALSE, 'id', $keys); - - if ($provide_field) { - $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); - $field_definition->isTranslatable()->willReturn($translatable); - if (!$translatable) { - $field_definition->setTranslatable(!$translatable)->shouldBeCalled(); - } - - $entity_class = EntityManagerTestEntity::class; - $entity_class::$baseFieldDefinitions += array('langcode' => $field_definition->reveal()); - } - - $this->entityType->isTranslatable()->willReturn(TRUE); - $this->entityType->getLabel()->willReturn('Test'); - - $this->entityManager->getBaseFieldDefinitions('test_entity_type'); - } - - /** - * Provides test data for testGetBaseFieldDefinitionsTranslatableEntityTypeLangcode(). - * - * @return array - * Test data. - */ - public function providerTestGetBaseFieldDefinitionsTranslatableEntityTypeLangcode() { - return [ - [FALSE, TRUE, TRUE], - [TRUE, FALSE, TRUE], - [TRUE, TRUE, FALSE], - ]; - } - - /** - * Tests the getBaseFieldDefinitions() method with caching. - * - * @covers ::getBaseFieldDefinitions - */ - public function testGetBaseFieldDefinitionsWithCaching() { - $field_definition = $this->setUpEntityWithFieldDefinition(); - - $expected = array('id' => $field_definition); - - $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') - ->willReturn(FALSE) - ->shouldBeCalled(); - $this->cacheBackend->set('entity_base_field_definitions:test_entity_type:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) - ->will(function ($args) { - $data = (object) ['data' => $args[1]]; - $this->get('entity_base_field_definitions:test_entity_type:en') - ->willReturn($data) - ->shouldBeCalled(); - }) - ->shouldBeCalled(); - $this->cacheBackend->get('entity_type')->willReturn(FALSE); - $this->cacheBackend->set('entity_type', Argument::any(), Cache::PERMANENT, ['entity_types'])->shouldBeCalled(); - - $this->assertSame($expected, $this->entityManager->getBaseFieldDefinitions('test_entity_type')); - $this->entityManager->testClearEntityFieldInfo(); - $this->assertSame($expected, $this->entityManager->getBaseFieldDefinitions('test_entity_type')); - } - - /** - * Tests the getFieldDefinitions() method with caching. - * - * @covers ::getFieldDefinitions - */ - public function testGetFieldDefinitionsWithCaching() { - $field_definition = $this->setUpEntityWithFieldDefinition(FALSE, 'id'); - - $expected = array('id' => $field_definition); - - $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') - ->willReturn((object) array('data' => $expected)) - ->shouldBeCalledTimes(2); - $this->cacheBackend->get('entity_bundle_field_definitions:test_entity_type:test_bundle:en') - ->willReturn(FALSE) - ->shouldBeCalledTimes(1); - $this->cacheBackend->get('entity_type')->willReturn(FALSE); - $this->cacheBackend->set('entity_type', Argument::any(), Cache::PERMANENT, ['entity_types'])->shouldBeCalled(); - $this->cacheBackend->set('entity_bundle_field_definitions:test_entity_type:test_bundle:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) - ->will(function ($args) { - $data = (object) ['data' => $args[1]]; - $this->get('entity_bundle_field_definitions:test_entity_type:test_bundle:en') - ->willReturn($data) - ->shouldBeCalled(); - }) - ->shouldBeCalled(); - - $this->assertSame($expected, $this->entityManager->getFieldDefinitions('test_entity_type', 'test_bundle')); - $this->entityManager->testClearEntityFieldInfo(); - $this->assertSame($expected, $this->entityManager->getFieldDefinitions('test_entity_type', 'test_bundle')); - } - - /** - * Tests the getFieldStorageDefinitions() method with caching. - * - * @covers ::getFieldStorageDefinitions - */ - public function testGetFieldStorageDefinitionsWithCaching() { - $field_definition = $this->setUpEntityWithFieldDefinition(TRUE, 'id'); - $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class); - $field_storage_definition->getName()->willReturn('field_storage'); - - $definitions = ['field_storage' => $field_storage_definition->reveal()]; - - $this->moduleHandler->getImplementations('entity_field_storage_info')->willReturn(['example_module']); - $this->moduleHandler->invoke('example_module', 'entity_field_storage_info', [$this->entityType])->willReturn($definitions); - $this->moduleHandler->alter('entity_field_storage_info', $definitions, $this->entityType)->willReturn(NULL); - - $expected = array( - 'id' => $field_definition, - 'field_storage' => $field_storage_definition->reveal(), - ); - - $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') - ->willReturn((object) ['data' => ['id' => $expected['id']]]) - ->shouldBeCalledTimes(2); - $this->cacheBackend->get('entity_field_storage_definitions:test_entity_type:en')->willReturn(FALSE); - $this->cacheBackend->get('entity_type')->willReturn(FALSE); - - $this->cacheBackend->set('entity_type', Argument::any(), Cache::PERMANENT, ['entity_types'])->shouldBeCalled(); - $this->cacheBackend->set('entity_field_storage_definitions:test_entity_type:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) - ->will(function () use ($expected) { - $this->get('entity_field_storage_definitions:test_entity_type:en') - ->willReturn((object) ['data' => $expected]) - ->shouldBeCalled(); - }) - ->shouldBeCalled(); - - - $this->assertSame($expected, $this->entityManager->getFieldStorageDefinitions('test_entity_type')); - $this->entityManager->testClearEntityFieldInfo(); - $this->assertSame($expected, $this->entityManager->getFieldStorageDefinitions('test_entity_type')); - } - - /** - * Tests the getBaseFieldDefinitions() method with an invalid definition. - * - * @covers ::getBaseFieldDefinitions - * @covers ::buildBaseFieldDefinitions - * - * @expectedException \LogicException - */ - public function testGetBaseFieldDefinitionsInvalidDefinition() { - $this->setUpEntityWithFieldDefinition(FALSE, 'langcode', array('langcode' => 'langcode')); - - $this->entityType->isTranslatable()->willReturn(TRUE); - $this->entityType->getLabel()->willReturn('the_label'); - - $this->entityManager->getBaseFieldDefinitions('test_entity_type'); - } - - /** - * Tests that getFieldDefinitions() method sets the 'provider' definition key. - * - * @covers ::getFieldDefinitions - * @covers ::buildBundleFieldDefinitions - */ - public function testGetFieldDefinitionsProvider() { - $this->setUpEntityWithFieldDefinition(TRUE); - - $module = 'entity_manager_test_module'; - - // @todo Mock FieldDefinitionInterface once it exposes a proper provider - // setter. See https://www.drupal.org/node/2225961. - $field_definition = $this->prophesize(BaseFieldDefinition::class); - - // We expect two calls as the field definition will be returned from both - // base and bundle entity field info hook implementations. - $field_definition->getProvider()->shouldBeCalled(); - $field_definition->setProvider($module)->shouldBeCalledTimes(2); - $field_definition->setName(0)->shouldBeCalledTimes(2); - $field_definition->setTargetEntityTypeId('test_entity_type')->shouldBeCalled(); - $field_definition->setTargetBundle(NULL)->shouldBeCalled(); - $field_definition->setTargetBundle('test_bundle')->shouldBeCalled(); - - $this->moduleHandler->getImplementations(Argument::type('string'))->willReturn([$module]); - $this->moduleHandler->invoke($module, 'entity_base_field_info', [$this->entityType])->willReturn([$field_definition->reveal()]); - $this->moduleHandler->invoke($module, 'entity_bundle_field_info', Argument::type('array'))->willReturn([$field_definition->reveal()]); - - $this->entityManager->getFieldDefinitions('test_entity_type', 'test_bundle'); - } - - /** - * Prepares an entity that defines a field definition. - * - * @param bool $custom_invoke_all - * (optional) Whether the test will set up its own - * ModuleHandlerInterface::invokeAll() implementation. Defaults to FALSE. - * @param string $field_definition_id - * (optional) The ID to use for the field definition. Defaults to 'id'. - * @param array $entity_keys - * (optional) An array of entity keys for the mocked entity type. Defaults - * to an empty array. - * - * @return \Drupal\Core\Field\BaseFieldDefinition|\Prophecy\Prophecy\ProphecyInterface - * A field definition object. - */ - protected function setUpEntityWithFieldDefinition($custom_invoke_all = FALSE, $field_definition_id = 'id', $entity_keys = array()) { - $field_type_manager = $this->prophesize(FieldTypePluginManagerInterface::class); - $field_type_manager->getDefaultStorageSettings('boolean')->willReturn([]); - $field_type_manager->getDefaultFieldSettings('boolean')->willReturn([]); - $this->container->get('plugin.manager.field.field_type')->willReturn($field_type_manager->reveal()); - - $string_translation = $this->prophesize(TranslationInterface::class); - $this->container->get('string_translation')->willReturn($string_translation->reveal()); - - $entity_class = EntityManagerTestEntity::class; - - $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); - $entity_class::$baseFieldDefinitions = array( - $field_definition_id => $field_definition->reveal(), - ); - $entity_class::$bundleFieldDefinitions = array(); - - if (!$custom_invoke_all) { - $this->moduleHandler->getImplementations(Argument::cetera())->willReturn([]); - } - - // Mock the base field definition override. - $override_entity_type = $this->prophesize(EntityTypeInterface::class); - - $this->entityType = $this->prophesize(EntityTypeInterface::class); - $this->setUpEntityManager(array('test_entity_type' => $this->entityType, 'base_field_override' => $override_entity_type)); - - $override_entity_type->getClass()->willReturn($entity_class); - $override_entity_type->getHandlerClass('storage')->willReturn(TestConfigEntityStorage::class); - - $this->entityType->getClass()->willReturn($entity_class); - $this->entityType->getKeys()->willReturn($entity_keys + ['default_langcode' => 'default_langcode']); - $this->entityType->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); - $this->entityType->isTranslatable()->willReturn(FALSE); - $this->entityType->getProvider()->willReturn('the_provider'); - $this->entityType->id()->willReturn('the_entity_id'); - - return $field_definition->reveal(); - } - - /** - * Tests the clearCachedFieldDefinitions() method. - * - * @covers ::clearCachedFieldDefinitions - */ - public function testClearCachedFieldDefinitions() { - $this->setUpEntityManager(); - - $this->cacheTagsInvalidator->invalidateTags(['entity_field_info'])->shouldBeCalled(); - - $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); - - $this->entityManager->clearCachedFieldDefinitions(); - } - - /** - * Tests the clearCachedBundles() method. - * - * @covers ::clearCachedBundles - */ - public function testClearCachedBundles() { - $this->setUpEntityManager(); - - $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); - - $this->cacheTagsInvalidator->invalidateTags(['entity_bundles'])->shouldBeCalled(); - - $this->entityManager->clearCachedBundles(); - } - - /** - * Tests the getBundleInfo() method. - * - * @covers ::getBundleInfo - * - * @dataProvider providerTestGetBundleInfo - */ - public function testGetBundleInfo($entity_type_id, $expected) { - $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([]); - $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); - - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getLabel()->willReturn('Apple'); - $apple->getBundleOf()->willReturn(NULL); - - $banana = $this->prophesize(EntityTypeInterface::class); - $banana->getLabel()->willReturn('Banana'); - $banana->getBundleOf()->willReturn(NULL); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $bundle_info = $this->entityManager->getBundleInfo($entity_type_id); - $this->assertSame($expected, $bundle_info); - } - - /** - * Provides test data for testGetBundleInfo(). - * - * @return array - * Test data. - */ - public function providerTestGetBundleInfo() { - return array( - array('apple', array( - 'apple' => array( - 'label' => 'Apple', - ), - )), - array('banana', array( - 'banana' => array( - 'label' => 'Banana', - ), - )), - array('pear', array()), - ); - } - - /** - * Tests the getAllBundleInfo() method. - * - * @covers ::getAllBundleInfo - */ - public function testGetAllBundleInfo() { - $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([]); - $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); - - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getLabel()->willReturn('Apple'); - $apple->getBundleOf()->willReturn(NULL); - - $banana = $this->prophesize(EntityTypeInterface::class); - $banana->getLabel()->willReturn('Banana'); - $banana->getBundleOf()->willReturn(NULL); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $this->cacheBackend->get('entity_bundle_info:en')->willReturn(FALSE); - $this->cacheBackend->get('entity_type')->willReturn(FALSE); - $this->cacheBackend->set('entity_type', Argument::any(), Cache::PERMANENT, ['entity_types'])->shouldBeCalled(); - $this->cacheBackend->set('entity_bundle_info:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_bundles']) - ->will(function () { - $this->get('entity_bundle_info:en') - ->willReturn((object) ['data' => 'cached data']) - ->shouldBeCalled(); - }) - ->shouldBeCalled(); - - $this->cacheTagsInvalidator->invalidateTags(['entity_types'])->shouldBeCalled(); - $this->cacheTagsInvalidator->invalidateTags(['entity_bundles'])->shouldBeCalled(); - $this->cacheTagsInvalidator->invalidateTags(['entity_field_info'])->shouldBeCalled(); - - $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); - - $expected = array( - 'apple' => array( - 'apple' => array( - 'label' => 'Apple', - ), - ), - 'banana' => array( - 'banana' => array( - 'label' => 'Banana', - ), - ), - ); - $bundle_info = $this->entityManager->getAllBundleInfo(); - $this->assertSame($expected, $bundle_info); - - $bundle_info = $this->entityManager->getAllBundleInfo(); - $this->assertSame($expected, $bundle_info); - - $this->entityManager->clearCachedDefinitions(); - - $bundle_info = $this->entityManager->getAllBundleInfo(); - $this->assertSame('cached data', $bundle_info); - } - - /** - * Tests the getEntityTypeLabels() method. - * - * @covers ::getEntityTypeLabels - */ - public function testGetEntityTypeLabels() { - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getLabel()->willReturn('Apple'); - $apple->getBundleOf()->willReturn(NULL); - - $banana = $this->prophesize(EntityTypeInterface::class); - $banana->getLabel()->willReturn('Banana'); - $banana->getBundleOf()->willReturn(NULL); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $expected = array( - 'apple' => 'Apple', - 'banana' => 'Banana', - ); - $this->assertSame($expected, $this->entityManager->getEntityTypeLabels()); - } - - /** - * Tests the getTranslationFromContext() method. - * - * @covers ::getTranslationFromContext - */ - public function testGetTranslationFromContext() { - $this->setUpEntityManager(); - - $language = new Language(['id' => 'en']); - $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT) - ->willReturn($language) - ->shouldBeCalledTimes(1); - $this->languageManager->getFallbackCandidates(Argument::type('array')) - ->will(function ($args) { - $context = $args[0]; - $candidates = array(); - if (!empty($context['langcode'])) { - $candidates[$context['langcode']] = $context['langcode']; - } - return $candidates; - }) - ->shouldBeCalledTimes(1); - - $translated_entity = $this->prophesize(ContentEntityInterface::class); - - $entity = $this->prophesize(ContentEntityInterface::class); - $entity->getUntranslated()->willReturn($entity); - $entity->language()->willReturn($language); - $entity->hasTranslation(LanguageInterface::LANGCODE_DEFAULT)->willReturn(FALSE); - $entity->hasTranslation('custom_langcode')->willReturn(TRUE); - $entity->getTranslation('custom_langcode')->willReturn($translated_entity->reveal()); - $entity->getTranslationLanguages()->willReturn([new Language(['id' => 'en']), new Language(['id' => 'custom_langcode'])]); - $entity->addCacheContexts(['languages:language_content'])->shouldBeCalled(); - - $this->assertSame($entity->reveal(), $this->entityManager->getTranslationFromContext($entity->reveal())); - $this->assertSame($translated_entity->reveal(), $this->entityManager->getTranslationFromContext($entity->reveal(), 'custom_langcode')); - } - - /** - * @covers ::getExtraFields - */ - function testGetExtraFields() { - $this->setUpEntityManager(); - - $entity_type_id = $this->randomMachineName(); - $bundle = $this->randomMachineName(); - $language_code = 'en'; - $hook_bundle_extra_fields = array( - $entity_type_id => array( - $bundle => array( - 'form' => array( - 'foo_extra_field' => array( - 'label' => 'Foo', - ), - ), - ), - ), - ); - $processed_hook_bundle_extra_fields = $hook_bundle_extra_fields; - $processed_hook_bundle_extra_fields[$entity_type_id][$bundle] += array( - 'display' => array(), - ); - $cache_id = 'entity_bundle_extra_fields:' . $entity_type_id . ':' . $bundle . ':' . $language_code; - - $language = new Language(array('id' => $language_code)); - $this->languageManager->getCurrentLanguage() - ->willReturn($language) - ->shouldBeCalledTimes(1); - - $this->cacheBackend->get($cache_id)->shouldBeCalled(); - - $this->moduleHandler->invokeAll('entity_extra_field_info')->willReturn($hook_bundle_extra_fields); - $this->moduleHandler->alter('entity_extra_field_info', $hook_bundle_extra_fields)->shouldBeCalled(); - - $this->cacheBackend->set($cache_id, $processed_hook_bundle_extra_fields[$entity_type_id][$bundle], Cache::PERMANENT, ['entity_field_info'])->shouldBeCalled(); - - $this->assertSame($processed_hook_bundle_extra_fields[$entity_type_id][$bundle], $this->entityManager->getExtraFields($entity_type_id, $bundle)); - } - - /** - * @covers ::getFieldMap - */ - public function testGetFieldMap() { - $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([]); - $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); - - // Set up a content entity type. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_class = EntityManagerTestEntity::class; - - // Define an ID field definition as a base field. - $id_definition = $this->prophesize(FieldDefinitionInterface::class); - $id_definition->getType()->willReturn('integer'); - $base_field_definitions = array( - 'id' => $id_definition->reveal(), - ); - $entity_class::$baseFieldDefinitions = $base_field_definitions; - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->getAll()->willReturn([ - 'test_entity_type' => [ - 'by_bundle' => [ - 'type' => 'string', - 'bundles' => ['second_bundle' => 'second_bundle'], - ], - ], - ]); - - // Set up a non-content entity type. - $non_content_entity_type = $this->prophesize(EntityTypeInterface::class); - - // Mock the base field definition override. - $override_entity_type = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityManager(array( - 'test_entity_type' => $entity_type, - 'non_fieldable' => $non_content_entity_type, - 'base_field_override' => $override_entity_type, - )); - - $entity_type->getClass()->willReturn($entity_class); - $entity_type->getKeys()->willReturn(['default_langcode' => 'default_langcode']); - $entity_type->getBundleOf()->willReturn(NULL); - $entity_type->id()->willReturn('test_entity_type'); - $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); - $entity_type->isTranslatable()->shouldBeCalled(); - $entity_type->getProvider()->shouldBeCalled(); - - $non_content_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); - $non_content_entity_type->getBundleOf()->willReturn(NULL); - $non_content_entity_type->getLabel()->shouldBeCalled(); - - $override_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); - $override_entity_type->getHandlerClass('storage')->willReturn(TestConfigEntityStorage::class); - $override_entity_type->getBundleOf()->willReturn(NULL); - $override_entity_type->getLabel()->shouldBeCalled(); - - // Set up the module handler to return two bundles for the fieldable entity - // type. - $this->moduleHandler->alter(Argument::type('string'), Argument::type('array')); - $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([]); - $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([ - 'test_entity_type' => [ - 'first_bundle' => [], - 'second_bundle' => [], - ], - ]); - - $expected = array( - 'test_entity_type' => array( - 'id' => array( - 'type' => 'integer', - 'bundles' => array('first_bundle' => 'first_bundle', 'second_bundle' => 'second_bundle'), - ), - 'by_bundle' => array( - 'type' => 'string', - 'bundles' => array('second_bundle' => 'second_bundle'), - ), - ) - ); - $this->assertEquals($expected, $this->entityManager->getFieldMap()); - } - - /** - * @covers ::getFieldMap - */ - public function testGetFieldMapFromCache() { - $expected = array( - 'test_entity_type' => array( - 'id' => array( - 'type' => 'integer', - 'bundles' => array('first_bundle' => 'first_bundle', 'second_bundle' => 'second_bundle'), - ), - 'by_bundle' => array( - 'type' => 'string', - 'bundles' => array('second_bundle' => 'second_bundle'), - ), - ) - ); - $this->setUpEntityManager(); - $this->cacheBackend->get('entity_field_map')->willReturn((object) array('data' => $expected)); - - // Call the field map twice to make sure the static cache works. - $this->assertEquals($expected, $this->entityManager->getFieldMap()); - $this->assertEquals($expected, $this->entityManager->getFieldMap()); - } - - /** - * @covers ::getFieldMapByFieldType - */ - public function testGetFieldMapByFieldType() { - // Set up a content entity type. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_class = EntityManagerTestEntity::class; - - // Set up the module handler to return two bundles for the fieldable entity - // type. - $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([]); - $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([ - 'test_entity_type' => [ - 'first_bundle' => [], - 'second_bundle' => [], - ], - ]); - $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); - - // Define an ID field definition as a base field. - $id_definition = $this->prophesize(FieldDefinitionInterface::class); - $id_definition->getType()->willReturn('integer'); - $base_field_definitions = array( - 'id' => $id_definition->reveal(), - ); - $entity_class::$baseFieldDefinitions = $base_field_definitions; - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->getAll()->willReturn([ - 'test_entity_type' => [ - 'by_bundle' => [ - 'type' => 'string', - 'bundles' => ['second_bundle' => 'second_bundle'], - ], - ], - ]); - - // Mock the base field definition override. - $override_entity_type = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityManager(array( - 'test_entity_type' => $entity_type, - 'base_field_override' => $override_entity_type, - )); - - $entity_type->getClass()->willReturn($entity_class); - $entity_type->getKeys()->willReturn(['default_langcode' => 'default_langcode']); - $entity_type->id()->willReturn('test_entity_type'); - $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); - $entity_type->getBundleOf()->shouldBeCalled(); - $entity_type->isTranslatable()->shouldBeCalled(); - $entity_type->getProvider()->shouldBeCalled(); - - $override_entity_type->getClass()->willReturn($entity_class); - $override_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); - $override_entity_type->getHandlerClass('storage')->willReturn(TestConfigEntityStorage::class); - $override_entity_type->getBundleOf()->shouldBeCalled(); - $override_entity_type->getLabel()->shouldBeCalled(); - - $integerFields = $this->entityManager->getFieldMapByFieldType('integer'); - $this->assertCount(1, $integerFields['test_entity_type']); - $this->assertArrayNotHasKey('non_fieldable', $integerFields); - $this->assertArrayHasKey('id', $integerFields['test_entity_type']); - $this->assertArrayNotHasKey('by_bundle', $integerFields['test_entity_type']); - - $stringFields = $this->entityManager->getFieldMapByFieldType('string'); - $this->assertCount(1, $stringFields['test_entity_type']); - $this->assertArrayNotHasKey('non_fieldable', $stringFields); - $this->assertArrayHasKey('by_bundle', $stringFields['test_entity_type']); - $this->assertArrayNotHasKey('id', $stringFields['test_entity_type']); - } - - /** - * @covers ::onFieldDefinitionCreate - */ - public function testOnFieldDefinitionCreateNewField() { - $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); - $field_definition->getTargetBundle()->willReturn('test_bundle'); - $field_definition->getName()->willReturn('test_field'); - $field_definition->getType()->willReturn('test_type'); - $field = $field_definition->reveal(); - - $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); - $storage->onFieldDefinitionCreate($field) - ->shouldBeCalledTimes(1); - $this->entityManager->setHandler('storage', 'test_entity_type', $storage->reveal()); - $entity = $this->prophesize(EntityTypeInterface::class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->get('test_entity_type')->willReturn([]); - $key_value_store->set('test_entity_type', [ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['test_bundle' => 'test_bundle'], - ], - ])->shouldBeCalled(); - - $this->entityManager->onFieldDefinitionCreate($field); - } - - /** - * @covers ::onFieldDefinitionCreate - */ - public function testOnFieldDefinitionCreateExistingField() { - $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); - $field_definition->getTargetBundle()->willReturn('test_bundle'); - $field_definition->getName()->willReturn('test_field'); - $field = $field_definition->reveal(); - - $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); - $storage->onFieldDefinitionCreate($field) - ->shouldBeCalledTimes(1); - $this->entityManager->setHandler('storage', 'test_entity_type', $storage->reveal()); - $entity = $this->prophesize(EntityTypeInterface::class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->get('test_entity_type')->willReturn([ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['existing_bundle' => 'existing_bundle'], - ], - ]); - $key_value_store->set('test_entity_type', [ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['existing_bundle' => 'existing_bundle', 'test_bundle' => 'test_bundle'], - ], - ]) - ->shouldBeCalled(); - - $this->entityManager->onFieldDefinitionCreate($field); - } - - /** - * @covers ::onFieldDefinitionUpdate - */ - public function testOnFieldDefinitionUpdate() { - $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); - $field = $field_definition->reveal(); - - $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); - $storage->onFieldDefinitionUpdate($field, $field) - ->shouldBeCalledTimes(1); - $entity = $this->prophesize(EntityTypeInterface::class); - $this->entityManager->setHandler('storage', 'test_entity_type', $storage->reveal()); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - $this->entityManager->onFieldDefinitionUpdate($field, $field); - } - - /** - * @covers ::onFieldDefinitionDelete - */ - public function testOnFieldDefinitionDeleteMultipleBundles() { - $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); - $field_definition->getTargetBundle()->willReturn('test_bundle'); - $field_definition->getName()->willReturn('test_field'); - $field = $field_definition->reveal(); - - $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); - $storage->onFieldDefinitionDelete($field) - ->shouldBeCalledTimes(1); - $this->entityManager->setHandler('storage', 'test_entity_type', $storage->reveal()); - $entity = $this->prophesize(EntityTypeInterface::class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->get('test_entity_type')->willReturn([ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['test_bundle' => 'test_bundle'], - ], - 'second_field' => [ - 'type' => 'test_type', - 'bundles' => ['test_bundle' => 'test_bundle'], - ], - ]); - $key_value_store->set('test_entity_type', [ - 'second_field' => [ - 'type' => 'test_type', - 'bundles' => ['test_bundle' => 'test_bundle'], - ], - ]) - ->shouldBeCalled(); - - $this->entityManager->onFieldDefinitionDelete($field); - } - - - /** - * @covers ::onFieldDefinitionDelete - */ - public function testOnFieldDefinitionDeleteSingleBundles() { - $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); - $field_definition->getTargetBundle()->willReturn('test_bundle'); - $field_definition->getName()->willReturn('test_field'); - $field = $field_definition->reveal(); - - $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); - $storage->onFieldDefinitionDelete($field) - ->shouldBeCalledTimes(1); - $this->entityManager->setHandler('storage', 'test_entity_type', $storage->reveal()); - $entity = $this->prophesize(EntityTypeInterface::class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->get('test_entity_type')->willReturn([ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['test_bundle' => 'test_bundle', 'second_bundle' => 'second_bundle'], - ], - ]); - $key_value_store->set('test_entity_type', [ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['second_bundle' => 'second_bundle'], - ], - ]) - ->shouldBeCalled(); - - $this->entityManager->onFieldDefinitionDelete($field); - } - - /** - * @covers ::getEntityTypeFromClass - */ - public function testGetEntityTypeFromClass() { - $apple = $this->prophesize(EntityTypeInterface::class); - $banana = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $apple->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); - - $banana->getOriginalClass()->willReturn('\Drupal\banana\Entity\Banana'); - $banana->getClass()->willReturn('\Drupal\mango\Entity\Mango'); - $banana->id() - ->willReturn('banana') - ->shouldBeCalledTimes(2); - - $entity_type_id = $this->entityManager->getEntityTypeFromClass('\Drupal\banana\Entity\Banana'); - $this->assertSame('banana', $entity_type_id); - $entity_type_id = $this->entityManager->getEntityTypeFromClass('\Drupal\mango\Entity\Mango'); - $this->assertSame('banana', $entity_type_id); - } - - /** - * @covers ::getEntityTypeFromClass - * - * @expectedException \Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException - * @expectedExceptionMessage The \Drupal\pear\Entity\Pear class does not correspond to an entity type. - */ - public function testGetEntityTypeFromClassNoMatch() { - $apple = $this->prophesize(EntityTypeInterface::class); - $banana = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $apple->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); - $banana->getOriginalClass()->willReturn('\Drupal\banana\Entity\Banana'); - - $this->entityManager->getEntityTypeFromClass('\Drupal\pear\Entity\Pear'); - } - - /** - * @covers ::getEntityTypeFromClass - * - * @expectedException \Drupal\Core\Entity\Exception\AmbiguousEntityClassException - * @expectedExceptionMessage Multiple entity types found for \Drupal\apple\Entity\Apple. - */ - public function testGetEntityTypeFromClassAmbiguous() { - $boskoop = $this->prophesize(EntityTypeInterface::class); - $boskoop->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); - $boskoop->id()->willReturn('boskop'); - - $gala = $this->prophesize(EntityTypeInterface::class); - $gala->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); - $gala->id()->willReturn('gala'); - - $this->setUpEntityManager(array( - 'boskoop' => $boskoop, - 'gala' => $gala, - )); - - $this->entityManager->getEntityTypeFromClass('\Drupal\apple\Entity\Apple'); - } - - /** - * @covers ::getRouteProviders - */ - public function testGetRouteProviders() { - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getRouteProviderClasses()->willReturn(['default' => TestRouteProvider::class]); - - $this->setUpEntityManager(array( - 'apple' => $apple, - )); - - $apple_route_provider = $this->entityManager->getRouteProviders('apple'); - $this->assertInstanceOf(TestRouteProvider::class, $apple_route_provider['default']); - $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_route_provider['default']); - $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_route_provider['default']); - } - - /** - * Gets a mock controller class name. - * - * @return string - * A mock controller class name. - */ - protected function getTestHandlerClass() { - return get_class($this->getMockForAbstractClass(EntityHandlerBase::class)); - } - -} - -/* - * Provides a content entity with dummy static method implementations. - */ -abstract class EntityManagerTestEntity implements \Iterator, ContentEntityInterface { - - /** - * The base field definitions. - * - * @var \Drupal\Core\Field\FieldDefinitionInterface[] - */ - public static $baseFieldDefinitions = array(); - - /** - * The bundle field definitions. - * - * @var array[] - * Keys are entity type IDs, values are arrays of which the keys are bundle - * names and the values are field definitions. - */ - public static $bundleFieldDefinitions = array(); - - /** - * {@inheritdoc} - */ - public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - return static::$baseFieldDefinitions; - } - - /** - * {@inheritdoc} - */ - public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { - return isset(static::$bundleFieldDefinitions[$entity_type->id()][$bundle]) ? static::$bundleFieldDefinitions[$entity_type->id()][$bundle] : array(); - } - -} - -/** - * Provides a testing version of EntityManager with an empty constructor. - */ -class TestEntityManager extends EntityManager { - - /** - * Sets the discovery for the manager. - * - * @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $discovery - * The discovery object. - */ - public function setDiscovery(DiscoveryInterface $discovery) { - $this->discovery = $discovery; - } - - /** - * Allow settings of mock handlers. - * - * @param string $entity_type - * The entity type for this handler. - * @param string $handler_type - * The handler type to create an instance for. - * @param object $handler - * A handler instance. - */ - public function setHandler($handler_type, $entity_type, $handler) { - $this->handlers[$handler_type][$entity_type] = $handler; - } - - /** - * Allows the static caches to be cleared. - */ - public function testClearEntityFieldInfo() { - $this->baseFieldDefinitions = array(); - $this->fieldDefinitions = array(); - $this->fieldStorageDefinitions = array(); - } - -} - -/** - * Provides a test entity handler that uses injection. - */ -class TestEntityHandlerInjected implements EntityHandlerInterface { - - /** - * The color of the entity type. - * - * @var string - */ - protected $color; - - /** - * Constructs a new TestEntityHandlerInjected. - * - * @param string $color - * The color of the entity type. - */ - public function __construct($color) { - $this->color = $color; - } - - /** - * {@inheritdoc} - */ - public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - return new static('yellow'); - } - -} - -/** - * Provides a test entity form. - */ -class TestEntityForm extends EntityHandlerBase { - - /** - * The entity manager. - * - * @var \Drupal\Tests\Core\Entity\TestEntityManager - */ - protected $entityManager; - - /** - * {@inheritdoc} - */ - public function getBaseFormId() { - return 'the_base_form_id'; - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'the_form_id'; - } - - /** - * {@inheritdoc} - */ - public function setEntity(EntityInterface $entity) { - return $this; - } - - /** - * {@inheritdoc} - */ - public function setOperation($operation) { - return $this; - } - - /** - * {@inheritdoc} - */ - public function setEntityManager(EntityManagerInterface $entity_manager) { - $this->entityManager = $entity_manager; - return $this; - } - -} - -/** - * Provides a test entity form that uses injection. - */ -class TestEntityFormInjected extends TestEntityForm implements ContainerInjectionInterface { - - /** - * The color of the entity type. - * - * @var string - */ - protected $color; - - /** - * Constructs a new TestEntityFormInjected. - * - * @param string $color - * The color of the entity type. - */ - public function __construct($color) { - $this->color = $color; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static('yellow'); - } - -} - -/** - * Provides a test entity route provider. - */ -class TestRouteProvider extends EntityHandlerBase { - -} - - -/** - * Provides a test config entity storage for base field overrides. - */ -class TestConfigEntityStorage extends ConfigEntityStorage { - - public function __construct($entity_type) { - } - - public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - return new static( - $entity_type - ); - } - - public function loadMultiple(array $ids = NULL) { - return array(); - } -} - -} - -namespace { - - /** - * Implements hook_entity_type_build(). - */ - function entity_manager_test_module_entity_type_build() { - } } diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityRepositoryTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityRepositoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4d718c79f7eeb04b43314021cc2cd41fd7c8e41f --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/EntityRepositoryTest.php @@ -0,0 +1,94 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Entity\EntityRepositoryTest. + */ + +namespace Drupal\Tests\Core\Entity; + +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityRepository; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; + +/** + * @coversDefaultClass \Drupal\Core\Entity\EntityManager + * @group Entity + */ +class EntityRepositoryTest extends UnitTestCase { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $entityTypeManager; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $languageManager; + + /** + * The entity repository under test. + * + * @var \Drupal\Core\Entity\EntityRepository + */ + protected $entityRepository; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->languageManager = $this->prophesize(LanguageManagerInterface::class); + + $this->entityRepository = new EntityRepository($this->entityTypeManager->reveal(), $this->languageManager->reveal()); + } + + /** + * Tests the getTranslationFromContext() method. + * + * @covers ::getTranslationFromContext + */ + public function testGetTranslationFromContext() { + $language = new Language(['id' => 'en']); + $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT) + ->willReturn($language) + ->shouldBeCalledTimes(1); + $this->languageManager->getFallbackCandidates(Argument::type('array')) + ->will(function ($args) { + $context = $args[0]; + $candidates = array(); + if (!empty($context['langcode'])) { + $candidates[$context['langcode']] = $context['langcode']; + } + return $candidates; + }) + ->shouldBeCalledTimes(1); + + $translated_entity = $this->prophesize(ContentEntityInterface::class); + + $entity = $this->prophesize(ContentEntityInterface::class); + $entity->getUntranslated()->willReturn($entity); + $entity->language()->willReturn($language); + $entity->hasTranslation(LanguageInterface::LANGCODE_DEFAULT)->willReturn(FALSE); + $entity->hasTranslation('custom_langcode')->willReturn(TRUE); + $entity->getTranslation('custom_langcode')->willReturn($translated_entity->reveal()); + $entity->getTranslationLanguages()->willReturn([new Language(['id' => 'en']), new Language(['id' => 'custom_langcode'])]); + $entity->addCacheContexts(['languages:language_content'])->shouldBeCalled(); + + $this->assertSame($entity->reveal(), $this->entityRepository->getTranslationFromContext($entity->reveal())); + $this->assertSame($translated_entity->reveal(), $this->entityRepository->getTranslationFromContext($entity->reveal(), 'custom_langcode')); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeBundleInfoTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeBundleInfoTest.php new file mode 100644 index 0000000000000000000000000000000000000000..82626ec742b0805e4e9a73e455a57e1c77be2bab --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeBundleInfoTest.php @@ -0,0 +1,274 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Entity\EntityTypeBundleInfoTest. + */ + +namespace Drupal\Tests\Core\Entity; + +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Cache\CacheTagsInvalidatorInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeBundleInfo; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\TypedData\TypedDataManagerInterface; +use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * @coversDefaultClass \Drupal\Core\Entity\EntityTypeBundleInfo + * @group Entity + */ +class EntityTypeBundleInfoTest extends UnitTestCase { + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $moduleHandler; + + /** + * The cache backend to use. + * + * @var \Drupal\Core\Cache\CacheBackendInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $cacheBackend; + + /** + * The cache tags invalidator. + * + * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $cacheTagsInvalidator; + + /** + * The typed data manager. + * + * @var \Drupal\Core\TypedData\TypedDataManagerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $typedDataManager; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $entityTypeManager; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * The entity type bundle info under test. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfo + */ + protected $entityTypeBundleInfo; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); + $this->moduleHandler->getImplementations('entity_type_build')->willReturn([]); + $this->moduleHandler->alter('entity_type', Argument::type('array'))->willReturn(NULL); + + $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + + $this->cacheTagsInvalidator = $this->prophesize(CacheTagsInvalidatorInterface::class); + + $language = new Language(['id' => 'en']); + $this->languageManager = $this->prophesize(LanguageManagerInterface::class); + $this->languageManager->getCurrentLanguage()->willReturn($language); + $this->languageManager->getLanguages()->willReturn(['en' => (object) ['id' => 'en']]); + + $this->typedDataManager = $this->prophesize(TypedDataManagerInterface::class); + + $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); + + $container = $this->prophesize(ContainerInterface::class); + $container->get('cache_tags.invalidator')->willReturn($this->cacheTagsInvalidator->reveal()); + //$container->get('typed_data_manager')->willReturn($this->typedDataManager->reveal()); + \Drupal::setContainer($container->reveal()); + + $this->entityTypeBundleInfo = new EntityTypeBundleInfo($this->entityTypeManager->reveal(), $this->languageManager->reveal(), $this->moduleHandler->reveal(), $this->typedDataManager->reveal(), $this->cacheBackend->reveal()); + } + + /** + * Sets up the entity type manager to be tested. + * + * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions + * (optional) An array of entity type definitions. + */ + protected function setUpEntityTypeDefinitions($definitions = []) { + $class = $this->getMockClass(EntityInterface::class); + foreach ($definitions as $key => $entity_type) { + // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called + // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must + // always be mocked. + $entity_type->getLinkTemplates()->willReturn([]); + + // Give the entity type a legitimate class to return. + $entity_type->getClass()->willReturn($class); + + $definitions[$key] = $entity_type->reveal(); + } + + $this->entityTypeManager->getDefinition(Argument::cetera()) + ->will(function ($args) use ($definitions) { + $entity_type_id = $args[0]; + $exception_on_invalid = $args[1]; + if (isset($definitions[$entity_type_id])) { + return $definitions[$entity_type_id]; + } + elseif (!$exception_on_invalid) { + return NULL; + } + else throw new PluginNotFoundException($entity_type_id); + }); + $this->entityTypeManager->getDefinitions()->willReturn($definitions); + + } + + /** + * Tests the clearCachedBundles() method. + * + * @covers ::clearCachedBundles + */ + public function testClearCachedBundles() { + $this->setUpEntityTypeDefinitions(); + + $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); + + $this->cacheTagsInvalidator->invalidateTags(['entity_bundles'])->shouldBeCalled(); + + $this->entityTypeBundleInfo->clearCachedBundles(); + } + + /** + * Tests the getBundleInfo() method. + * + * @covers ::getBundleInfo + * + * @dataProvider providerTestGetBundleInfo + */ + public function testGetBundleInfo($entity_type_id, $expected) { + $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([]); + $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); + + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getLabel()->willReturn('Apple'); + $apple->getBundleOf()->willReturn(NULL); + + $banana = $this->prophesize(EntityTypeInterface::class); + $banana->getLabel()->willReturn('Banana'); + $banana->getBundleOf()->willReturn(NULL); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $bundle_info = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); + $this->assertSame($expected, $bundle_info); + } + + /** + * Provides test data for testGetBundleInfo(). + * + * @return array + * Test data. + */ + public function providerTestGetBundleInfo() { + return [ + ['apple', [ + 'apple' => [ + 'label' => 'Apple', + ], + ]], + ['banana', [ + 'banana' => [ + 'label' => 'Banana', + ], + ]], + ['pear', []], + ]; + } + + /** + * Tests the getAllBundleInfo() method. + * + * @covers ::getAllBundleInfo + */ + public function testGetAllBundleInfo() { + $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([]); + $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); + + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getLabel()->willReturn('Apple'); + $apple->getBundleOf()->willReturn(NULL); + + $banana = $this->prophesize(EntityTypeInterface::class); + $banana->getLabel()->willReturn('Banana'); + $banana->getBundleOf()->willReturn(NULL); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $this->cacheBackend->get('entity_bundle_info:en')->willReturn(FALSE); + $this->cacheBackend->set('entity_bundle_info:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_bundles']) + ->will(function () { + $this->get('entity_bundle_info:en') + ->willReturn((object) ['data' => 'cached data']) + ->shouldBeCalled(); + }) + ->shouldBeCalled(); + + $this->cacheTagsInvalidator->invalidateTags(['entity_bundles'])->shouldBeCalled(); + + $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); + + $expected = [ + 'apple' => [ + 'apple' => [ + 'label' => 'Apple', + ], + ], + 'banana' => [ + 'banana' => [ + 'label' => 'Banana', + ], + ], + ]; + $bundle_info = $this->entityTypeBundleInfo->getAllBundleInfo(); + $this->assertSame($expected, $bundle_info); + + $bundle_info = $this->entityTypeBundleInfo->getAllBundleInfo(); + $this->assertSame($expected, $bundle_info); + + $this->entityTypeBundleInfo->clearCachedBundles(); + + $bundle_info = $this->entityTypeBundleInfo->getAllBundleInfo(); + $this->assertSame('cached data', $bundle_info); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeManagerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c2325d423909023039c9cc6b29a63d745e0fbd52 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeManagerTest.php @@ -0,0 +1,510 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Entity\EntityTypeManagerTest. + */ + +namespace Drupal\Tests\Core\Entity; + +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\EntityHandlerBase; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManager; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * @coversDefaultClass \Drupal\Core\Entity\EntityTypeManager + * @group Entity + */ +class EntityTypeManagerTest extends UnitTestCase { + + /** + * The entity type manager under test. + * + * @var \Drupal\Core\Entity\EntityTypeManager + */ + protected $entityTypeManager; + + /** + * The translation manager. + * + * @var \Drupal\Core\StringTranslation\TranslationInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $translationManager; + + /** + * The plugin discovery. + * + * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $discovery; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $moduleHandler; + + /** + * The cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $cacheBackend; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); + $this->moduleHandler->getImplementations('entity_type_build')->willReturn([]); + $this->moduleHandler->alter('entity_type', Argument::type('array'))->willReturn(NULL); + + $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); + $this->translationManager = $this->prophesize(TranslationInterface::class); + + $this->entityTypeManager = new TestEntityTypeManager(new \ArrayObject(), $this->moduleHandler->reveal(), $this->cacheBackend->reveal(), $this->translationManager->reveal(), $this->getClassResolverStub()); + $this->discovery = $this->prophesize(DiscoveryInterface::class); + $this->entityTypeManager->setDiscovery($this->discovery->reveal()); + } + + /** + * Sets up the entity type manager to be tested. + * + * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions + * (optional) An array of entity type definitions. + */ + protected function setUpEntityTypeDefinitions($definitions = []) { + $class = $this->getMockClass(EntityInterface::class); + foreach ($definitions as $key => $entity_type) { + // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called + // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must + // always be mocked. + $entity_type->getLinkTemplates()->willReturn([]); + + // Give the entity type a legitimate class to return. + $entity_type->getClass()->willReturn($class); + + $definitions[$key] = $entity_type->reveal(); + } + + $this->discovery->getDefinition(Argument::cetera()) + ->will(function ($args) use ($definitions) { + $entity_type_id = $args[0]; + $exception_on_invalid = $args[1]; + if (isset($definitions[$entity_type_id])) { + return $definitions[$entity_type_id]; + } + elseif (!$exception_on_invalid) { + return NULL; + } + else throw new PluginNotFoundException($entity_type_id); + }); + $this->discovery->getDefinitions()->willReturn($definitions); + + } + + /** + * Tests the hasHandler() method. + * + * @covers ::hasHandler + * + * @dataProvider providerTestHasHandler + */ + public function testHasHandler($entity_type_id, $expected) { + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->hasHandlerClass('storage')->willReturn(TRUE); + + $banana = $this->prophesize(EntityTypeInterface::class); + $banana->hasHandlerClass('storage')->willReturn(FALSE); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $entity_type = $this->entityTypeManager->hasHandler($entity_type_id, 'storage'); + $this->assertSame($expected, $entity_type); + } + + /** + * Provides test data for testHasHandler(). + * + * @return array + * Test data. + */ + public function providerTestHasHandler() { + return [ + ['apple', TRUE], + ['banana', FALSE], + ['pear', FALSE], + ]; + } + + /** + * Tests the getStorage() method. + * + * @covers ::getStorage + */ + public function testGetStorage() { + $class = $this->getTestHandlerClass(); + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getHandlerClass('storage')->willReturn($class); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + + $this->assertInstanceOf($class, $this->entityTypeManager->getStorage('test_entity_type')); + } + + /** + * Tests the getListBuilder() method. + * + * @covers ::getListBuilder + */ + public function testGetListBuilder() { + $class = $this->getTestHandlerClass(); + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getHandlerClass('list_builder')->willReturn($class); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + + $this->assertInstanceOf($class, $this->entityTypeManager->getListBuilder('test_entity_type')); + } + + /** + * Tests the getViewBuilder() method. + * + * @covers ::getViewBuilder + */ + public function testGetViewBuilder() { + $class = $this->getTestHandlerClass(); + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getHandlerClass('view_builder')->willReturn($class); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + + $this->assertInstanceOf($class, $this->entityTypeManager->getViewBuilder('test_entity_type')); + } + + /** + * Tests the getAccessControlHandler() method. + * + * @covers ::getAccessControlHandler + */ + public function testGetAccessControlHandler() { + $class = $this->getTestHandlerClass(); + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getHandlerClass('access')->willReturn($class); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + + $this->assertInstanceOf($class, $this->entityTypeManager->getAccessControlHandler('test_entity_type')); + } + + /** + * Tests the getFormObject() method. + * + * @covers ::getFormObject + */ + public function testGetFormObject() { + $entity_manager = $this->prophesize(EntityManagerInterface::class); + $container = $this->prophesize(ContainerInterface::class); + $container->get('entity.manager')->willReturn($entity_manager->reveal()); + \Drupal::setContainer($container->reveal()); + + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getFormClass('default')->willReturn(TestEntityForm::class); + + $banana = $this->prophesize(EntityTypeInterface::class); + $banana->getFormClass('default')->willReturn(TestEntityFormInjected::class); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $apple_form = $this->entityTypeManager->getFormObject('apple', 'default'); + $this->assertInstanceOf(TestEntityForm::class, $apple_form); + $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_form); + $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_form); + + $banana_form = $this->entityTypeManager->getFormObject('banana', 'default'); + $this->assertInstanceOf(TestEntityFormInjected::class, $banana_form); + $this->assertAttributeEquals('yellow', 'color', $banana_form); + + } + + /** + * Tests the getFormObject() method with an invalid operation. + * + * @covers ::getFormObject + * + * @expectedException \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + public function testGetFormObjectInvalidOperation() { + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getFormClass('edit')->willReturn(''); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + + $this->entityTypeManager->getFormObject('test_entity_type', 'edit'); + } + + /** + * Tests the getHandler() method. + * + * @covers ::getHandler + */ + public function testGetHandler() { + $class = $this->getTestHandlerClass(); + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getHandlerClass('storage')->willReturn($class); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + ]); + + $apple_controller = $this->entityTypeManager->getHandler('apple', 'storage'); + $this->assertInstanceOf($class, $apple_controller); + $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_controller); + $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_controller); + } + + /** + * Tests the getHandler() method when no controller is defined. + * + * @covers ::getHandler + * + * @expectedException \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + public function testGetHandlerMissingHandler() { + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getHandlerClass('storage')->willReturn(''); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + $this->entityTypeManager->getHandler('test_entity_type', 'storage'); + } + + /** + * @covers ::getRouteProviders + */ + public function testGetRouteProviders() { + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getRouteProviderClasses()->willReturn(['default' => TestRouteProvider::class]); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + ]); + + $apple_route_provider = $this->entityTypeManager->getRouteProviders('apple'); + $this->assertInstanceOf(TestRouteProvider::class, $apple_route_provider['default']); + $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_route_provider['default']); + $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_route_provider['default']); + } + + /** + * Tests the processDefinition() method. + * + * @covers ::processDefinition + * + * @expectedException \Drupal\Core\Entity\Exception\InvalidLinkTemplateException + * @expectedExceptionMessage Link template 'canonical' for entity type 'apple' must start with a leading slash, the current link template is 'path/to/apple' + */ + public function testProcessDefinition() { + $apple = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityTypeDefinitions(['apple' => $apple]); + + $apple->getLinkTemplates()->willReturn(['canonical' => 'path/to/apple']); + + $definition = $apple->reveal(); + $this->entityTypeManager->processDefinition($definition, 'apple'); + } + + /** + * Tests the getDefinition() method. + * + * @covers ::getDefinition + * + * @dataProvider providerTestGetDefinition + */ + public function testGetDefinition($entity_type_id, $expected) { + $entity = $this->prophesize(EntityTypeInterface::class); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $entity, + 'banana' => $entity, + ]); + + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id, FALSE); + if ($expected) { + $this->assertInstanceOf(EntityTypeInterface::class, $entity_type); + } + else { + $this->assertNull($entity_type); + } + } + + /** + * Provides test data for testGetDefinition(). + * + * @return array + * Test data. + */ + public function providerTestGetDefinition() { + return [ + ['apple', TRUE], + ['banana', TRUE], + ['pear', FALSE], + ]; + } + + /** + * Tests the getDefinition() method with an invalid definition. + * + * @covers ::getDefinition + * + * @expectedException \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @expectedExceptionMessage The "pear" entity type does not exist. + */ + public function testGetDefinitionInvalidException() { + $this->setUpEntityTypeDefinitions(); + + $this->entityTypeManager->getDefinition('pear', TRUE); + } + + /** + * Gets a mock controller class name. + * + * @return string + * A mock controller class name. + */ + protected function getTestHandlerClass() { + return get_class($this->getMockForAbstractClass(EntityHandlerBase::class)); + } + +} + +class TestEntityTypeManager extends EntityTypeManager { + + /** + * Sets the discovery for the manager. + * + * @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $discovery + * The discovery object. + */ + public function setDiscovery(DiscoveryInterface $discovery) { + $this->discovery = $discovery; + } + +} + +/** + * Provides a test entity form. + */ +class TestEntityForm extends EntityHandlerBase { + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * {@inheritdoc} + */ + public function getBaseFormId() { + return 'the_base_form_id'; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'the_form_id'; + } + + /** + * {@inheritdoc} + */ + public function setEntity(EntityInterface $entity) { + return $this; + } + + /** + * {@inheritdoc} + */ + public function setOperation($operation) { + return $this; + } + + /** + * {@inheritdoc} + */ + public function setEntityManager(EntityManagerInterface $entity_manager) { + $this->entityManager = $entity_manager; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + return $this; + } + +} + +/** + * Provides a test entity form that uses injection. + */ +class TestEntityFormInjected extends TestEntityForm implements ContainerInjectionInterface { + + /** + * The color of the entity type. + * + * @var string + */ + protected $color; + + /** + * Constructs a new TestEntityFormInjected. + * + * @param string $color + * The color of the entity type. + */ + public function __construct($color) { + $this->color = $color; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static('yellow'); + } + +} + +/** + * Provides a test entity route provider. + */ +class TestRouteProvider extends EntityHandlerBase { + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeRepositoryTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeRepositoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9ec3853f4a90f7a3adea83c752a7645e2426092e --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeRepositoryTest.php @@ -0,0 +1,180 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Entity\EntityTypeRepositoryTest. + */ + +namespace Drupal\Tests\Core\Entity; + +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\EntityTypeRepository; +use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; + +/** + * @coversDefaultClass \Drupal\Core\Entity\EntityTypeRepository + * @group Entity + */ +class EntityTypeRepositoryTest extends UnitTestCase { + + /** + * The entity type repository under test. + * + * @var \Drupal\Core\Entity\EntityTypeRepository + */ + protected $entityTypeRepository; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $entityTypeManager; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + + $this->entityTypeRepository = new EntityTypeRepository($this->entityTypeManager->reveal()); + } + + /** + * Sets up the entity type manager to be tested. + * + * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions + * (optional) An array of entity type definitions. + */ + protected function setUpEntityTypeDefinitions($definitions = []) { + $class = $this->getMockClass(EntityInterface::class); + foreach ($definitions as $key => $entity_type) { + // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called + // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must + // always be mocked. + $entity_type->getLinkTemplates()->willReturn([]); + + // Give the entity type a legitimate class to return. + $entity_type->getClass()->willReturn($class); + + $definitions[$key] = $entity_type->reveal(); + } + + $this->entityTypeManager->getDefinition(Argument::cetera()) + ->will(function ($args) use ($definitions) { + $entity_type_id = $args[0]; + $exception_on_invalid = $args[1]; + if (isset($definitions[$entity_type_id])) { + return $definitions[$entity_type_id]; + } + elseif (!$exception_on_invalid) { + return NULL; + } + else throw new PluginNotFoundException($entity_type_id); + }); + $this->entityTypeManager->getDefinitions()->willReturn($definitions); + } + + /** + * Tests the getEntityTypeLabels() method. + * + * @covers ::getEntityTypeLabels + */ + public function testGetEntityTypeLabels() { + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getLabel()->willReturn('Apple'); + $apple->getBundleOf()->willReturn(NULL); + + $banana = $this->prophesize(EntityTypeInterface::class); + $banana->getLabel()->willReturn('Banana'); + $banana->getBundleOf()->willReturn(NULL); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $expected = [ + 'apple' => 'Apple', + 'banana' => 'Banana', + ]; + $this->assertSame($expected, $this->entityTypeRepository->getEntityTypeLabels()); + } + + /** + * @covers ::getEntityTypeFromClass + */ + public function testGetEntityTypeFromClass() { + $apple = $this->prophesize(EntityTypeInterface::class); + $banana = $this->prophesize(EntityTypeInterface::class); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $apple->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); + + $banana->getOriginalClass()->willReturn('\Drupal\banana\Entity\Banana'); + $banana->getClass()->willReturn('\Drupal\mango\Entity\Mango'); + $banana->id() + ->willReturn('banana') + ->shouldBeCalledTimes(2); + + $entity_type_id = $this->entityTypeRepository->getEntityTypeFromClass('\Drupal\banana\Entity\Banana'); + $this->assertSame('banana', $entity_type_id); + $entity_type_id = $this->entityTypeRepository->getEntityTypeFromClass('\Drupal\mango\Entity\Mango'); + $this->assertSame('banana', $entity_type_id); + } + + /** + * @covers ::getEntityTypeFromClass + * + * @expectedException \Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException + * @expectedExceptionMessage The \Drupal\pear\Entity\Pear class does not correspond to an entity type. + */ + public function testGetEntityTypeFromClassNoMatch() { + $apple = $this->prophesize(EntityTypeInterface::class); + $banana = $this->prophesize(EntityTypeInterface::class); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $apple->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); + $banana->getOriginalClass()->willReturn('\Drupal\banana\Entity\Banana'); + + $this->entityTypeRepository->getEntityTypeFromClass('\Drupal\pear\Entity\Pear'); + } + + /** + * @covers ::getEntityTypeFromClass + * + * @expectedException \Drupal\Core\Entity\Exception\AmbiguousEntityClassException + * @expectedExceptionMessage Multiple entity types found for \Drupal\apple\Entity\Apple. + */ + public function testGetEntityTypeFromClassAmbiguous() { + $boskoop = $this->prophesize(EntityTypeInterface::class); + $boskoop->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); + $boskoop->id()->willReturn('boskop'); + + $gala = $this->prophesize(EntityTypeInterface::class); + $gala->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); + $gala->id()->willReturn('gala'); + + $this->setUpEntityTypeDefinitions([ + 'boskoop' => $boskoop, + 'gala' => $gala, + ]); + + $this->entityTypeRepository->getEntityTypeFromClass('\Drupal\apple\Entity\Apple'); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Field/FieldDefinitionListenerTest.php b/core/tests/Drupal/Tests/Core/Field/FieldDefinitionListenerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8ca9ae0334388049dc891f25c7a9d010dc60314a --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Field/FieldDefinitionListenerTest.php @@ -0,0 +1,275 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Field\FieldDefinitionListenerTest. + */ + +namespace Drupal\Tests\Core\Field; + +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldDefinitionListener; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreInterface; +use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; + +/** + * @coversDefaultClass \Drupal\Core\Field\FieldDefinitionListener + * @group Field + */ +class FieldDefinitionListenerTest extends UnitTestCase { + + /** + * The key-value factory. + * + * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $keyValueFactory; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $entityTypeManager; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $entityFieldManager; + + /** + * The cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $cacheBackend; + + /** + * The field definition listener under test. + * + * @var \Drupal\Core\Field\FieldDefinitionListener + */ + protected $fieldDefinitionListener; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->keyValueFactory = $this->prophesize(KeyValueFactoryInterface::class); + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class); + $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); + + $this->fieldDefinitionListener = new FieldDefinitionListener($this->entityTypeManager->reveal(), $this->entityFieldManager->reveal(), $this->keyValueFactory->reveal(), $this->cacheBackend->reveal()); + } + + /** + * Sets up the entity manager to be tested. + * + * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions + * (optional) An array of entity type definitions. + */ + protected function setUpEntityManager($definitions = array()) { + $class = $this->getMockClass(EntityInterface::class); + foreach ($definitions as $key => $entity_type) { + // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called + // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must + // always be mocked. + $entity_type->getLinkTemplates()->willReturn([]); + + // Give the entity type a legitimate class to return. + $entity_type->getClass()->willReturn($class); + + $definitions[$key] = $entity_type->reveal(); + } + + $this->entityTypeManager->getDefinition(Argument::cetera()) + ->will(function ($args) use ($definitions) { + $entity_type_id = $args[0]; + $exception_on_invalid = $args[1]; + if (isset($definitions[$entity_type_id])) { + return $definitions[$entity_type_id]; + } + elseif (!$exception_on_invalid) { + return NULL; + } + else throw new PluginNotFoundException($entity_type_id); + }); + $this->entityTypeManager->getDefinitions()->willReturn($definitions); + } + + /** + * @covers ::onFieldDefinitionCreate + */ + public function testOnFieldDefinitionCreateNewField() { + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); + $field_definition->getTargetBundle()->willReturn('test_bundle'); + $field_definition->getName()->willReturn('test_field'); + $field_definition->getType()->willReturn('test_type'); + + $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); + $storage->onFieldDefinitionCreate($field_definition->reveal())->shouldBeCalledTimes(1); + $this->entityTypeManager->getStorage('test_entity_type')->willReturn($storage->reveal()); + + $entity = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityManager(['test_entity_type' => $entity]); + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); + $key_value_store->get('test_entity_type')->willReturn([]); + $key_value_store->set('test_entity_type', [ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['test_bundle' => 'test_bundle'], + ], + ])->shouldBeCalled(); + + $this->fieldDefinitionListener->onFieldDefinitionCreate($field_definition->reveal()); + } + + /** + * @covers ::onFieldDefinitionCreate + */ + public function testOnFieldDefinitionCreateExistingField() { + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); + $field_definition->getTargetBundle()->willReturn('test_bundle'); + $field_definition->getName()->willReturn('test_field'); + + $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); + $storage->onFieldDefinitionCreate($field_definition->reveal())->shouldBeCalledTimes(1); + $this->entityTypeManager->getStorage('test_entity_type')->willReturn($storage->reveal()); + + $entity = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityManager(['test_entity_type' => $entity]); + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); + $key_value_store->get('test_entity_type')->willReturn([ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['existing_bundle' => 'existing_bundle'], + ], + ]); + $key_value_store->set('test_entity_type', [ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['existing_bundle' => 'existing_bundle', 'test_bundle' => 'test_bundle'], + ], + ]) + ->shouldBeCalled(); + + $this->fieldDefinitionListener->onFieldDefinitionCreate($field_definition->reveal()); + } + + /** + * @covers ::onFieldDefinitionUpdate + */ + public function testOnFieldDefinitionUpdate() { + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); + + $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); + $storage->onFieldDefinitionUpdate($field_definition->reveal(), $field_definition->reveal())->shouldBeCalledTimes(1); + $this->entityTypeManager->getStorage('test_entity_type')->willReturn($storage->reveal()); + + $entity = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityManager(['test_entity_type' => $entity]); + + $this->fieldDefinitionListener->onFieldDefinitionUpdate($field_definition->reveal(), $field_definition->reveal()); + } + + /** + * @covers ::onFieldDefinitionDelete + */ + public function testOnFieldDefinitionDeleteMultipleBundles() { + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); + $field_definition->getTargetBundle()->willReturn('test_bundle'); + $field_definition->getName()->willReturn('test_field'); + + $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); + $storage->onFieldDefinitionDelete($field_definition->reveal())->shouldBeCalledTimes(1); + $this->entityTypeManager->getStorage('test_entity_type')->willReturn($storage->reveal()); + + $entity = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityManager(['test_entity_type' => $entity]); + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); + $key_value_store->get('test_entity_type')->willReturn([ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['test_bundle' => 'test_bundle'], + ], + 'second_field' => [ + 'type' => 'test_type', + 'bundles' => ['test_bundle' => 'test_bundle'], + ], + ]); + $key_value_store->set('test_entity_type', [ + 'second_field' => [ + 'type' => 'test_type', + 'bundles' => ['test_bundle' => 'test_bundle'], + ], + ]) + ->shouldBeCalled(); + + $this->fieldDefinitionListener->onFieldDefinitionDelete($field_definition->reveal()); + } + + + /** + * @covers ::onFieldDefinitionDelete + */ + public function testOnFieldDefinitionDeleteSingleBundles() { + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); + $field_definition->getTargetBundle()->willReturn('test_bundle'); + $field_definition->getName()->willReturn('test_field'); + + $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); + $storage->onFieldDefinitionDelete($field_definition->reveal())->shouldBeCalledTimes(1); + $this->entityTypeManager->getStorage('test_entity_type')->willReturn($storage->reveal()); + + $entity = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityManager(['test_entity_type' => $entity]); + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); + $key_value_store->get('test_entity_type')->willReturn([ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['test_bundle' => 'test_bundle', 'second_bundle' => 'second_bundle'], + ], + ]); + $key_value_store->set('test_entity_type', [ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['second_bundle' => 'second_bundle'], + ], + ]) + ->shouldBeCalled(); + + $this->fieldDefinitionListener->onFieldDefinitionDelete($field_definition->reveal()); + } + +}