diff --git a/dynamic_entity_reference.install b/dynamic_entity_reference.install index 85c5b38..8117f54 100644 --- a/dynamic_entity_reference.install +++ b/dynamic_entity_reference.install @@ -8,40 +8,49 @@ use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; /** - * Change target_id to string. + * Changes target_id column to string and creates target_id_int. */ function dynamic_entity_reference_update_8001() { /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */ $schema = \Drupal::database()->schema(); $entity_field_manager = \Drupal::service('entity_field.manager'); $entity_type_manager = \Drupal::entityTypeManager(); - $kv = \Drupal::keyValue('entity.storage_schema.sql'); + // The key-value collection for tracking installed storage schema. + $installed_storage_schema = \Drupal::keyValue('entity.storage_schema.sql'); + // DER field storage subscriber service to create integer column and add + // triggers for target_id column. /** @var \Drupal\dynamic_entity_reference\EventSubscriber\FieldStorageSubscriber $service */ $service = \Drupal::service('dynamic_entity_reference.entity_type_subscriber'); + // Only update dynamic_entity_reference fields. foreach ($entity_field_manager->getFieldMapByFieldType('dynamic_entity_reference') as $entity_type_id => $map) { - $storage = $entity_type_manager->getStorage($entity_type_id); - if ($storage instanceof SqlEntityStorageInterface) { - $storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id); - $mapping = $storage->getTableMapping($storage_definitions); - /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition */ - foreach (array_intersect_key($storage_definitions, $map) as $storage_definition) { - $field_name = $storage_definition->getName(); - $table = $mapping->getFieldTableName($field_name); - $column = $mapping->getFieldColumnName($storage_definition, 'target_id'); + $entity_storage = $entity_type_manager->getStorage($entity_type_id); + // Only SQL storage based entities are supported. + if ($entity_storage instanceof SqlEntityStorageInterface) { + $field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id); + $table_mapping = $entity_storage->getTableMapping($field_storage_definitions); + // Only need field storage definitions of dynamic_entity_reference fields. + /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition */ + foreach (array_intersect_key($field_storage_definitions, $map) as $field_storage_definition) { + $field_name = $field_storage_definition->getName(); + $table = $table_mapping->getFieldTableName($field_name); + $column = $table_mapping->getFieldColumnName($field_storage_definition, 'target_id'); $spec = [ 'description' => 'The ID of the target entity.', 'type' => 'varchar', - 'length' => 128, + 'length' => 255, 'not null' => TRUE, ]; + // Name of the column is not changed only specifications are changed. $schema->changeField($table, $column, $column, $spec); + // Update the installed storage schema for this field as well. $key = $entity_type_id . '.field_schema_data.' . $field_name; - if ($field_schema_data = $kv->get($key)) { + if ($field_schema_data = $installed_storage_schema->get($key)) { $field_schema_data[$table]['fields'][$column] = $spec; - $kv->set($key, $field_schema_data); + $installed_storage_schema->set($key, $field_schema_data); } + // Add the integer column after converting the original column. + $service->handleEntityType($entity_type_id, $field_name, $field_storage_definition); } - $service->handleEntityType($entity_type_id); } } } diff --git a/src/EventSubscriber/FieldStorageSubscriber.php b/src/EventSubscriber/FieldStorageSubscriber.php index 703a991..c4ad25b 100644 --- a/src/EventSubscriber/FieldStorageSubscriber.php +++ b/src/EventSubscriber/FieldStorageSubscriber.php @@ -70,7 +70,7 @@ class FieldStorageSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { $events[FieldStorageDefinitionEvents::CREATE][] = ['onFieldStorage', 100]; $events[EntityTypeEvents::CREATE][] = ['onEntityType', 100]; - $events[EntityTypeEvents::CREATE][] = ['onEntityType', 100]; + $events[EntityTypeEvents::UPDATE][] = ['onEntityType', 100]; return $events; } diff --git a/src/Plugin/Field/FieldType/DynamicEntityReferenceItem.php b/src/Plugin/Field/FieldType/DynamicEntityReferenceItem.php index bf44eeb..7fccffb 100644 --- a/src/Plugin/Field/FieldType/DynamicEntityReferenceItem.php +++ b/src/Plugin/Field/FieldType/DynamicEntityReferenceItem.php @@ -100,7 +100,7 @@ class DynamicEntityReferenceItem extends EntityReferenceItem { 'target_id' => array( 'description' => 'The ID of the target entity.', 'type' => 'varchar', - 'length' => 128, + 'length' => 255, ), 'target_type' => array( 'description' => 'The Entity Type ID of the target entity.', diff --git a/src/Storage/IntColumnHandler.php b/src/Storage/IntColumnHandler.php index dadd884..6f3b339 100644 --- a/src/Storage/IntColumnHandler.php +++ b/src/Storage/IntColumnHandler.php @@ -101,12 +101,14 @@ class IntColumnHandler implements IntColumnHandlerInterface { } /** - * @TODO not sure whether we want to bother with deleting. + * Removes the trigger. * * @param string $table * Name of the table. - * @param $column + * @param string $column * Name of the column. + * + * @TODO not sure whether we want to bother with deleting. */ public function delete($table, $column) { diff --git a/src/Storage/IntColumnHandlerInterface.php b/src/Storage/IntColumnHandlerInterface.php index c01422a..a56a73b 100644 --- a/src/Storage/IntColumnHandlerInterface.php +++ b/src/Storage/IntColumnHandlerInterface.php @@ -2,7 +2,9 @@ namespace Drupal\dynamic_entity_reference\Storage; - +/** + * The interface for IntColumnHandler. + */ interface IntColumnHandlerInterface { /** diff --git a/src/Storage/IntColumnHandlerPostgreSQL.php b/src/Storage/IntColumnHandlerPostgreSQL.php index be06a85..c1a1706 100644 --- a/src/Storage/IntColumnHandlerPostgreSQL.php +++ b/src/Storage/IntColumnHandlerPostgreSQL.php @@ -5,6 +5,9 @@ namespace Drupal\dynamic_entity_reference\Storage; use Drupal\Core\Database\Connection; +/** + * PostgreSQL implementation of denormalizing into integer columns. + */ class IntColumnHandlerPostgreSQL implements IntColumnHandlerInterface { /** @@ -52,11 +55,11 @@ class IntColumnHandlerPostgreSQL implements IntColumnHandlerInterface { /** * Creates the actual table function. * - * @param $table + * @param string $table * The name of the table. - * @param $column + * @param string $column * The name of the target_id column. - * @param $column_int + * @param string $column_int * The name of the target_id_int column. */ protected function createTriggerFunction($table, $column, $column_int) { @@ -73,11 +76,11 @@ class IntColumnHandlerPostgreSQL implements IntColumnHandlerInterface { /** * Creates the trigger. * - * @param $table + * @param string $table * The name of the table. - * @param $column + * @param string $column * The name of the target_id column. - * @param $column_int + * @param string $column_int * The name of the target_id_int column. */ protected function createTrigger($table, $column, $column_int) { @@ -98,11 +101,9 @@ class IntColumnHandlerPostgreSQL implements IntColumnHandlerInterface { /** * Returns an appropriate plpgsql function name. * - * @param $table + * @param string $table * The name of the table. - * @param $column - * The name of the target_id column. - * @param $column_int + * @param string $column_int * The name of the target_id_int column. * * @return string @@ -115,7 +116,7 @@ class IntColumnHandlerPostgreSQL implements IntColumnHandlerInterface { /** * Gets the prefxied table name. * - * @param $table + * @param string $table * The name of the table. * * @return string diff --git a/src/Storage/IntColumnHandlerSQLite.php b/src/Storage/IntColumnHandlerSQLite.php index 30cae2a..5d047c8 100644 --- a/src/Storage/IntColumnHandlerSQLite.php +++ b/src/Storage/IntColumnHandlerSQLite.php @@ -2,7 +2,9 @@ namespace Drupal\dynamic_entity_reference\Storage; - +/** + * SQLite implementation of denormalizing into integer columns. + */ class IntColumnHandlerSQLite extends IntColumnHandler { /** @@ -28,7 +30,7 @@ class IntColumnHandlerSQLite extends IntColumnHandler { UPDATE $table_name SET $body WHERE ROWID=NEW.ROWID"; // SQLite requires a ; in the query which requires bypassing Drupal's built // in single statement only protection. Although this method is not - // supposed to be called by user submitted data, + // supposed to be called by user submitted data. if (strpos($query, ';') !== FALSE) { throw new \InvalidArgumentException('; is not supported in SQL strings. Use only one statement at a time.'); } diff --git a/src/Tests/Update/derUpdateTest.php b/src/Tests/Update/DerUpdateTest.php similarity index 27% rename from src/Tests/Update/derUpdateTest.php rename to src/Tests/Update/DerUpdateTest.php index 12d399d..3eae178 100644 --- a/src/Tests/Update/derUpdateTest.php +++ b/src/Tests/Update/DerUpdateTest.php @@ -2,8 +2,8 @@ namespace Drupal\dynamic_entity_reference\Tests\Update; -use Drupal\entity_test\Entity\EntityTestBundle; use Drupal\entity_test\Entity\EntityTestMul; +use Drupal\entity_test\Entity\EntityTestStringId; use Drupal\system\Tests\Update\UpdatePathTestBase; /** @@ -11,33 +11,46 @@ use Drupal\system\Tests\Update\UpdatePathTestBase; * * @group dynamic_entity_reference */ -class derUpdateTest extends UpdatePathTestBase { +class DerUpdateTest extends UpdatePathTestBase { + /** + * {@inheritdoc} + */ protected $installProfile = 'testing'; /** * {@inheritdoc} */ protected function setDatabaseDumpFiles() { + // For more information on this db dump see + // https://www.drupal.org/node/2555027#comment-11307815. $this->databaseDumpFiles = [ __DIR__ . '/der_dump.php.gz', ]; } - public function testUpdate8000() { + /** + * Test that target_id is converted to string and target_id_int is created. + * + * @see dynamic_entity_reference_update_8001() + */ + public function testUpdate8001() { $this->runUpdates(); - $this->assertEqual([1, 1, 1, 1], db_query('SELECT field_test_mul_target_id_int FROM {entity_test_mul__field_test_mul}')->fetchCol()); - $etb = EntityTestBundle::create([ + // The db dump contain two entity_test entities referencing one entity_test + // entity and one entity_test_mul entity. + $this->assertEqual([1, 1, 1, 1], \Drupal::database()->query('SELECT field_test_target_id_int FROM {entity_test__field_test}')->fetchCol()); + // The db dump contain two entity_test_mul entities referencing one + // entity_test entity and a entity_test_mul entity. + $this->assertEqual([1, 1, 1, 1], \Drupal::database()->query('SELECT field_test_mul_target_id_int FROM {entity_test_mul__field_test_mul}')->fetchCol()); + $referenced_entity = EntityTestStringId::create([ 'id' => 'test', - 'label' => 'Test label', - 'description' => 'My test description', ]); - $etb->save(); + $referenced_entity->save(); $entity = EntityTestMul::load(3); - $entity->field_test_mul[] = $etb; + $entity->field_test_mul[] = $referenced_entity; $entity->save(); - $this->assertEqual([1, 1, 1, 1, 0], db_query('SELECT field_test_mul_target_id_int FROM {entity_test_mul__field_test_mul} ORDER BY entity_id, delta')->fetchCol()); - $this->assertEqual([1, 1, 1, 1, 'test'], db_query('SELECT field_test_mul_target_id FROM {entity_test_mul__field_test_mul} ORDER BY entity_id, delta')->fetchCol()); + $this->assertEqual([1, 1, 1, 1, 0], \Drupal::database()->query('SELECT field_test_mul_target_id_int FROM {entity_test_mul__field_test_mul} ORDER BY entity_id, delta')->fetchCol()); + $this->assertEqual([1, 1, 1, 1, 'test'], \Drupal::database()->query('SELECT field_test_mul_target_id FROM {entity_test_mul__field_test_mul} ORDER BY entity_id, delta')->fetchCol()); } } diff --git a/tests/src/Kernel/DynamicEntityReferenceFieldTest.php b/tests/src/Kernel/DynamicEntityReferenceFieldTest.php index 272046c..46123ad 100644 --- a/tests/src/Kernel/DynamicEntityReferenceFieldTest.php +++ b/tests/src/Kernel/DynamicEntityReferenceFieldTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\dynamic_entity_reference\Kernel; use Drupal\config\Tests\SchemaCheckTestTrait; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\entity_test\Entity\EntityTestStringId; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; @@ -226,4 +227,191 @@ class DynamicEntityReferenceFieldTest extends EntityKernelTestBase { } } + /** + * Tests referencing entities with string IDs. + */ + public function testReferencedEntitiesStringId() { + $field_name = 'entity_reference_string_id'; + $this->installEntitySchema('entity_test_string_id'); + + // Create a field. + FieldStorageConfig::create(array( + 'field_name' => $field_name, + 'type' => 'dynamic_entity_reference', + 'entity_type' => $this->entityType, + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'settings' => array( + 'exclude_entity_types' => FALSE, + 'entity_type_ids' => [ + 'entity_test_string_id', + ], + ), + ))->save(); + + FieldConfig::create(array( + 'field_name' => $field_name, + 'entity_type' => $this->entityType, + 'bundle' => $this->bundle, + 'label' => 'Field test', + 'settings' => [ + 'entity_test_string_id' => [ + 'handler' => "default:entity_test_string_id", + 'handler_settings' => [ + 'target_bundles' => [ + 'entity_test_string_id' => 'entity_test_string_id', + ], + ], + ], + ], + ))->save(); + // Create the parent entity. + $entity = $this->container->get('entity_type.manager') + ->getStorage($this->entityType) + ->create(array('type' => $this->bundle)); + + // Create the default target entity. + $target_entity = EntityTestStringId::create([ + 'id' => $this->randomString(), + 'type' => 'entity_test_string_id', + ]); + $target_entity->save(); + + // Set the field value. + $entity->{$field_name}->setValue([ + [ + 'target_id' => $target_entity->id(), + 'target_type' => $target_entity->getEntityTypeId(), + ], + ]); + + // Load the target entities using + // DynamicEntityReferenceFieldItemList::referencedEntities(). + $entities = $entity->{$field_name}->referencedEntities(); + $this->assertEquals($entities[0]->id(), $target_entity->id()); + + // Test that a string ID works as a default value and the field's config + // schema is correct. + $field = FieldConfig::loadByName($this->entityType, $this->bundle, $field_name); + $field->setDefaultValue([ + [ + 'target_id' => $target_entity->id(), + 'target_type' => $target_entity->getEntityTypeId(), + ], + ]); + $field->save(); + $this->assertConfigSchema(\Drupal::service('config.typed'), 'field.field.' . $field->id(), $field->toArray()); + + // Test that the default value works. + $entity = $this->container->get('entity_type.manager') + ->getStorage($this->entityType) + ->create(array('type' => $this->bundle)); + $entities = $entity->{$field_name}->referencedEntities(); + $this->assertEquals($entities[0]->id(), $target_entity->id()); + } + + /** + * Tests referencing entities with string and int IDs. + */ + public function testReferencedEntitiesMixId() { + $field_name = 'entity_reference_mix_id'; + $this->installEntitySchema('entity_test_string_id'); + + // Create a field. + FieldStorageConfig::create(array( + 'field_name' => $field_name, + 'type' => 'dynamic_entity_reference', + 'entity_type' => $this->entityType, + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'settings' => array( + 'exclude_entity_types' => FALSE, + 'entity_type_ids' => [ + $this->referencedEntityType, + 'entity_test_string_id', + ], + ), + ))->save(); + + FieldConfig::create(array( + 'field_name' => $field_name, + 'entity_type' => $this->entityType, + 'bundle' => $this->bundle, + 'label' => 'Field test', + 'settings' => [ + 'entity_test_string_id' => [ + 'handler' => "default:entity_test_string_id", + 'handler_settings' => [ + 'target_bundles' => [ + 'entity_test_string_id' => 'entity_test_string_id', + ], + ], + ], + $this->referencedEntityType => [ + 'handler' => 'default:' . $this->referencedEntityType, + 'handler_settings' => [ + 'target_bundles' => [ + $this->bundle => $this->bundle, + ], + ], + ], + ], + ))->save(); + // Create the parent entity. + $entity = $this->container->get('entity_type.manager') + ->getStorage($this->entityType) + ->create(array('type' => $this->bundle)); + + // Create the default target entity. + $target_entity = EntityTestStringId::create([ + 'id' => $this->randomString(), + 'type' => 'entity_test_string_id', + ]); + $target_entity->save(); + $referenced_entity = $this->container->get('entity_type.manager') + ->getStorage($this->referencedEntityType) + ->create(['type' => $this->bundle]); + $referenced_entity->save(); + + // Set the field value. + $entity->{$field_name}->setValue([ + [ + 'target_id' => $target_entity->id(), + 'target_type' => $target_entity->getEntityTypeId(), + ], + [ + 'target_id' => $referenced_entity->id(), + 'target_type' => $referenced_entity->getEntityTypeId(), + ], + ]); + + // Load the target entities using + // DynamicEntityReferenceFieldItemList::referencedEntities(). + $entities = $entity->{$field_name}->referencedEntities(); + $this->assertEquals($entities[0]->id(), $target_entity->id()); + $this->assertEquals($entities[1]->id(), $referenced_entity->id()); + + // Test that a string ID works as a default value and the field's config + // schema is correct. + $field = FieldConfig::loadByName($this->entityType, $this->bundle, $field_name); + $field->setDefaultValue([ + [ + 'target_id' => $target_entity->id(), + 'target_type' => $target_entity->getEntityTypeId(), + ], + [ + 'target_id' => $referenced_entity->id(), + 'target_type' => $referenced_entity->getEntityTypeId(), + ], + ]); + $field->save(); + $this->assertConfigSchema(\Drupal::service('config.typed'), 'field.field.' . $field->id(), $field->toArray()); + + // Test that the default value works. + $entity = $this->container->get('entity_type.manager') + ->getStorage($this->entityType) + ->create(array('type' => $this->bundle)); + $entities = $entity->{$field_name}->referencedEntities(); + $this->assertEquals($entities[0]->id(), $target_entity->id()); + $this->assertEquals($entities[1]->id(), $referenced_entity->id()); + } + }