diff --git a/core/core.services.yml b/core/core.services.yml index a842043e12d66e2e04939a03041140db01135bef..20e5da30d887879ea752927913e8ad52783bf274 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -564,7 +564,7 @@ services: 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'] + arguments: ['@entity_type.manager', '@language_manager', '@context.repository'] entity_display.repository: class: Drupal\Core\Entity\EntityDisplayRepository arguments: ['@entity_type.manager', '@module_handler', '@cache.discovery', '@language_manager'] @@ -1005,7 +1005,7 @@ services: class: Drupal\Core\ParamConverter\EntityConverter tags: - { name: paramconverter } - arguments: ['@entity_type.manager', '@language_manager', '@entity.repository'] + arguments: ['@entity_type.manager', '@entity.repository'] paramconverter.entity_revision: class: Drupal\Core\ParamConverter\EntityRevisionParamConverter tags: @@ -1016,7 +1016,7 @@ services: tags: # Use a higher priority than EntityConverter, see the class for details. - { name: paramconverter, priority: 5 } - arguments: ['@entity_type.manager', '@config.factory', '@router.admin_context', '@language_manager', '@entity.repository'] + arguments: ['@entity_type.manager', '@config.factory', '@router.admin_context', '@entity.repository'] lazy: true route_subscriber.module: class: Drupal\Core\EventSubscriber\ModuleRouteSubscriber diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index 56fe0a72185acb1b1302fba7e1c1ae79cef5664d..e46d9f9f489e6bd65d2f812209f3960517ba59bd 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -380,6 +380,61 @@ public function getTranslationFromContext(EntityInterface $entity, $langcode = N return $this->container->get('entity.repository')->getTranslationFromContext($entity, $langcode, $context); } + /** + * {@inheritdoc} + * + * @deprecated in Drupal 8.7.0, will be removed before Drupal 9.0.0. + * Use \Drupal\Core\Entity\EntityRepositoryInterface::getActive() instead. + * + * @see https://www.drupal.org/node/2549139 + */ + public function getActive($entity_type_id, $entity_id, array $contexts = NULL) { + @trigger_error('EntityManagerInterface::getActive() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityRepositoryInterface::getActive() instead. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); + return $this->container->get('entity.repository')->getActive($entity_type_id, $entity_id, $contexts); + } + + /** + * {@inheritdoc} + * + * @deprecated in Drupal 8.7.0, will be removed before Drupal 9.0.0. + * Use \Drupal\Core\Entity\EntityRepositoryInterface::getActiveMultiple() + * instead. + * + * @see https://www.drupal.org/node/2549139 + */ + public function getActiveMultiple($entity_type_id, array $entity_ids, array $contexts = NULL) { + @trigger_error('EntityManagerInterface::getActiveMultiple() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityRepositoryInterface::getActiveMultiple() instead. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); + return $this->container->get('entity.repository')->getActiveMultiple($entity_type_id, $entity_ids, $contexts); + } + + /** + * {@inheritdoc} + * + * @deprecated in Drupal 8.7.0, will be removed before Drupal 9.0.0. + * Use \Drupal\Core\Entity\EntityRepositoryInterface::getCanonical() + * instead. + * + * @see https://www.drupal.org/node/2549139 + */ + public function getCanonical($entity_type_id, $entity_id, array $contexts = NULL) { + @trigger_error('EntityManagerInterface::getCanonical() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityRepositoryInterface::getCanonical() instead. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); + return $this->container->get('entity.repository')->getCanonical($entity_type_id, $entity_id, $contexts); + } + + /** + * {@inheritdoc} + * + * @deprecated in Drupal 8.7.0, will be removed before Drupal 9.0.0. + * Use \Drupal\Core\Entity\EntityRepositoryInterface::getCanonicalMultiple() + * instead. + * + * @see https://www.drupal.org/node/2549139 + */ + public function getCanonicalMultiple($entity_type_id, array $entity_ids, array $contexts = NULL) { + @trigger_error('EntityManagerInterface::getCanonicalMultiple() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityRepositoryInterface::getCanonicalMultiple() instead. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); + return $this->container->get('entity.repository')->getCanonicalMultiple($entity_type_id, $entity_ids, $contexts); + } + /** * {@inheritdoc} * diff --git a/core/lib/Drupal/Core/Entity/EntityRepository.php b/core/lib/Drupal/Core/Entity/EntityRepository.php index 37a89d793ebd4cd8548af1079f0e9def8be134a7..ab62bd9b096ccd7265f069cdb1edfb57ee8a3f86 100644 --- a/core/lib/Drupal/Core/Entity/EntityRepository.php +++ b/core/lib/Drupal/Core/Entity/EntityRepository.php @@ -5,6 +5,7 @@ use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Plugin\Context\ContextRepositoryInterface; use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface; /** @@ -26,6 +27,13 @@ class EntityRepository implements EntityRepositoryInterface { */ protected $languageManager; + /** + * The context repository service. + * + * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface + */ + protected $contextRepository; + /** * Constructs a new EntityRepository. * @@ -33,10 +41,19 @@ class EntityRepository implements EntityRepositoryInterface { * The entity type manager. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * The language manager. + * @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository + * The context repository service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, ContextRepositoryInterface $context_repository = NULL) { $this->entityTypeManager = $entity_type_manager; $this->languageManager = $language_manager; + if (isset($context_repository)) { + $this->contextRepository = $context_repository; + } + else { + @trigger_error('The context.repository service must be passed to EntityRepository::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2938929.', E_USER_DEPRECATED); + $this->contextRepository = \Drupal::service('context.repository'); + } } /** @@ -112,4 +129,178 @@ public function getTranslationFromContext(EntityInterface $entity, $langcode = N return $translation; } + /** + * {@inheritdoc} + */ + public function getActive($entity_type_id, $entity_id, array $contexts = NULL) { + return current($this->getActiveMultiple($entity_type_id, [$entity_id], $contexts)) ?: NULL; + } + + /** + * {@inheritdoc} + */ + public function getActiveMultiple($entity_type_id, array $entity_ids, array $contexts = NULL) { + $active = []; + + if (!isset($contexts)) { + $contexts = $this->contextRepository->getAvailableContexts(); + } + + // @todo Consider implementing a more performant version of this logic fully + // supporting multiple entities in https://www.drupal.org/node/3031082. + $langcode = $this->languageManager->isMultilingual() + ? $this->getContentLanguageFromContexts($contexts) + : $this->languageManager->getDefaultLanguage()->getId(); + + $entities = $this->entityTypeManager + ->getStorage($entity_type_id) + ->loadMultiple($entity_ids); + + foreach ($entities as $id => $entity) { + // Retrieve the fittest revision, if needed. + if ($entity instanceof RevisionableInterface && $entity->getEntityType()->isRevisionable()) { + $entity = $this->getLatestTranslationAffectedRevision($entity, $langcode); + } + + // Retrieve the fittest translation, if needed. + if ($entity instanceof TranslatableInterface) { + $entity = $this->getTranslationFromContext($entity, $langcode); + } + + $active[$id] = $entity; + } + + return $active; + } + + /** + * {@inheritdoc} + */ + public function getCanonical($entity_type_id, $entity_id, array $contexts = NULL) { + return current($this->getCanonicalMultiple($entity_type_id, [$entity_id], $contexts)) ?: NULL; + } + + /** + * {@inheritdoc} + */ + public function getCanonicalMultiple($entity_type_id, array $entity_ids, array $contexts = NULL) { + $entities = $this->entityTypeManager->getStorage($entity_type_id) + ->loadMultiple($entity_ids); + + if (!$entities || !$this->languageManager->isMultilingual()) { + return $entities; + } + + if (!isset($contexts)) { + $contexts = $this->contextRepository->getAvailableContexts(); + } + + // @todo Consider deprecating the legacy context operation altogether in + // https://www.drupal.org/node/3031124. + $legacy_context = []; + $key = static::CONTEXT_ID_LEGACY_CONTEXT_OPERATION; + if (isset($contexts[$key])) { + $legacy_context['operation'] = $contexts[$key]->getContextValue(); + } + + $canonical = []; + $langcode = $this->getContentLanguageFromContexts($contexts); + foreach ($entities as $id => $entity) { + $canonical[$id] = $this->getTranslationFromContext($entity, $langcode, $legacy_context); + } + + return $canonical; + } + + /** + * Retrieves the current content language from the specified contexts. + * + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + * An array of context items. + * + * @return string|null + * A language code or NULL if no language context was provided. + */ + protected function getContentLanguageFromContexts(array $contexts) { + // Content language might not be configurable, in which case we need to fall + // back to a configurable language type. + foreach ([LanguageInterface::TYPE_CONTENT, LanguageInterface::TYPE_INTERFACE] as $language_type) { + $context_id = '@language.current_language_context:' . $language_type; + if (isset($contexts[$context_id])) { + return $contexts[$context_id]->getContextValue()->getId(); + } + } + return $this->languageManager->getDefaultLanguage()->getId(); + } + + /** + * Returns the latest revision translation of the specified entity. + * + * @param \Drupal\Core\Entity\RevisionableInterface $entity + * The default revision of the entity being converted. + * @param string $langcode + * The language of the revision translation to be loaded. + * + * @return \Drupal\Core\Entity\RevisionableInterface + * The latest translation-affecting revision for the specified entity, or + * just the latest revision, if the specified entity is not translatable or + * does not have a matching translation yet. + */ + protected function getLatestTranslationAffectedRevision(RevisionableInterface $entity, $langcode) { + $revision = NULL; + $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); + + if ($entity instanceof TranslatableRevisionableInterface && $entity->isTranslatable()) { + /** @var \Drupal\Core\Entity\TranslatableRevisionableStorageInterface $storage */ + $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode); + + // If the latest translation-affecting revision was a default revision, it + // is fine to load the latest revision instead, because in this case the + // latest revision, regardless of it being default or pending, will always + // contain the most up-to-date values for the specified translation. This + // provides a BC behavior when the route is defined by a module always + // expecting the latest revision to be loaded and to be the default + // revision. In this particular case the latest revision is always going + // to be the default revision, since pending revisions would not be + // supported. + $revision = $revision_id ? $this->loadRevision($entity, $revision_id) : NULL; + if (!$revision || ($revision->wasDefaultRevision() && !$revision->isDefaultRevision())) { + $revision = NULL; + } + } + + // Fall back to the latest revisions if no affected revision for the current + // content language could be found. This is acceptable as it means the + // entity is not translated. This is the correct logic also on monolingual + // sites. + if (!isset($revision)) { + $revision_id = $storage->getLatestRevisionId($entity->id()); + $revision = $this->loadRevision($entity, $revision_id); + } + + return $revision; + } + + /** + * Loads the specified entity revision. + * + * @param \Drupal\Core\Entity\RevisionableInterface $entity + * The default revision of the entity being converted. + * @param string $revision_id + * The identifier of the revision to be loaded. + * + * @return \Drupal\Core\Entity\RevisionableInterface + * An entity revision object. + */ + protected function loadRevision(RevisionableInterface $entity, $revision_id) { + // We explicitly perform a loose equality check, since a revision ID may be + // returned as an integer or a string. + if ($entity->getLoadedRevisionId() != $revision_id) { + /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); + return $storage->loadRevision($revision_id); + } + return $entity; + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityRepositoryInterface.php b/core/lib/Drupal/Core/Entity/EntityRepositoryInterface.php index d1229d4554c30783808d8ce0785a160f0bfe47f2..8ac470dac6f32fe74c2a6ce9f0aa353d7887e12d 100644 --- a/core/lib/Drupal/Core/Entity/EntityRepositoryInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityRepositoryInterface.php @@ -7,6 +7,8 @@ */ interface EntityRepositoryInterface { + const CONTEXT_ID_LEGACY_CONTEXT_OPERATION = '@entity.repository:legacy_context_operation'; + /** * Loads an entity by UUID. * @@ -69,4 +71,100 @@ public function loadEntityByConfigTarget($entity_type_id, $target); */ public function getTranslationFromContext(EntityInterface $entity, $langcode = NULL, $context = []); + /** + * Retrieves the active entity variant matching the specified context. + * + * If an entity type is revisionable and/or translatable, which entity variant + * should be handled depends on the context in which the manipulation happens. + * Based on the specified contextual information, revision and translation + * negotiation needs to be performed to return the active variant, that is the + * most up-to-date entity variant in the context scope. This may or may not be + * an entity variant intended for unprivileged user consumption, in fact it + * might be a work in progress containing yet to be published information. The + * active variant should always be retrieved when editing an entity, both in + * form and in REST workflows, or previewing the related changes. + * + * The negotiation process will not perform any access check, so it is the + * responsibility of the caller to verify that the user manipulating the + * entity variant is actually allowed to do so. + * + * @param string $entity_type_id + * The entity type identifier. + * @param int|string $entity_id + * An entity identifier. + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + * (optional) An associative array of objects representing the contexts the + * entity will be edited in keyed by fully qualified context ID. Defaults to + * the currently available contexts. + * + * @return \Drupal\Core\Entity\EntityInterface|null + * An entity object variant or NULL if the entity does not exist. + */ + public function getActive($entity_type_id, $entity_id, array $contexts = NULL); + + /** + * Retrieves the active entity variants matching the specified context. + * + * @param string $entity_type_id + * The entity type identifier. + * @param int[]|string[] $entity_ids + * An array of entity identifiers. + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + * (optional) An associative array of objects representing the contexts the + * entity will be edited in keyed by fully qualified context ID. Defaults to + * the currently available contexts. + * + * @return \Drupal\Core\Entity\EntityInterface + * An array of entity object variants keyed by entity ID. + * + * @see getActive() + */ + public function getActiveMultiple($entity_type_id, array $entity_ids, array $contexts = NULL); + + /** + * Retrieves the canonical entity variant matching the specified context. + * + * If an entity type is revisionable and/or translatable, which entity variant + * should be handled depends on the context in which the manipulation happens. + * This will return the fittest entity variant intended for unprivileged user + * consumption matching the specified context. This is typically the variant + * that would be displayed on the entity's canonical route. + * + * The negotiation process will not perform any access check, so it is the + * responsibility of the caller to verify that the user manipulating the + * entity variant is actually allowed to do so. + * + * @param string $entity_type_id + * The entity type identifier. + * @param int|string $entity_id + * An entity identifier. + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + * (optional) An associative array of objects representing the contexts the + * entity will be edited in keyed by fully qualified context ID. Defaults to + * the currently available contexts. + * + * @return \Drupal\Core\Entity\EntityInterface|null + * An entity object variant or NULL if the entity does not exist. + */ + public function getCanonical($entity_type_id, $entity_id, array $contexts = NULL); + + /** + * Retrieves the canonical entity variants matching the specified context. + * + * @param string $entity_type_id + * The entity type identifier. + * @param int[]|string[] $entity_ids + * An array of entity identifiers. + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + * (optional) An associative array of objects representing the contexts the + * entity will be edited in keyed by fully qualified context ID. Defaults to + * the currently available contexts. + * + * @return \Drupal\Core\Entity\EntityInterface + * An array of entity object variants keyed by entity ID. + * + * @see getCanonical() + */ + public function getCanonicalMultiple($entity_type_id, array $entity_ids, array $contexts = NULL); + } diff --git a/core/lib/Drupal/Core/ParamConverter/AdminPathConfigEntityConverter.php b/core/lib/Drupal/Core/ParamConverter/AdminPathConfigEntityConverter.php index 89cfa503cb79b77ff69e34209307f24d04dbdb8a..5fbaa3fbf4d01425fde54977bf1119bf987f2477 100644 --- a/core/lib/Drupal/Core/ParamConverter/AdminPathConfigEntityConverter.php +++ b/core/lib/Drupal/Core/ParamConverter/AdminPathConfigEntityConverter.php @@ -2,13 +2,11 @@ namespace Drupal\Core\ParamConverter; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\Entity\ConfigEntityInterface; -use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Routing\AdminContext; use Symfony\Component\Routing\Route; -use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Entity\EntityRepositoryInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; /** * Makes sure the unmodified ConfigEntity is loaded on admin pages. @@ -50,13 +48,11 @@ class AdminPathConfigEntityConverter extends EntityConverter { * The config factory. * @param \Drupal\Core\Routing\AdminContext $admin_context * The route admin context service. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository * The entity repository. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, AdminContext $admin_context, LanguageManagerInterface $language_manager = NULL, EntityRepositoryInterface $entity_repository = NULL) { - parent::__construct($entity_type_manager, $language_manager, $entity_repository); + public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, AdminContext $admin_context, $entity_repository = NULL) { + parent::__construct($entity_type_manager, $entity_repository); $this->configFactory = $config_factory; $this->adminContext = $admin_context; diff --git a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php index 9bf3f61ab5b4736d460df2afdd88c3a56cc018f7..8027a35dbc4da6a3327c97003dd72711f35b2fc6 100644 --- a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php @@ -3,14 +3,13 @@ namespace Drupal\Core\ParamConverter; use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\RevisionableInterface; -use Drupal\Core\Entity\TranslatableRevisionableInterface; use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\TypedData\TranslatableInterface; use Symfony\Component\Routing\Route; @@ -74,7 +73,10 @@ class EntityConverter implements ParamConverterInterface { /** * {@inheritdoc} */ - protected $deprecatedProperties = ['entityManager' => 'entity.manager']; + protected $deprecatedProperties = [ + 'entityManager' => 'entity.manager', + 'languageManager' => 'language_manager', + ]; /** * Entity type manager which performs the upcasting in the end. @@ -90,36 +92,28 @@ class EntityConverter implements ParamConverterInterface { */ protected $entityRepository; - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - /** * Constructs a new EntityConverter. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. - * @param \Drupal\Core\Language\LanguageManagerInterface|null $language_manager - * (optional) The language manager. Defaults to none. * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository * The entity repository. + * + * @see https://www.drupal.org/node/2549139 + * @see https://www.drupal.org/node/2938929 */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager = NULL, EntityRepositoryInterface $entity_repository = NULL) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, $entity_repository = NULL) { if ($entity_type_manager instanceof EntityManagerInterface) { @trigger_error('Passing the entity.manager service to EntityConverter::__construct() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Pass the entity_type.manager service instead. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); } $this->entityTypeManager = $entity_type_manager; - if ($entity_repository) { - $this->entityRepository = $entity_repository; - } - else { - @trigger_error('The entity.repository service must be passed to EntityConverter::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); - $this->entityRepository = \Drupal::service('entity.repository'); + + if (!($entity_repository instanceof EntityRepositoryInterface)) { + @trigger_error('Calling EntityConverter::__construct() with the $entity_repository argument is supported in drupal:8.7.0 and will be required before drupal:9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); + $entity_repository = \Drupal::service('entity.repository'); } - $this->languageManager = $language_manager; + $this->entityRepository = $entity_repository; } /** @@ -127,32 +121,38 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Lan */ public function convert($value, $definition, $name, array $defaults) { $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults); - $storage = $this->entityTypeManager->getStorage($entity_type_id); - $entity_definition = $this->entityTypeManager->getDefinition($entity_type_id); - - $entity = $storage->load($value); // If the entity type is revisionable and the parameter has the - // "load_latest_revision" flag, load the latest revision. - if ($entity instanceof RevisionableInterface && !empty($definition['load_latest_revision']) && $entity_definition->isRevisionable()) { - // Retrieve the latest revision ID taking translations into account. - $langcode = $this->languageManager() - ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT) - ->getId(); - $entity = $this->getLatestTranslationAffectedRevision($entity, $langcode); + // "load_latest_revision" flag, load the active variant. + if (!empty($definition['load_latest_revision'])) { + return $this->entityRepository->getActive($entity_type_id, $value); } - // If the entity type is translatable, ensure we return the proper - // translation object for the current context. - if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) { - $entity = $this->entityRepository->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']); + // Do not inject the context repository as it is not an actual dependency: + // it will be removed once both the TODOs below are fixed. + /** @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface $contexts_repository */ + $contexts_repository = \Drupal::service('context.repository'); + // @todo Consider deprecating the legacy context operation altogether in + // https://www.drupal.org/node/3031124. + $contexts = $contexts_repository->getAvailableContexts(); + $contexts[EntityRepositoryInterface::CONTEXT_ID_LEGACY_CONTEXT_OPERATION] = + new Context(new ContextDefinition('string'), 'entity_upcast'); + // @todo At the moment we do not need the current user context, which is + // triggering some test failures. We can remove these lines once + // https://www.drupal.org/node/2934192 is fixed. + $context_id = '@user.current_user_context:current_user'; + if (isset($contexts[$context_id])) { + $account = $contexts[$context_id]->getContextValue(); + unset($account->_skipProtectedUserFieldConstraint); + unset($contexts[$context_id]); } + $entity = $this->entityRepository->getCanonical($entity_type_id, $value, $contexts); return $entity; } /** - * Returns the ID of the latest revision translation of the specified entity. + * Returns the latest revision translation of the specified entity. * * @param \Drupal\Core\Entity\RevisionableInterface $entity * The default revision of the entity being converted. @@ -163,39 +163,25 @@ public function convert($value, $definition, $name, array $defaults) { * The latest translation-affecting revision for the specified entity, or * just the latest revision, if the specified entity is not translatable or * does not have a matching translation yet. + * + * @deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. + * Use \Drupal\Core\Entity\EntityRepositoryInterface::getActive() instead. */ protected function getLatestTranslationAffectedRevision(RevisionableInterface $entity, $langcode) { - $revision = NULL; - $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); - - if ($entity instanceof TranslatableRevisionableInterface && $entity->isTranslatable()) { - /** @var \Drupal\Core\Entity\TranslatableRevisionableStorageInterface $storage */ - $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode); - - // If the latest translation-affecting revision was a default revision, it - // is fine to load the latest revision instead, because in this case the - // latest revision, regardless of it being default or pending, will always - // contain the most up-to-date values for the specified translation. This - // provides a BC behavior when the route is defined by a module always - // expecting the latest revision to be loaded and to be the default - // revision. In this particular case the latest revision is always going - // to be the default revision, since pending revisions would not be - // supported. - $revision = $revision_id ? $this->loadRevision($entity, $revision_id) : NULL; - if (!$revision || ($revision->wasDefaultRevision() && !$revision->isDefaultRevision())) { - $revision = NULL; - } + @trigger_error('\Drupal\Core\ParamConverter\EntityConverter::getLatestTranslationAffectedRevision() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityRepositoryInterface::getActive() instead.', E_USER_DEPRECATED); + $data_type = 'language'; + $context_id_prefix = '@language.current_language_context:'; + $contexts = [ + $context_id_prefix . LanguageInterface::TYPE_CONTENT => new Context(new ContextDefinition($data_type), $langcode), + $context_id_prefix . LanguageInterface::TYPE_INTERFACE => new Context(new ContextDefinition($data_type), $langcode), + ]; + $revision = $this->entityRepository->getActive($entity->getEntityTypeId(), $entity->id(), $contexts); + // The EntityRepositoryInterface::getActive() method performs entity + // translation negotiation, but this used to return an untranslated entity + // object as translation negotiation happened later in ::convert(). + if ($revision instanceof TranslatableInterface) { + $revision = $revision->getUntranslated(); } - - // Fall back to the latest revisions if no affected revision for the current - // content language could be found. This is acceptable as it means the - // entity is not translated. This is the correct logic also on monolingual - // sites. - if (!isset($revision)) { - $revision_id = $storage->getLatestRevisionId($entity->id()); - $revision = $this->loadRevision($entity, $revision_id); - } - return $revision; } @@ -209,8 +195,11 @@ protected function getLatestTranslationAffectedRevision(RevisionableInterface $e * * @return \Drupal\Core\Entity\RevisionableInterface * An entity revision object. + * + * @deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. */ protected function loadRevision(RevisionableInterface $entity, $revision_id) { + @trigger_error('\Drupal\Core\ParamConverter\EntityConverter::loadRevision() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0.', E_USER_DEPRECATED); // We explicitly perform a loose equality check, since a revision ID may // be returned as an integer or a string. if ($entity->getLoadedRevisionId() != $revision_id) { @@ -247,13 +236,7 @@ public function applies($definition, $name, Route $route) { * @internal */ protected function languageManager() { - if (!isset($this->languageManager)) { - $this->languageManager = \Drupal::languageManager(); - // @todo Turn this into a proper error (E_USER_ERROR) in - // https://www.drupal.org/node/2938929. - @trigger_error('The language manager parameter has been added to EntityConverter since version 8.5.0 and will be made required in version 9.0.0 when requesting the latest translation-affected revision of an entity.', E_USER_DEPRECATED); - } - return $this->languageManager; + return $this->__get('languageManager'); } } diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module index bfd02af4443134bfc62611e4b41342598e50a55c..6eb738f6d4a2f70046a2e5a423035490674119ee 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.module +++ b/core/modules/system/tests/modules/entity_test/entity_test.module @@ -234,7 +234,7 @@ function entity_test_entity_bundle_info_alter(&$bundles) { $state = \Drupal::state(); foreach ($bundles as $entity_type_id => &$all_bundle_info) { if ($entity_info[$entity_type_id]->getProvider() == 'entity_test') { - if ($state->get('entity_test.translation')) { + if ($state->get('entity_test.translation') && $entity_info[$entity_type_id]->isTranslatable()) { foreach ($all_bundle_info as $bundle_name => &$bundle_info) { $bundle_info['translatable'] = TRUE; if ($state->get('entity_test.untranslatable_fields.default_translation_affected')) { diff --git a/core/modules/views_ui/src/ParamConverter/ViewUIConverter.php b/core/modules/views_ui/src/ParamConverter/ViewUIConverter.php index 9c06c2940e4a54816337486eebfcec9809b25734..acb2120112f68c5e081564527f89dd2fe7086457 100644 --- a/core/modules/views_ui/src/ParamConverter/ViewUIConverter.php +++ b/core/modules/views_ui/src/ParamConverter/ViewUIConverter.php @@ -3,15 +3,13 @@ namespace Drupal\views_ui\ParamConverter; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\ParamConverter\AdminPathConfigEntityConverter; -use Drupal\Core\Routing\AdminContext; -use Symfony\Component\Routing\Route; use Drupal\Core\ParamConverter\ParamConverterInterface; +use Drupal\Core\Routing\AdminContext; use Drupal\Core\TempStore\SharedTempStoreFactory; use Drupal\views_ui\ViewUI; +use Symfony\Component\Routing\Route; /** * Provides upcasting for a view entity to be used in the Views UI. @@ -49,12 +47,10 @@ class ViewUIConverter extends AdminPathConfigEntityConverter implements ParamCon * The config factory. * @param \Drupal\Core\Routing\AdminContext $admin_context * The route admin context service. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository * The entity repository. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, SharedTempStoreFactory $temp_store_factory, ConfigFactoryInterface $config_factory = NULL, AdminContext $admin_context = NULL, LanguageManagerInterface $language_manager = NULL, EntityRepositoryInterface $entity_repository = NULL) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, SharedTempStoreFactory $temp_store_factory, ConfigFactoryInterface $config_factory = NULL, AdminContext $admin_context = NULL, $entity_repository = NULL) { // The config factory and admin context are new arguments due to changing // the parent. Avoid an error on updated sites by falling back to getting // them from the container. @@ -65,7 +61,7 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Sha if (!$admin_context) { $admin_context = \Drupal::service('router.admin_context'); } - parent::__construct($entity_type_manager, $config_factory, $admin_context, $language_manager, $entity_repository); + parent::__construct($entity_type_manager, $config_factory, $admin_context, $entity_repository); $this->tempStoreFactory = $temp_store_factory; } diff --git a/core/modules/views_ui/views_ui.services.yml b/core/modules/views_ui/views_ui.services.yml index 33c9e181ed568e5fff3cbbf359bc39f020466c2b..8828b7c513668d05f3799e1fdce371c8002da34f 100644 --- a/core/modules/views_ui/views_ui.services.yml +++ b/core/modules/views_ui/views_ui.services.yml @@ -1,7 +1,7 @@ services: paramconverter.views_ui: class: Drupal\views_ui\ParamConverter\ViewUIConverter - arguments: ['@entity_type.manager', '@tempstore.shared', '@config.factory', '@router.admin_context', '@language_manager', '@entity.repository'] + arguments: ['@entity_type.manager', '@tempstore.shared', '@config.factory', '@router.admin_context', '@entity.repository'] tags: - { name: paramconverter, priority: 10 } lazy: true diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityRepositoryTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityRepositoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a65530365e5d778439dc0d6f689aa26eee0be29a --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityRepositoryTest.php @@ -0,0 +1,325 @@ +<?php + +namespace Drupal\KernelTests\Core\Entity; + +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\entity_test\Entity\EntityTest; +use Drupal\KernelTests\KernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\Tests\user\Traits\UserCreationTrait; + +/** + * Tests the entity repository. + * + * @group Entity + * + * @coversDefaultClass \Drupal\Core\Entity\EntityRepository + */ +class EntityRepositoryTest extends KernelTestBase { + + use UserCreationTrait; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'entity_test', + 'user', + 'language', + 'system', + ]; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The entity repository. + * + * @var \Drupal\Core\Entity\EntityRepositoryInterface + */ + protected $entityRepository; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + $this->entityTypeManager = $this->container->get('entity_type.manager'); + $this->entityRepository = $this->container->get('entity.repository'); + + $this->setUpCurrentUser(); + + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('entity_test_rev'); + $this->installEntitySchema('entity_test_mul'); + $this->installEntitySchema('entity_test_mulrev'); + + $this->installConfig(['system', 'language']); + ConfigurableLanguage::createFromLangcode('it') + ->setWeight(1) + ->save(); + ConfigurableLanguage::createFromLangcode('ro') + ->setWeight(2) + ->save(); + + $this->container->get('state')->set('entity_test.translation', TRUE); + $this->container->get('entity_type.bundle.info')->clearCachedBundles(); + } + + /** + * Tests retrieving active variants. + * + * @covers ::getActive + * @covers ::getActiveMultiple + */ + public function testGetActive() { + $en_contexts = $this->getLanguageContexts('en'); + + // Check that when the entity does not exist NULL is returned. + $entity_type_id = 'entity_test'; + $active = $this->entityRepository->getActive($entity_type_id, -1); + $this->assertNull($active); + + // Check that the correct active variant is returned for a non-translatable, + // non-revisionable entity. + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage($entity_type_id); + $values = ['name' => $this->randomString()]; + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $storage->create($values); + $storage->save($entity); + $entity = $storage->load($entity->id()); + /** @var \Drupal\Core\Entity\ContentEntityInterface $active */ + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts); + $this->assertSame($entity, $active); + + // Check that the correct active variant is returned for a non-translatable + // revisionable entity. + $entity_type_id = 'entity_test_rev'; + $storage = $this->entityTypeManager->getStorage($entity_type_id); + $values = ['name' => $this->randomString()]; + $entity = $storage->create($values); + $storage->save($entity); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ + $revision = $storage->createRevision($entity, FALSE); + $revision->save(); + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts); + $this->assertEntityType($active, $entity_type_id); + $this->assertSame($revision->getLoadedRevisionId(), $active->getLoadedRevisionId()); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $revision2 */ + $revision2 = $storage->createRevision($revision); + $revision2->save(); + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts); + $this->assertSame($revision2->getLoadedRevisionId(), $active->getLoadedRevisionId()); + + // Check that the correct active variant is returned for a translatable + // non-revisionable entity. + $entity_type_id = 'entity_test_mul'; + $storage = $this->entityTypeManager->getStorage($entity_type_id); + $values = ['name' => $this->randomString()]; + $entity = $storage->create($values); + $storage->save($entity); + + $langcode = 'it'; + /** @var \Drupal\Core\Entity\ContentEntityInterface $translation */ + $translation = $entity->addTranslation($langcode, $values); + $storage->save($translation); + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts); + $this->assertEntityType($active, $entity_type_id); + $this->assertSame($entity->language()->getId(), $active->language()->getId()); + + $it_contexts = $this->getLanguageContexts($langcode); + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $it_contexts); + $this->assertSame($translation->language()->getId(), $active->language()->getId()); + + // Check that the correct active variant is returned for a translatable and + // revisionable entity. + $entity_type_id = 'entity_test_mulrev'; + $storage = $this->entityTypeManager->getStorage($entity_type_id); + $values = ['name' => $this->randomString()]; + $entity = $storage->create($values); + $storage->save($entity); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */ + $en_revision = $storage->createRevision($entity, FALSE); + $storage->save($en_revision); + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts); + $this->assertEntityType($active, $entity_type_id); + $this->assertSame($en_revision->getLoadedRevisionId(), $active->getLoadedRevisionId()); + + $revision_translation = $en_revision->addTranslation($langcode, $values); + /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */ + $it_revision = $storage->createRevision($revision_translation, FALSE); + $storage->save($it_revision); + + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts); + $this->assertSame($en_revision->getLoadedRevisionId(), $active->getLoadedRevisionId()); + $this->assertSame($en_revision->language()->getId(), $active->language()->getId()); + + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $it_contexts); + $this->assertSame($it_revision->getLoadedRevisionId(), $active->getLoadedRevisionId()); + $this->assertSame($it_revision->language()->getId(), $active->language()->getId()); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision2 */ + $en_revision2 = $storage->createRevision($en_revision); + $storage->save($en_revision2); + + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts); + $this->assertSame($en_revision2->getLoadedRevisionId(), $active->getLoadedRevisionId()); + $this->assertSame($en_revision2->language()->getId(), $active->language()->getId()); + + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $it_contexts); + $this->assertSame($it_revision->getLoadedRevisionId(), $active->getLoadedRevisionId()); + $this->assertSame($it_revision->language()->getId(), $active->language()->getId()); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision2 */ + $it_revision2 = $storage->createRevision($it_revision); + $storage->save($it_revision2); + + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $en_contexts); + $this->assertSame($it_revision2->getLoadedRevisionId(), $active->getLoadedRevisionId()); + $this->assertSame($it_revision2->getUntranslated()->language()->getId(), $active->language()->getId()); + + $active = $this->entityRepository->getActive($entity_type_id, $entity->id(), $it_contexts); + $this->assertSame($it_revision2->getLoadedRevisionId(), $active->getLoadedRevisionId()); + $this->assertSame($it_revision2->language()->getId(), $active->language()->getId()); + + /** @var \Drupal\entity_test\Entity\EntityTestMulRev $entity2 */ + $entity2 = $storage->create($values); + $storage->save($entity2); + /** @var \Drupal\Core\Entity\ContentEntityInterface[] $active */ + $active = $this->entityRepository->getActiveMultiple($entity_type_id, [$entity->id(), $entity2->id()], $it_contexts); + $this->assertSame($it_revision2->getLoadedRevisionId(), $active[$entity->id()]->getLoadedRevisionId()); + $this->assertSame($it_revision2->language()->getId(), $active[$entity->id()]->language()->getId()); + $this->assertSame($entity2->getLoadedRevisionId(), $active[$entity2->id()]->getLoadedRevisionId()); + $this->assertSame($entity2->language()->getId(), $active[$entity2->id()]->language()->getId()); + + $this->doTestLanguageFallback('getActive'); + } + + /** + * Tests retrieving canonical variants. + * + * @covers ::getCanonical + * @covers ::getCanonicalMultiple + */ + public function testGetCanonical() { + // Check that when the entity does not exist NULL is returned. + $entity_type_id = 'entity_test_mul'; + $canonical = $this->entityRepository->getActive($entity_type_id, -1); + $this->assertNull($canonical); + + // Check that the correct language fallback rules are applied. + $storage = $this->entityTypeManager->getStorage($entity_type_id); + $values = ['name' => $this->randomString()]; + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $storage->create($values); + $storage->save($entity); + + $langcode = 'it'; + $it_contexts = $this->getLanguageContexts($langcode); + $canonical = $this->entityRepository->getCanonical($entity_type_id, $entity->id(), $it_contexts); + $this->assertSame($entity->getUntranslated()->language()->getId(), $canonical->language()->getId()); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $translation */ + $translation = $entity->addTranslation($langcode, $values); + $storage->save($translation); + $canonical = $this->entityRepository->getCanonical($entity_type_id, $entity->id(), $it_contexts); + $this->assertSame($translation->language()->getId(), $canonical->language()->getId()); + + $canonical = $this->entityRepository->getCanonical($entity_type_id, $entity->id()); + $this->assertSame($entity->getUntranslated()->language()->getId(), $canonical->language()->getId()); + + /** @var \Drupal\entity_test\Entity\EntityTestMul $entity2 */ + $entity2 = $storage->create($values); + $storage->save($entity2); + /** @var \Drupal\Core\Entity\ContentEntityInterface[] $canonical */ + $canonical = $this->entityRepository->getCanonicalMultiple($entity_type_id, [$entity->id(), $entity2->id()], $it_contexts); + $this->assertSame($translation->language()->getId(), $canonical[$entity->id()]->language()->getId()); + $this->assertSame($entity2->language()->getId(), $canonical[$entity2->id()]->language()->getId()); + + $this->doTestLanguageFallback('getCanonical'); + } + + /** + * Check that language fallback is applied. + * + * @param string $method_name + * An entity repository method name. + */ + protected function doTestLanguageFallback($method_name) { + $entity_type_id = 'entity_test_mulrev'; + $en_contexts = $this->getLanguageContexts('en'); + $it_contexts = $this->getLanguageContexts('it'); + $ro_contexts = $this->getLanguageContexts('ro'); + + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage($entity_type_id); + $values = ['name' => $this->randomString()]; + + /** @var \Drupal\entity_test\Entity\EntityTestMulRev $entity3 */ + $entity3 = $storage->create(['langcode' => 'it'] + $values); + $entity3->addTranslation('ro', $values); + $storage->save($entity3); + /** @var \Drupal\entity_test\Entity\EntityTestMulRev $active */ + $active = $this->entityRepository->{$method_name}($entity_type_id, $entity3->id(), $en_contexts); + $this->assertSame('it', $active->language()->getId()); + + $active = $this->entityRepository->{$method_name}($entity_type_id, $entity3->id(), $ro_contexts); + $this->assertSame('ro', $active->language()->getId()); + + /** @var \Drupal\entity_test\Entity\EntityTestMulRev $entity4 */ + $entity4 = $storage->create(['langcode' => 'ro'] + $values); + $entity4->addTranslation('en', $values); + $storage->save($entity4); + $active = $this->entityRepository->{$method_name}($entity_type_id, $entity4->id(), $it_contexts); + $this->assertSame('en', $active->language()->getId()); + + /** @var \Drupal\entity_test\Entity\EntityTestMulRev $entity5 */ + $entity5 = $storage->create(['langcode' => 'ro'] + $values); + $storage->save($entity5); + $active = $this->entityRepository->{$method_name}($entity_type_id, $entity5->id(), $it_contexts); + $this->assertSame('ro', $active->language()->getId()); + $active = $this->entityRepository->{$method_name}($entity_type_id, $entity5->id(), $en_contexts); + $this->assertSame('ro', $active->language()->getId()); + } + + /** + * Asserts that the entity has the expected entity type ID + * + * @param object|null $entity + * An entity object or NULL. + * @param string $expected_entity_type_id + * The expected entity type ID. + */ + protected function assertEntityType($entity, $expected_entity_type_id) { + $this->assertTrue($entity instanceof EntityTest && $entity->getEntityTypeId() === $expected_entity_type_id); + } + + /** + * Returns a set of language contexts matching the specified language. + * + * @param string $langcode + * A language code. + * + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + * An array of contexts. + */ + protected function getLanguageContexts($langcode) { + $prefix = '@language.current_language_context:'; + return [ + $prefix . LanguageInterface::TYPE_INTERFACE => new Context(new ContextDefinition('language'), $langcode), + $prefix . LanguageInterface::TYPE_CONTENT => new Context(new ContextDefinition('language'), $langcode), + ]; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php index c3c659ca88d56dcf236e92607632a8e6067e1c8d..81f4c0e8cefb532fc71459c14aaf8a9731380ca1 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php @@ -3,11 +3,11 @@ namespace Drupal\Tests\Core\Entity; use Drupal\Core\DependencyInjection\ContainerBuilder; -use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface; use Drupal\Core\Entity\EntityManager; +use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityType; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -57,7 +57,7 @@ class EntityManagerTest extends UnitTestCase { protected $entityFieldManager; /** - * The entity display repository. + * The entity repository. * * @var \Drupal\Core\Entity\EntityRepositoryInterface|\Prophecy\Prophecy\ProphecyInterface */ @@ -274,4 +274,64 @@ public function testGetInstance() { $this->entityManager->getInstance(['example' => TRUE]); } + /** + * Tests the getActive() method. + * + * @covers ::getActive + * + * @expectedDeprecation EntityManagerInterface::getActive() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityRepositoryInterface::getActive() instead. See https://www.drupal.org/node/2549139. + */ + public function testGetActive() { + $entity_type_id = 'entity_test'; + $entity_id = 0; + $contexts = []; + $this->entityRepository->getActive($entity_type_id, $entity_id, $contexts)->shouldBeCalled($entity_type_id, $entity_id, $contexts); + $this->entityManager->getActive($entity_type_id, $entity_id, $contexts); + } + + /** + * Tests the getActiveMultiple() method. + * + * @covers ::getActiveMultiple + * + * @expectedDeprecation EntityManagerInterface::getActiveMultiple() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityRepositoryInterface::getActiveMultiple() instead. See https://www.drupal.org/node/2549139. + */ + public function testActiveMultiple() { + $entity_type_id = 'entity_test'; + $entity_ids = [0]; + $contexts = []; + $this->entityRepository->getActiveMultiple($entity_type_id, $entity_ids, $contexts)->shouldBeCalled($entity_type_id, $entity_ids, $contexts); + $this->entityManager->getActiveMultiple($entity_type_id, $entity_ids, $contexts); + } + + /** + * Tests the getCanonical() method. + * + * @covers ::getCanonical + * + * @expectedDeprecation EntityManagerInterface::getCanonical() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityRepositoryInterface::getCanonical() instead. See https://www.drupal.org/node/2549139. + */ + public function testGetCanonical() { + $entity_type_id = 'entity_test'; + $entity_id = ''; + $contexts = []; + $this->entityRepository->getCanonical($entity_type_id, $entity_id, $contexts)->shouldBeCalled($entity_type_id, $entity_id, $contexts); + $this->entityManager->getCanonical($entity_type_id, $entity_id, $contexts); + } + + /** + * Tests the getCanonicalMultiple() method. + * + * @covers ::getCanonicalMultiple + * + * @expectedDeprecation EntityManagerInterface::getCanonicalMultiple() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityRepositoryInterface::getCanonicalMultiple() instead. See https://www.drupal.org/node/2549139. + */ + public function testGetCanonicalMultiple() { + $entity_type_id = 'entity_test'; + $entity_ids = [0]; + $contexts = []; + $this->entityRepository->getCanonicalMultiple($entity_type_id, $entity_ids, $contexts)->shouldBeCalled($entity_type_id, $entity_ids, $contexts); + $this->entityManager->getCanonicalMultiple($entity_type_id, $entity_ids, $contexts); + } + } diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityRepositoryTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityRepositoryTest.php index 688dd75b9b5e2217b919d182610c29cd33214457..4092921ecc664d2837851cdf149b7d9d92a302f1 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityRepositoryTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityRepositoryTest.php @@ -8,6 +8,7 @@ use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Plugin\Context\ContextRepositoryInterface; use Drupal\Tests\UnitTestCase; use Prophecy\Argument; @@ -31,6 +32,13 @@ class EntityRepositoryTest extends UnitTestCase { */ protected $languageManager; + /** + * The context repository. + * + * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $contextRepository; + /** * The entity repository under test. * @@ -46,8 +54,9 @@ protected function setUp() { $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); $this->languageManager = $this->prophesize(LanguageManagerInterface::class); + $this->contextRepository = $this->prophesize(ContextRepositoryInterface::class); - $this->entityRepository = new EntityRepository($this->entityTypeManager->reveal(), $this->languageManager->reveal()); + $this->entityRepository = new EntityRepository($this->entityTypeManager->reveal(), $this->languageManager->reveal(), $this->contextRepository->reveal()); } /** diff --git a/core/tests/Drupal/Tests/Core/ParamConverter/EntityConverterTest.php b/core/tests/Drupal/Tests/Core/ParamConverter/EntityConverterTest.php index e96064703e9627c8654cf9a983ecdf825b74d221..8480c969df987af63883d7b79d7bd9034a7fb1b4 100644 --- a/core/tests/Drupal/Tests/Core/ParamConverter/EntityConverterTest.php +++ b/core/tests/Drupal/Tests/Core/ParamConverter/EntityConverterTest.php @@ -2,16 +2,22 @@ namespace Drupal\Tests\Core\ParamConverter; -use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\ContentEntityStorageInterface; use Drupal\Core\Entity\ContentEntityTypeInterface; -use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\ParamConverter\EntityConverter; use Drupal\Core\ParamConverter\ParamNotConvertedException; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\Plugin\Context\ContextRepositoryInterface; +use Drupal\Core\TypedData\DataDefinition; +use Drupal\Core\TypedData\TypedDataInterface; +use Drupal\Core\TypedData\TypedDataManagerInterface; use Drupal\Tests\UnitTestCase; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Routing\Route; @@ -30,13 +36,6 @@ class EntityConverterTest extends UnitTestCase { */ protected $entityTypeManager; - /** - * The mocked language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $languageManager; - /** * The mocked entities repository. * @@ -58,10 +57,199 @@ protected function setUp() { parent::setUp(); $this->entityTypeManager = $this->createMock(EntityTypeManagerInterface::class); - $this->languageManager = $this->createMock(LanguageManagerInterface::class); $this->entityRepository = $this->createMock(EntityRepositoryInterface::class); - $this->entityConverter = new EntityConverter($this->entityTypeManager, $this->languageManager, $this->entityRepository); + $this->entityConverter = new EntityConverter($this->entityTypeManager, $this->entityRepository); + } + + /** + * Sets up mock services and class instances. + * + * @param object[] $service_map + * An associative array of service instances keyed by service name. + */ + protected function setUpMocks($service_map = []) { + $entity = $this->createMock(ContentEntityInterface::class); + $entity->expects($this->any()) + ->method('getEntityTypeId') + ->willReturn('entity_test'); + $entity->expects($this->any()) + ->method('id') + ->willReturn('id'); + $entity->expects($this->any()) + ->method('isTranslatable') + ->willReturn(FALSE); + $entity->expects($this->any()) + ->method('getLoadedRevisionId') + ->willReturn('revision_id'); + + $storage = $this->createMock(ContentEntityStorageInterface::class); + $storage->expects($this->any()) + ->method('load') + ->with('id') + ->willReturn($entity); + $storage->expects($this->any()) + ->method('getLatestRevisionId') + ->with('id') + ->willReturn('revision_id'); + + $this->entityTypeManager->expects($this->any()) + ->method('getStorage') + ->with('entity_test') + ->willReturn($storage); + + $entity_type = $this->createMock(ContentEntityTypeInterface::class); + $entity_type->expects($this->any()) + ->method('isRevisionable') + ->willReturn(TRUE); + + $this->entityTypeManager->expects($this->any()) + ->method('getDefinition') + ->with('entity_test') + ->willReturn($entity_type); + + $context_repository = $this->createMock(ContextRepositoryInterface::class); + $context_repository->expects($this->any()) + ->method('getAvailableContexts') + ->willReturn([]); + + $context_definition = $this->createMock(DataDefinition::class); + foreach (['setLabel', 'setDescription', 'setRequired', 'setConstraints'] as $method) { + $context_definition->expects($this->any()) + ->method($method) + ->willReturn($context_definition); + } + $context_definition->expects($this->any()) + ->method('getConstraints') + ->willReturn([]); + + $typed_data_manager = $this->createMock(TypedDataManagerInterface::class); + $typed_data_manager->expects($this->any()) + ->method('create') + ->willReturn($this->createMock(TypedDataInterface::class)); + $typed_data_manager->expects($this->any()) + ->method('createDataDefinition') + ->willReturn($context_definition); + + $service_map += [ + 'context.repository' => $context_repository, + 'typed_data_manager' => $typed_data_manager, + ]; + + /** @var \Symfony\Component\DependencyInjection\ContainerInterface|\PHPUnit_Framework_MockObject_MockObject $container */ + $container = $this->createMock(ContainerInterface::class); + $return_map = []; + foreach ($service_map as $name => $service) { + $return_map[] = [$name, 1, $service]; + } + $container + ->expects($this->any()) + ->method('get') + ->willReturnMap($return_map); + + \Drupal::setContainer($container); + } + + /** + * Tests that passing the language manager triggers a deprecation error. + * + * @group legacy + * + * @expectedDeprecation Calling EntityConverter::__construct() with the $entity_repository argument is supported in drupal:8.7.0 and will be required before drupal:9.0.0. See https://www.drupal.org/node/2549139. + */ + public function testDeprecatedLanguageManager() { + $container_entity_repository = clone $this->entityRepository; + $this->setUpMocks([ + 'entity.repository' => $container_entity_repository, + ]); + $language_manager = $this->createMock(LanguageManagerInterface::class); + + $this->entityConverter = new EntityConverter($this->entityTypeManager, $language_manager); + } + + /** + * Tests that retrieving the language manager triggers a deprecation error. + * + * @group legacy + * + * @expectedDeprecation The property languageManager (language_manager service) is deprecated in Drupal\Core\ParamConverter\EntityConverter and will be removed before Drupal 9.0.0. + */ + public function testDeprecatedLanguageManagerMethod() { + $this->setUpMocks([ + 'language_manager' => $this->createMock(LanguageManagerInterface::class), + ]); + $this->entityConverter = new EntityConverter($this->entityTypeManager, $this->entityRepository); + $reflector = new \ReflectionMethod(EntityConverter::class, 'languageManager'); + $reflector->setAccessible(TRUE); + $this->assertSame(\Drupal::service('language_manager'), $reflector->invoke($this->entityConverter)); + } + + /** + * Tests that retrieving the language manager triggers a deprecation error. + * + * @group legacy + * + * @expectedDeprecation The property languageManager (language_manager service) is deprecated in Drupal\Core\ParamConverter\EntityConverter and will be removed before Drupal 9.0.0. + */ + public function testDeprecatedLanguageManagerProperty() { + $this->setUpMocks([ + 'language_manager' => $this->createMock(LanguageManagerInterface::class), + ]); + $this->entityConverter = new EntityConverter($this->entityTypeManager, $this->entityRepository); + $this->assertSame(\Drupal::service('language_manager'), $this->entityConverter->__get('languageManager')); + } + + /** + * Tests that ::getLatestTranslationAffectedRevision() is deprecated. + * + * @group legacy + * + * @expectedDeprecation \Drupal\Core\ParamConverter\EntityConverter::getLatestTranslationAffectedRevision() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\EntityRepositoryInterface::getActive() instead. + */ + public function testDeprecatedGetLatestTranslationAffectedRevision() { + $this->setUpMocks(); + + /** @var \Drupal\Core\Entity\ContentEntityInterface|\PHPUnit_Framework_MockObject_MockObject $revision */ + $revision = $this->createMock(ContentEntityInterface::class); + $revision->expects($this->any()) + ->method('getEntityTypeId') + ->willReturn('entity_test'); + $revision->expects($this->any()) + ->method('id') + ->willReturn('1'); + + /** @var static $test */ + $test = $this; + $this->entityRepository->expects($this->any()) + ->method('getActive') + ->willReturnCallback(function ($entity_type_id, $entity_id, $contexts) use ($test) { + $test->assertSame('entity_test', $entity_type_id); + $test->assertSame('1', $entity_id); + $context_id_prefix = '@language.current_language_context:'; + $test->assertTrue(isset($contexts[$context_id_prefix . LanguageInterface::TYPE_CONTENT])); + $test->assertTrue(isset($contexts[$context_id_prefix . LanguageInterface::TYPE_INTERFACE])); + }); + + $this->entityConverter = new EntityConverter($this->entityTypeManager, $this->entityRepository); + $reflector = new \ReflectionMethod(EntityConverter::class, 'getLatestTranslationAffectedRevision'); + $reflector->setAccessible(TRUE); + $reflector->invoke($this->entityConverter, $revision, NULL); + } + + /** + * Tests that ::loadRevision() is deprecated. + * + * @group legacy + * + * @expectedDeprecation \Drupal\Core\ParamConverter\EntityConverter::loadRevision() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. + */ + public function testDeprecatedLoadRevision() { + $this->setUpMocks(); + $this->entityConverter = new EntityConverter($this->entityTypeManager, $this->entityRepository); + $reflector = new \ReflectionMethod(EntityConverter::class, 'loadRevision'); + $reflector->setAccessible(TRUE); + $revision = $this->createMock(ContentEntityInterface::class); + $reflector->invoke($this->entityConverter, $revision, NULL); } /** @@ -103,17 +291,13 @@ public function providerTestApplies() { * @covers ::convert */ public function testConvert($value, array $definition, array $defaults, $expected_result) { - $entity_storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface'); - $this->entityTypeManager->expects($this->once()) - ->method('getStorage') - ->with('entity_test') - ->willReturn($entity_storage); - $entity_storage->expects($this->any()) - ->method('load') - ->willReturnMap([ - ['valid_id', (object) ['id' => 'valid_id']], - ['invalid_id', NULL], - ]); + $this->setUpMocks(); + + $this->entityRepository->expects($this->any()) + ->method('getCanonical') + ->willReturnCallback(function ($entity_type_id, $entity_id) { + return $entity_type_id === 'entity_test' && $entity_id === 'valid_id' ? (object) ['id' => 'valid_id'] : NULL; + }); $this->assertEquals($expected_result, $this->entityConverter->convert($value, $definition, 'foo', $defaults)); } @@ -137,13 +321,21 @@ public function providerTestConvert() { * Tests the convert() method with an invalid entity type. */ public function testConvertWithInvalidEntityType() { - $this->entityTypeManager->expects($this->once()) - ->method('getStorage') - ->with('invalid_id') - ->willThrowException(new InvalidPluginDefinitionException('invalid_id')); + $this->setUpMocks(); + + $contexts = [ + EntityRepositoryInterface::CONTEXT_ID_LEGACY_CONTEXT_OPERATION => new Context(new ContextDefinition('string'), 'entity_upcast'), + ]; + + $plugin_id = 'invalid_id'; + $this->entityRepository->expects($this->once()) + ->method('getCanonical') + ->with($plugin_id, 'id', $contexts) + ->willThrowException(new PluginNotFoundException($plugin_id)); - $this->setExpectedException(InvalidPluginDefinitionException::class); - $this->entityConverter->convert('id', ['type' => 'entity:invalid_id'], 'foo', ['foo' => 'id']); + $this->setExpectedException(PluginNotFoundException::class); + + $this->entityConverter->convert('id', ['type' => 'entity:' . $plugin_id], 'foo', ['foo' => 'id']); } /** @@ -154,76 +346,4 @@ public function testConvertWithInvalidDynamicEntityType() { $this->entityConverter->convert('id', ['type' => 'entity:{invalid_id}'], 'foo', ['foo' => 'id']); } - /** - * Tests that omitting the language manager triggers a deprecation error. - * - * @group legacy - * - * @expectedDeprecation The language manager parameter has been added to EntityConverter since version 8.5.0 and will be made required in version 9.0.0 when requesting the latest translation-affected revision of an entity. - */ - public function testDeprecatedOptionalLanguageManager() { - $entity = $this->createMock(ContentEntityInterface::class); - $entity->expects($this->any()) - ->method('getEntityTypeId') - ->willReturn('entity_test'); - $entity->expects($this->any()) - ->method('id') - ->willReturn('id'); - $entity->expects($this->any()) - ->method('isTranslatable') - ->willReturn(FALSE); - $entity->expects($this->any()) - ->method('getLoadedRevisionId') - ->willReturn('revision_id'); - - $storage = $this->createMock(ContentEntityStorageInterface::class); - $storage->expects($this->any()) - ->method('load') - ->with('id') - ->willReturn($entity); - $storage->expects($this->any()) - ->method('getLatestRevisionId') - ->with('id') - ->willReturn('revision_id'); - - $this->entityTypeManager->expects($this->any()) - ->method('getStorage') - ->with('entity_test') - ->willReturn($storage); - - $entity_type = $this->createMock(ContentEntityTypeInterface::class); - $entity_type->expects($this->any()) - ->method('isRevisionable') - ->willReturn(TRUE); - - $this->entityTypeManager->expects($this->any()) - ->method('getDefinition') - ->with('entity_test') - ->willReturn($entity_type); - - $language = $this->createMock(LanguageInterface::class); - $language->expects($this->any()) - ->method('getId') - ->willReturn('en'); - - $language_manager = $this->createMock(LanguageManagerInterface::class); - $language_manager->expects($this->any()) - ->method('getCurrentLanguage') - ->with(LanguageInterface::TYPE_CONTENT) - ->willReturn($language); - - /** @var \Symfony\Component\DependencyInjection\ContainerInterface|\PHPUnit_Framework_MockObject_MockObject $container */ - $container = $this->createMock(ContainerInterface::class); - $container->expects($this->any()) - ->method('get') - ->with('language_manager') - ->willReturn($language_manager); - - \Drupal::setContainer($container); - $definition = ['type' => 'entity:entity_test', 'load_latest_revision' => TRUE]; - - $this->entityConverter = new EntityConverter($this->entityTypeManager, NULL, $this->entityRepository); - $this->entityConverter->convert('id', $definition, 'foo', []); - } - }