diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php index 53be393a3ff4e67a7403de61c4886fb518b6940b..a031ac8229d420d9bd8a35b5db41d79f47ea4de2 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php @@ -334,9 +334,9 @@ protected function invokeHook($hook, EntityInterface $entity) { } /** - * Implements Drupal\Core\Entity\EntityStorageInterface::getQueryServiceName(). + * {@inheritdoc} */ - public function getQueryServiceName() { + protected function getQueryServiceName() { return 'entity.query.config'; } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php index 12aeca3e6800baf7d66ccae0248aca399298a432..b9843c8d8af394b58c3556e649fe9d773511c39b 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php @@ -78,7 +78,7 @@ public function save(EntityInterface $entity) { /** * {@inheritdoc} */ - public function getQueryServiceName() { + protected function getQueryServiceName() { throw new QueryException('Null implementation can not be queried.'); } @@ -138,4 +138,11 @@ public function countFieldData($storage_definition, $as_bool = FALSE) { return $as_bool ? FALSE : 0; } + /** + * {@inheritdoc} + */ + public function hasData() { + return FALSE; + } + } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index 64152853d8f4ed880b345ec9fada4e7b74b5c400..3a6517652bac945ef5f447db2c5f1c315613a6ad 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -42,6 +42,16 @@ public static function createInstance(ContainerInterface $container, EntityTypeI ); } + /** + * {@inheritdoc} + */ + public function hasData() { + return (bool) $this->getQuery() + ->accessCheck(FALSE) + ->range(0, 1) + ->execute(); + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php index 6d12c98f8d30a67eae4bab3c78fbbb17554c8180..a5a9a94a7a327aa23a4787654eee94e49d4b253b 100644 --- a/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php @@ -84,6 +84,14 @@ public function purgeFieldData(FieldDefinitionInterface $field_definition, $batc */ public function countFieldData($storage_definition, $as_bool = FALSE); + /** + * Determines if the storage contains any data. + * + * @return bool + * TRUE if the storage contains data, FALSE if not. + */ + public function hasData(); + /** * Performs final cleanup after all data of a field has been purged. * diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index 128d21d0d623244f825ce2656ab2ad769195f2ab..7d7eded17486f24ec4fc7abd43ffce1f7e28e2bf 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -324,23 +324,30 @@ public function getHandler($entity_type, $handler_type) { if (!$class) { throw new InvalidPluginDefinitionException($entity_type, sprintf('The "%s" entity type did not specify a %s handler.', $entity_type, $handler_type)); } - 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); - } - $this->handlers[$handler_type][$entity_type] = $handler; + $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->translationManager); + } + return $handler; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php index 8cf95eee1cbb077f7c6000899c7d8b29f6db25d5..13f9862d5099d2a77ee73c6c5cb6fd345a41b112 100644 --- a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php @@ -237,20 +237,37 @@ public function getFormObject($entity_type, $operation); public function hasHandler($entity_type, $handler_type); /** - * Creates a new handler instance. + * Creates a new handler instance for a entity type and handler type. * * @param string $entity_type * The entity type for this controller. * @param string $handler_type * The controller type to create an instance for. * - * @return mixed + * @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); + /** * Get the bundle info of an entity type. * diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php index 4a40a9e6df307407ae57388f01c6a7f412876fe0..4333c472938749bd06111984f4c0cf2acac9c705 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php @@ -460,7 +460,26 @@ public function loadByProperties(array $values = array()) { * {@inheritdoc} */ public function getQuery($conjunction = 'AND') { - return \Drupal::entityQuery($this->getEntityTypeId(), $conjunction); + // Access the service directly rather than entity.query factory so the + // storage's current entity type is used. + return \Drupal::service($this->getQueryServiceName())->get($this->entityType, $conjunction); } + /** + * {@inheritdoc} + */ + public function getAggregateQuery($conjunction = 'AND') { + // Access the service directly rather than entity.query factory so the + // storage's current entity type is used. + return \Drupal::service($this->getQueryServiceName())->getAggregate($this->entityType, $conjunction); + } + + /** + * Gets the name of the service for the query for this entity storage. + * + * @return string + * The name of the service for the query for this entity storage. + */ + abstract protected function getQueryServiceName(); + } diff --git a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php index 6da72fe0d4d6d4fc34934d9bf3352fa174599ffe..b4c897ea59f296aa2dc3d0051ae2bb7a003bb23a 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php @@ -147,14 +147,6 @@ public function delete(array $entities); */ public function save(EntityInterface $entity); - /** - * Gets the name of the service for the query for this entity storage. - * - * @return string - * The name of the service for the query for this entity storage. - */ - public function getQueryServiceName(); - /** * Returns an entity query instance. * @@ -166,10 +158,25 @@ public function getQueryServiceName(); * @return \Drupal\Core\Entity\Query\QueryInterface * The query instance. * - * @see \Drupal\Core\Entity\EntityStorageInterface::getQueryServiceName() + * @see \Drupal\Core\Entity\EntityStorageBase::getQueryServiceName() */ public function getQuery($conjunction = 'AND'); + /** + * Returns an aggregated query instance. + * + * @param string $conjunction + * (optional) The logical operator for the query, either: + * - AND: all of the conditions on the query need to match. + * - OR: at least one of the conditions on the query need to match. + * + * @return \Drupal\Core\Entity\Query\QueryAggregateInterface + * The aggregated query object that can query the given entity type. + * + * @see \Drupal\Core\Entity\EntityStorageBase::getQueryServiceName() + */ + public function getAggregateQuery($conjunction = 'AND'); + /** * Returns the entity type ID. * diff --git a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php index 59ca4d89c0af350ae4e621a5368205631c76761f..35075027287a7376326a450715d26ddef378396c 100644 --- a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php @@ -202,7 +202,7 @@ protected function has($id, EntityInterface $entity) { /** * {@inheritdoc} */ - public function getQueryServiceName() { + protected function getQueryServiceName() { return 'entity.query.keyvalue'; } diff --git a/core/lib/Drupal/Core/Entity/Query/QueryFactory.php b/core/lib/Drupal/Core/Entity/Query/QueryFactory.php index 5f1ab13f8f45af3a0e7132e95c0d01ec77c6e2bb..d7777c98f62926abad68f5df17ac00a7193c27f6 100644 --- a/core/lib/Drupal/Core/Entity/Query/QueryFactory.php +++ b/core/lib/Drupal/Core/Entity/Query/QueryFactory.php @@ -13,6 +13,14 @@ /** * Factory class Creating entity query objects. + * + * Any implementation of this service must call getQuery()/getAggregateQuery() + * of the corresponding entity storage. + * + * @see \Drupal\Core\Entity\EntityStorageBase::getQuery() + * + * @todo https://www.drupal.org/node/2389335 remove entity.query service and + * replace with using the entity storage's getQuery() method. */ class QueryFactory implements ContainerAwareInterface { @@ -48,8 +56,7 @@ public function __construct(EntityManagerInterface $entity_manager) { * The query object that can query the given entity type. */ public function get($entity_type_id, $conjunction = 'AND') { - $service_name = $this->entityManager->getStorage($entity_type_id)->getQueryServiceName(); - return $this->container->get($service_name)->get($this->entityManager->getDefinition($entity_type_id), $conjunction); + return $this->entityManager->getStorage($entity_type_id)->getQuery($conjunction); } /** @@ -65,8 +72,7 @@ public function get($entity_type_id, $conjunction = 'AND') { * The aggregated query object that can query the given entity type. */ public function getAggregate($entity_type_id, $conjunction = 'AND') { - $service_name = $this->entityManager->getStorage($entity_type_id)->getQueryServiceName(); - return $this->container->get($service_name)->getAggregate($this->entityManager->getDefinition($entity_type_id), $conjunction); + return $this->entityManager->getStorage($entity_type_id)->getAggregateQuery($conjunction); } } diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index b8db248a729623c6694a3f46a5f6a380908b8215..d46c37e4b2f7550fdf18266ef5fefc8630a33e51 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -1198,7 +1198,7 @@ protected function saveRevision(EntityInterface $entity) { /** * {@inheritdoc} */ - public function getQueryServiceName() { + protected function getQueryServiceName() { return 'entity.query.sql'; } diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index dac658484c8240681263fa732fb00e3069f22b3a..d3087b7e60e348678f64f6aedf0eeb023c3220ae 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -178,23 +178,15 @@ public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterfac * {@inheritdoc} */ public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) { - // If we're updating from NULL storage, then there's no stored data that - // requires migration. - // @todo Remove in https://www.drupal.org/node/2335879. + // If the original storage has existing entities, or it is impossible to + // determine if that is the case, require entity data to be migrated. $original_storage_class = $original->getStorageClass(); - $null_storage_class = 'Drupal\Core\Entity\ContentEntityNullStorage'; - if ($original_storage_class == $null_storage_class || is_subclass_of($original_storage_class, $null_storage_class)) { - return FALSE; + if (!class_exists($original_storage_class)) { + return TRUE; } - - return - // If the original storage class is different, then there might be - // existing entities in that storage even if the new storage's base - // table is empty. - // @todo Ask the old storage handler rather than assuming: - // https://www.drupal.org/node/2335879. - $entity_type->getStorageClass() != $original_storage_class || - !$this->isTableEmpty($this->storage->getBaseTable()); + // Use the original entity type since the storage has not been updated. + $original_storage = $this->entityManager->createHandlerInstance($original_storage_class, $original); + return $original_storage->hasData(); } /** diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php index 3a28de00317218bf8dad65edef3db5bb1671ca71..a3a701349cadc742c4d19440e7e90347c70ee19c 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php @@ -1056,6 +1056,88 @@ public function testDedicatedTableSchemaForEntityWithStringIdentifier() { ); } + public function providerTestRequiresEntityDataMigration() { + $updated_entity_type_definition = $this->getMockBuilder('\Drupal\Core\Entity\EntityTypeInterface') + ->disableOriginalConstructor() + ->getMock(); + $updated_entity_type_definition->expects($this->any()) + ->method('getStorageClass') + // A class that exists, *any* class. + ->willReturn('\Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema'); + $original_entity_type_definition = $this->getMockBuilder('\Drupal\Core\Entity\EntityTypeInterface') + ->disableOriginalConstructor() + ->getMock(); + $original_entity_type_definition->expects($this->any()) + ->method('getStorageClass') + // A class that exists, *any* class. + ->willReturn('\Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema'); + $original_entity_type_definition_other_nonexisting = $this->getMockBuilder('\Drupal\Core\Entity\EntityTypeInterface') + ->disableOriginalConstructor() + ->getMock(); + $original_entity_type_definition_other_nonexisting->expects($this->any()) + ->method('getStorageClass') + ->willReturn('bar'); + $original_entity_type_definition_other_existing = $this->getMockBuilder('\Drupal\Core\Entity\EntityTypeInterface') + ->disableOriginalConstructor() + ->getMock(); + $original_entity_type_definition_other_existing->expects($this->any()) + ->method('getStorageClass') + // A class that exists, *any* class. + ->willReturn('\Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema'); + + return [ + // Case 1: same storage class, ::hasData() === TRUE. + [$updated_entity_type_definition, $original_entity_type_definition, TRUE, TRUE], + // Case 2: same storage class, ::hasData() === FALSE. + [$updated_entity_type_definition, $original_entity_type_definition, FALSE, FALSE], + // Case 3: different storage class, original storage class does not exist. + [$updated_entity_type_definition, $original_entity_type_definition_other_nonexisting, NULL, TRUE], + // Case 4: different storage class, original storage class exists, ::hasData() === TRUE. + [$updated_entity_type_definition, $original_entity_type_definition_other_existing, TRUE, TRUE], + // Case 5: different storage class, original storage class exists, ::hasData() === FALSE. + [$updated_entity_type_definition, $original_entity_type_definition_other_existing, FALSE, FALSE], + ]; + } + + /** + * @covers ::requiresEntityDataMigration + * + * @dataProvider providerTestRequiresEntityDataMigration + */ + public function testRequiresEntityDataMigration($updated_entity_type_definition, $original_entity_type_definition, $original_storage_has_data, $migration_required) { + $this->entityType = new ContentEntityType(array( + 'id' => 'entity_test', + 'entity_keys' => array('id' => 'id'), + )); + + $original_storage = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorage') + ->disableOriginalConstructor() + ->getMock(); + + $original_storage->expects($this->exactly(is_null($original_storage_has_data) ? 0 : 1)) + ->method('hasData') + ->willReturn($original_storage_has_data); + + // Assert hasData() is never called on the new storage definition. + $this->storage->expects($this->never()) + ->method('hasData'); + + $connection = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + + $this->entityManager->expects($this->any()) + ->method('createHandlerInstance') + ->willReturn($original_storage); + + $this->storageSchema = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema') + ->setConstructorArgs(array($this->entityManager, $this->entityType, $this->storage, $connection)) + ->setMethods(array('installedStorageSchema')) + ->getMock(); + + $this->assertEquals($migration_required, $this->storageSchema->requiresEntityDataMigration($updated_entity_type_definition, $original_entity_type_definition)); + } + /** * Sets up the storage schema object to test. * diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php index 3589a636a2e1de56f5c1d9b8ea3fb52a3c0effe8..3a37f909b4a19387406575656b2def02ad6d1002 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php @@ -1180,6 +1180,56 @@ public function testLoadMultiplePersistentCacheMiss() { $this->assertEquals($entity, $entities[$id]); } + /** + * @covers ::hasData + */ + public function testHasData() { + $query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface'); + $query->expects(($this->once())) + ->method('accessCheck') + ->with(FALSE) + ->willReturn($query); + $query->expects(($this->once())) + ->method('range') + ->with(0, 1) + ->willReturn($query); + $query->expects(($this->once())) + ->method('execute') + ->willReturn(array(5)); + + $factory = $this->getMockBuilder('Drupal\Core\Entity\Query\QueryFactory') + ->disableOriginalConstructor() + ->getMock(); + $factory->expects($this->once()) + ->method('get') + ->with($this->entityType, 'AND') + ->willReturn($query); + + $this->container->set('entity.query.sql', $factory); + + $database = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + + $this->entityManager->expects($this->any()) + ->method('getDefinition') + ->will($this->returnValue($this->entityType)); + + $this->entityManager->expects($this->any()) + ->method('getFieldStorageDefinitions') + ->will($this->returnValue($this->fieldDefinitions)); + + $this->entityManager->expects($this->any()) + ->method('getBaseFieldDefinitions') + ->will($this->returnValue($this->fieldDefinitions)); + + $this->entityStorage = new SqlContentEntityStorage($this->entityType, $database, $this->entityManager, $this->cache); + + $result = $this->entityStorage->hasData(); + + $this->assertTrue($result, 'hasData returned TRUE'); + } + /** * Tests entity ID sanitization. */