diff --git a/docroot/modules/custom/va_gov_live_field_migration/drush.services.yml b/docroot/modules/custom/va_gov_live_field_migration/drush.services.yml index e3f89e571a..e3d75523e6 100644 --- a/docroot/modules/custom/va_gov_live_field_migration/drush.services.yml +++ b/docroot/modules/custom/va_gov_live_field_migration/drush.services.yml @@ -1,6 +1,8 @@ services: va_gov_live_field_migration.commands: class: \Drupal\va_gov_live_field_migration\Commands\Commands - arguments: [] + arguments: + - '@va_gov_live_field_migration.field_provider_resolver' + - '@va_gov_live_field_migration.migration_runner' tags: - { name: drush.command } diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Annotation/FieldProvider.php b/docroot/modules/custom/va_gov_live_field_migration/src/Annotation/FieldProvider.php new file mode 100644 index 0000000000..a2ab460fd4 --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Annotation/FieldProvider.php @@ -0,0 +1,32 @@ +output()->writeln('Error: ' . $exception->getMessage()); - } - finally { - $elapsedTime = microtime(TRUE) - $startTime; - $peakMemoryUsage = memory_get_peak_usage(); - $this->output()->writeln('Elapsed time: ' . number_format($elapsedTime, 2) . ' seconds'); - $this->output()->writeln('Peak memory usage: ' . number_format($peakMemoryUsage / 1024 / 1024, 2) . ' MB'); - } + protected $fieldProviderResolver; + + /** + * The migration runner service. + * + * @var \Drupal\va_gov_live_field_migration\Migration\Runner\RunnerInterface + */ + protected $migrationRunner; + + /** + * Commands constructor. + * + * @param \Drupal\va_gov_live_field_migration\FieldProvider\Resolver\ResolverInterface $fieldProviderResolver + * The field provider resolver service. + * @param \Drupal\va_gov_live_field_migration\Migration\Runner\RunnerInterface $migrationRunner + * The migration runner service. + */ + public function __construct( + FieldProviderResolverInterface $fieldProviderResolver, + RunnerInterface $migrationRunner + ) { + $this->fieldProviderResolver = $fieldProviderResolver; + $this->migrationRunner = $migrationRunner; } /** - * Migrate a specific field on a specific content type. + * Migrate a specific field on a specific entity type. * * @param string $entityType * The entity type. - * @param string $bundle - * The entity bundle or content type. * @param string $fieldName * The field name. - * - * @command va-gov-live-field-migration:migrate-field - * @aliases va-gov-live-field-migration-migrate-field */ - public function migrateField( + #[CLI\Command(name: 'va-gov-live-field-migration:migrate', aliases: ['va-gov-live-field-migration-migrate'])] + #[CLI\Argument(name: 'entityType', description: 'The entity type')] + #[CLI\Argument(name: 'fieldName', description: 'The field name')] + public function migrate( string $entityType, - string $bundle, string $fieldName ) { - $this->performOperation(function () use ($entityType, $bundle, $fieldName) { - $this->output()->writeln('Migrating field ' . $fieldName . ' on ' . $entityType . ' ' . $bundle); - // Logic for the migration. - $this->output()->writeln('Migration successful.'); - }); + $migration = $this->migrationRunner->getMigration($entityType, $fieldName); + $this->migrationRunner->runMigration($migration, $entityType, $fieldName); } /** - * Rollback a specific field on a specific content type. + * Rollback a specific field on a specific entity type. * * @param string $entityType * The entity type. - * @param string $bundle - * The entity bundle or content type. * @param string $fieldName * The field name. - * - * @command va-gov-live-field-migration:rollback-field - * @aliases va-gov-live-field-migration-rollback-field */ - public function rollbackField( + #[CLI\Command(name: 'va-gov-live-field-migration:rollback', aliases: ['va-gov-live-field-migration-rollback'])] + #[CLI\Argument(name: 'entityType', description: 'The entity type')] + #[CLI\Argument(name: 'fieldName', description: 'The field name')] + public function rollback( string $entityType, - string $bundle, string $fieldName ) { - $this->performOperation(function () use ($entityType, $bundle, $fieldName) { - $this->output()->writeln('Rolling back field ' . $fieldName . ' on ' . $entityType . ' ' . $bundle); - // Logic for the rollback. - $this->output()->writeln('Rollback successful.'); - }); + $migration = $this->migrationRunner->getMigration($entityType, $fieldName); + $this->migrationRunner->rollbackMigration($migration, $entityType, $fieldName); } /** @@ -86,45 +85,45 @@ public function rollbackField( * * @param string $entityType * The entity type. - * @param string $bundle - * The entity bundle or content type. * @param string $fieldName * The field name. - * - * @command va-gov-live-field-migration:verify - * @aliases va-gov-live-field-migration-verify */ + #[CLI\Command(name: 'va-gov-live-field-migration:verify', aliases: ['va-gov-live-field-migration-verify'])] + #[CLI\Argument(name: 'entityType', description: 'The entity type')] + #[CLI\Argument(name: 'fieldName', description: 'The field name')] public function verify( string $entityType, - string $bundle, string $fieldName ) { - $this->performOperation(function () use ($entityType, $bundle, $fieldName) { - $this->output()->writeln('Verifying field ' . $fieldName . ' on ' . $entityType . ' ' . $bundle); - // Logic for the verification. - $this->output()->writeln('Verification successful.'); - }); + $migration = $this->migrationRunner->getMigration($entityType, $fieldName); + $this->migrationRunner->verifyMigration($migration, $entityType, $fieldName); } /** * Find fields that haven't been migrated yet. - * - * @param string $entityType - * The entity type. - * @param string $bundle - * The entity bundle or content type. - * - * @command va-gov-live-field-migration:find - * @aliases va-gov-live-field-migration-find */ - public function find( - string $entityType, - string $bundle - ) { - $this->performOperation(function () use ($entityType, $bundle) { - $this->output()->writeln('Finding fields on ' . $entityType . ' ' . $bundle); - // Logic for finding fields. - }); + #[CLI\Command(name: 'va-gov-live-field-migration:find', aliases: ['va-gov-live-field-migration-find'])] + #[CLI\Option(name: 'field-provider', description: 'The field provider to use')] + #[CLI\Option(name: 'entity-type', description: 'The entity type to use')] + #[CLI\Option(name: 'bundle', description: 'The bundle to use')] + public function find($options = [ + // Default to the issue 14995 field provider. + // @see https://github.com/department-of-veterans-affairs/va-gov-cms/issues/14995 + 'field-provider' => 'issue_14995', + 'entity-type' => 'node', + 'bundle' => NULL, + ]) { + $fieldProvider = $options['field-provider']; + $entityType = $options['entity-type']; + $bundle = $options['bundle']; + $this->output()->writeln('Finding fields with field provider "' . $fieldProvider . '" on entity type "' . $entityType . '", bundle "' . ($bundle ?: 'NULL') . '"...'); + $fields = $this->fieldProviderResolver + ->getFieldProvider($fieldProvider) + ->getFields($entityType, $bundle); + $this->output()->writeln('Found ' . count($fields) . ' fields.'); + foreach ($fields as $field) { + $this->output()->writeln($field); + } } } diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Database/Database.php b/docroot/modules/custom/va_gov_live_field_migration/src/Database/Database.php new file mode 100644 index 0000000000..2830386ecf --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Database/Database.php @@ -0,0 +1,116 @@ +connection = $connection; + } + + /** + * {@inheritDoc} + */ + public function dropTable(string $table): void { + $this->connection->schema()->dropTable($table); + } + + /** + * {@inheritDoc} + */ + public function getPrimaryFieldTableName(string $entityType, string $fieldName): string { + return "{$entityType}__{$fieldName}"; + } + + /** + * {@inheritDoc} + */ + public function getFieldRevisionTableName(string $entityType, string $fieldName): string { + return "{$entityType}__{$fieldName}"; + } + + /** + * {@inheritDoc} + */ + public function getBackupTableName(string $tableName): string { + // Table names are limited to 64 characters, but Drupal field tables can + // be longer than that. So we need to truncate the table name. + $tableName = substr($tableName, 0, 64 - strlen(self::BACKUP_TABLE_SUFFIX)); + return "{$tableName}__backup"; + } + + /** + * {@inheritDoc} + */ + public function createTable(string $newTable, string $existingTable): void { + $this->connection->query("CREATE TABLE {$newTable} LIKE {$existingTable};"); + } + + /** + * {@inheritDoc} + */ + public function copyTable(string $sourceTable, string $destinationTable, bool $preserve = FALSE): void { + if (!$preserve) { + $this->dropTable($destinationTable); + $this->createTable($destinationTable, $sourceTable); + } + $this->connection->query("INSERT {$destinationTable} SELECT * FROM {$sourceTable};"); + } + + /** + * {@inheritDoc} + */ + public function backupPrimaryFieldTable(string $entityType, string $fieldName, bool $preserve = FALSE): void { + $primaryTable = $this->getPrimaryFieldTableName($entityType, $fieldName); + $backupTable = $this->getBackupTableName($primaryTable); + $this->copyTable($primaryTable, $backupTable, $preserve); + } + + /** + * {@inheritDoc} + */ + public function backupFieldRevisionTable(string $entityType, string $fieldName, bool $preserve = FALSE): void { + $revisionTable = $this->getFieldRevisionTableName($entityType, $fieldName); + $backupTable = $this->getBackupTableName($revisionTable); + $this->copyTable($revisionTable, $backupTable, $preserve); + } + + /** + * {@inheritDoc} + */ + public function restorePrimaryFieldTable(string $entityType, string $fieldName, bool $preserve = FALSE): void { + $primaryTable = $this->getPrimaryFieldTableName($entityType, $fieldName); + $backupTable = $this->getBackupTableName($primaryTable); + $this->copyTable($backupTable, $primaryTable, $preserve); + } + + /** + * {@inheritDoc} + */ + public function restoreFieldRevisionTable(string $entityType, string $fieldName, bool $preserve = FALSE): void { + $revisionTable = $this->getFieldRevisionTableName($entityType, $fieldName); + $backupTable = $this->getBackupTableName($revisionTable); + $this->copyTable($backupTable, $revisionTable, $preserve); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Database/DatabaseInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Database/DatabaseInterface.php new file mode 100644 index 0000000000..b13c0a76be --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Database/DatabaseInterface.php @@ -0,0 +1,127 @@ +stringTranslation = $stringTranslation; + } + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + public static function create( + ContainerInterface $container, + array $configuration, + $plugin_id, + $plugin_definition + ) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('string_translation') + ); + } + + /** + * {@inheritDoc} + */ + abstract public function getFields(string $entityType, string $bundle = NULL): array; + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/FieldProvider/Plugin/FieldProviderPluginInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/FieldProvider/Plugin/FieldProviderPluginInterface.php new file mode 100644 index 0000000000..b141dc29fb --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/FieldProvider/Plugin/FieldProviderPluginInterface.php @@ -0,0 +1,23 @@ +alterInfo('va_gov_live_field_migration_field_provider_info'); + $this->setCacheBackend($cache_backend, 'va_gov_live_field_migration_field_provider'); + } + + /** + * {@inheritDoc} + */ + public function getFieldProvider(string $id) : FieldProviderPluginInterface { + try { + return $this->createInstance($id); + } + catch (PluginException) { + throw new FieldProviderNotFoundException("Unknown field provider: $id"); + } + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/FieldProvider/Plugin/FieldProviderPluginManagerInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/FieldProvider/Plugin/FieldProviderPluginManagerInterface.php new file mode 100644 index 0000000000..849e2fefac --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/FieldProvider/Plugin/FieldProviderPluginManagerInterface.php @@ -0,0 +1,24 @@ +pluginManager = $pluginManager; + } + + /** + * {@inheritDoc} + */ + public function getFieldProvider(string $id) : FieldProviderPluginInterface { + return $this->pluginManager->getFieldProvider($id); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/FieldProvider/Resolver/ResolverInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/FieldProvider/Resolver/ResolverInterface.php new file mode 100644 index 0000000000..e6c1d31af0 --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/FieldProvider/Resolver/ResolverInterface.php @@ -0,0 +1,23 @@ +stringTranslation = $stringTranslation; + $this->reporter = $reporter; + $this->migratorFactory = $migratorFactory; + } + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + public static function create( + ContainerInterface $container, + array $configuration, + $plugin_id, + $plugin_definition + ) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('string_translation'), + $container->get('va_gov_live_field_migration.reporter'), + $container->get('va_gov_live_field_migration.migrator_factory') + ); + } + + /** + * {@inheritDoc} + */ + abstract public function runMigration(string $entityType, string $fieldName); + + /** + * {@inheritDoc} + */ + abstract public function rollbackMigration(string $entityType, string $fieldName); + + /** + * {@inheritDoc} + */ + abstract public function verifyMigration(string $entityType, string $fieldName); + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Plugin/MigrationPluginInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Plugin/MigrationPluginInterface.php new file mode 100644 index 0000000000..ed8a8c4297 --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Plugin/MigrationPluginInterface.php @@ -0,0 +1,60 @@ +alterInfo('va_gov_live_field_migration_migration_info'); + $this->setCacheBackend($cache_backend, 'va_gov_live_field_migration_migration'); + } + + /** + * {@inheritDoc} + */ + public function getMigration(string $id) : MigrationPluginInterface { + try { + return $this->createInstance($id); + } + catch (PluginException) { + throw new MigrationNotFoundException("Unknown migration: $id"); + } + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Plugin/MigrationPluginManagerInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Plugin/MigrationPluginManagerInterface.php new file mode 100644 index 0000000000..ab70ac5690 --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Plugin/MigrationPluginManagerInterface.php @@ -0,0 +1,24 @@ +pluginManager = $pluginManager; + $this->entityFieldManager = $entityFieldManager; + } + + /** + * Calculate the migration plugin ID for a given field. + * + * We might someday use a more complex algorithm for this, but right now we: + * + * - Retrieve the field storage for the specified field (or assert). + * - If the field is of type 'string', then we use 'string_to_string_long'. + * - If the field is of type 'text_long', then we use 'text_to_string_long'. + * - Otherwise, we throw an exception. + * + * @param string $entityType + * The entity type. + * @param string $fieldName + * The field name. + * + * @return string + * The migration plugin ID. + * + * @throws \Drupal\va_gov_live_field_migration\Exception\MigrationNotFoundException + * If a suitable strategy cannot be found. + */ + public function getMigrationId(string $entityType, string $fieldName): string { + $fieldStorage = $this->entityFieldManager->getFieldStorageDefinitions($entityType)[$fieldName]; + if ($fieldStorage === NULL) { + throw new MigrationNotFoundException(sprintf('Field %s not found on entity type %s.', $fieldName, $entityType)); + } + if ($fieldStorage->getType() === 'string') { + return 'string_to_string_long'; + } + elseif ($fieldStorage->getType() === 'text_long') { + return 'text_to_string_long'; + } + throw new MigrationNotFoundException(sprintf('No migration found for field %s on entity type %s.', $fieldName, $entityType)); + } + + /** + * {@inheritDoc} + */ + public function getMigration(string $entityType, string $fieldName) : MigrationPluginInterface { + $id = $this->getMigrationId($entityType, $fieldName); + return $this->pluginManager->getMigration($id); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Resolver/ResolverInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Resolver/ResolverInterface.php new file mode 100644 index 0000000000..4d3932003e --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Resolver/ResolverInterface.php @@ -0,0 +1,28 @@ +fieldProviderResolver = $fieldProviderResolver; + $this->migrationResolver = $migrationResolver; + $this->reporter = $reporter; + $this->state = $state; + } + + /** + * Perform an operation, such as migrating, rolling back, or verifying. + * + * @param callable $operation + * The operation to perform. + */ + public function performOperation(callable $operation) { + $startTime = microtime(TRUE); + try { + $operation(); + } + catch (\Exception $exception) { + $this->reporter->reportError('Error: ' . $exception->getMessage()); + } + finally { + $elapsedTime = microtime(TRUE) - $startTime; + $peakMemoryUsage = memory_get_peak_usage(); + $this->reporter->reportInfo('Elapsed time: ' . number_format($elapsedTime, 2) . ' seconds'); + $this->reporter->reportInfo('Peak memory usage: ' . number_format($peakMemoryUsage / 1024 / 1024, 2) . ' MB'); + } + } + + /** + * {@inheritdoc} + */ + public function getFieldProvider(string $id): FieldProviderPluginInterface { + return $this->fieldProviderResolver->getFieldProvider($id); + } + + /** + * {@inheritdoc} + */ + public function getMigration(string $entityType, string $fieldName) : MigrationPluginInterface { + return $this->migrationResolver->getMigration($entityType, $fieldName); + } + + /** + * {@inheritdoc} + */ + public function runMigration(MigrationPluginInterface $migration, string $entityType, string $fieldName) : void { + $this->performOperation(function () use ($migration, $entityType, $fieldName) { + $this->reporter->reportInfo('Migrating field "' . $fieldName . '" on entity type "' . $entityType . '" with migration "' . $migration->getPluginId() . '"...'); + $migration->runMigration($entityType, $fieldName); + $this->reporter->reportInfo('Migration successful.'); + }); + } + + /** + * {@inheritdoc} + */ + public function rollbackMigration(MigrationPluginInterface $migration, string $entityType, string $fieldName) : void { + $this->performOperation(function () use ($migration, $entityType, $fieldName) { + $this->reporter->reportInfo('Rolling back field "' . $fieldName . '" on entity type "' . $entityType . '" with migration "' . $migration->getPluginId() . '"...'); + $migration->rollbackMigration($entityType, $fieldName); + $this->reporter->reportInfo('Rollback successful.'); + }); + } + + /** + * {@inheritdoc} + */ + public function verifyMigration(MigrationPluginInterface $migration, string $entityType, string $fieldName) : void { + $this->performOperation(function () use ($migration, $entityType, $fieldName) { + $this->reporter->reportInfo('Verifying field "' . $fieldName . '" on entity type "' . $entityType . '" with migration "' . $migration->getPluginId() . '"...'); + $migration->rollbackMigration($entityType, $fieldName); + $this->reporter->reportInfo('Verification successful.'); + }); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Runner/RunnerInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Runner/RunnerInterface.php new file mode 100644 index 0000000000..33ebb3cafb --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Runner/RunnerInterface.php @@ -0,0 +1,94 @@ +entityType = $entityType; + $this->fieldName = $fieldName; + $this->migrationId = $migrationId; + $this->status = $status ?? static::DEFAULT_STATUS; + } + + /** + * {@inheritDoc} + */ + public function toJson(): string { + return json_encode($this); + } + + /** + * Validate a data object for deserialization. + * + * @param array $data + * The data object. + * + * @throws \Drupal\va_gov_live_field_migration\Exception\StatusDeserializationException + * If the data object is invalid. + */ + protected static function validateData(array $data) { + if (empty($data['entityType'])) { + throw new StatusDeserializationException('Missing entity type.'); + } + if (empty($data['fieldName'])) { + throw new StatusDeserializationException('Missing field name.'); + } + if (empty($data['migrationId'])) { + throw new StatusDeserializationException('Missing migration ID.'); + } + } + + /** + * Deserializes the object from a JSON string. + * + * @param string $json + * The JSON string. + * + * @return \Drupal\va_gov_live_field_migration\Migration\Status\StatusInterface + * The status object. + * + * @throws \Drupal\va_gov_live_field_migration\Exception\StatusDeserializationException + * If the JSON string cannot be deserialized. + */ + public static function fromJson($json): StatusInterface { + $data = json_decode($json, JSON_OBJECT_AS_ARRAY); + if (empty($data)) { + throw new StatusDeserializationException('Unable to deserialize status.'); + } + self::validateData($data); + $result = new Status( + $data['entityType'], + $data['fieldName'], + $data['migrationId'], + $data['status'] ?? static::DEFAULT_STATUS + ); + return $result; + } + + /** + * {@inheritDoc} + */ + public function getKey(): string { + return Key::getKey($this->migrationId, $this->entityType, $this->fieldName); + } + + /** + * {@inheritDoc} + */ + public function getEntityType(): string { + return $this->entityType; + } + + /** + * {@inheritDoc} + */ + public function getFieldName(): string { + return $this->fieldName; + } + + /** + * {@inheritDoc} + */ + public function getMigrationId(): string { + return $this->migrationId; + } + + /** + * {@inheritDoc} + */ + public function getStatus(): string { + return $this->status; + } + + /** + * {@inheritDoc} + */ + public function setStatus(string $status): void { + $this->status = $status; + } + + /** + * {@inheritDoc} + */ + public function jsonSerialize(): mixed { + return [ + 'entityType' => $this->getEntityType(), + 'fieldName' => $this->getFieldName(), + 'migrationId' => $this->getMigrationId(), + 'status' => $this->getStatus(), + ]; + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Status/StatusInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Status/StatusInterface.php new file mode 100644 index 0000000000..c4290aa2e6 --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Migration/Status/StatusInterface.php @@ -0,0 +1,84 @@ +stringTranslation = $stringTranslation; + $this->reporter = $reporter; + $this->state = $state; + $this->database = $database; + $this->entityDisplayRepository = $entityDisplayRepository; + $this->entityFieldManager = $entityFieldManager; + $this->entityTypeManager = $entityTypeManager; + $this->entityTypeRepository = $entityTypeRepository; + $this->fieldPurger = $fieldPurger; + } + + /** + * {@inheritDoc} + */ + protected function getReporter(): ReporterInterface { + return $this->reporter; + } + + /** + * {@inheritDoc} + */ + protected function getState(): StateInterface { + return $this->state; + } + + /** + * {@inheritDoc} + */ + protected function getDatabase(): DatabaseInterface { + return $this->database; + } + + /** + * {@inheritDoc} + */ + protected function getEntityDisplayRepository(): EntityDisplayRepositoryInterface { + return $this->entityDisplayRepository; + } + + /** + * {@inheritDoc} + */ + protected function getEntityFieldManager(): EntityFieldManagerInterface { + return $this->entityFieldManager; + } + + /** + * {@inheritDoc} + */ + protected function getEntityTypeManager(): EntityTypeManagerInterface { + return $this->entityTypeManager; + } + + /** + * {@inheritDoc} + */ + protected function getEntityTypeRepository(): EntityTypeRepositoryInterface { + return $this->entityTypeRepository; + } + + /** + * {@inheritDoc} + */ + public function getStringToStringLongMigrator(string $entityType, string $fieldName): MigratorInterface { + return new StringToStringLongMigrator( + $this->reporter, + $this->state, + $this->database, + $this->stringTranslation, + $this->entityDisplayRepository, + $this->entityFieldManager, + $this->entityTypeManager, + $this->entityTypeRepository, + $this->fieldPurger, + $entityType, + $fieldName + ); + } + + /** + * {@inheritDoc} + */ + public function getTextToStringLongMigrator(string $entityType, string $fieldName): MigratorInterface { + return new TextToStringLongMigrator( + $this->reporter, + $this->state, + $this->database, + $this->stringTranslation, + $this->entityDisplayRepository, + $this->entityFieldManager, + $this->entityTypeManager, + $this->entityTypeRepository, + $this->fieldPurger, + $entityType, + $fieldName + ); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Factory/FactoryInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Factory/FactoryInterface.php new file mode 100644 index 0000000000..435f8b83fa --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Factory/FactoryInterface.php @@ -0,0 +1,38 @@ +reporter = $reporter; + $this->state = $state; + $this->database = $database; + $this->stringTranslation = $stringTranslation; + $this->entityDisplayRepository = $entityDisplayRepository; + $this->entityFieldManager = $entityFieldManager; + $this->entityTypeManager = $entityTypeManager; + $this->entityTypeRepository = $entityTypeRepository; + $this->fieldPurger = $fieldPurger; + $this->entityType = $entityType; + $this->fieldName = $fieldName; + } + + /** + * {@inheritDoc} + */ + public function getEntityType(): string { + return $this->entityType; + } + + /** + * {@inheritDoc} + */ + public function getFieldName(): string { + return $this->fieldName; + } + + /** + * {@inheritDoc} + */ + protected function getState(): StateInterface { + return $this->state; + } + + /** + * {@inheritDoc} + */ + protected function getReporter(): ReporterInterface { + return $this->reporter; + } + + /** + * {@inheritDoc} + */ + protected function getDatabase(): DatabaseInterface { + return $this->database; + } + + /** + * {@inheritDoc} + */ + protected function getEntityDisplayRepository(): EntityDisplayRepositoryInterface { + return $this->entityDisplayRepository; + } + + /** + * {@inheritDoc} + */ + protected function getEntityFieldManager(): EntityFieldManagerInterface { + return $this->entityFieldManager; + } + + /** + * {@inheritDoc} + */ + protected function getEntityTypeManager(): EntityTypeManagerInterface { + return $this->entityTypeManager; + } + + /** + * {@inheritDoc} + */ + protected function getEntityTypeRepository(): EntityTypeRepositoryInterface { + return $this->entityTypeRepository; + } + + /** + * Get the source type for this migration. + * + * @return string + * The source type. + */ + protected function getSourceType(): string { + return 'string'; + } + + /** + * Get the destination type for this migration. + * + * @return string + * The destination type. + */ + protected function getDestinationType(): string { + return 'string_long'; + } + + /** + * Runs a migration. + * + * @throws \Drupal\va_gov_live_field_migration\Exception\MigrationRunException + * If the migration run fails. + */ + public function run() { + $this->verifyField($this->getSourceType()); + $this->backupFieldTables(); + $this->fieldConfigs = []; + $this->formDisplayConfigs = []; + $this->viewDisplayConfigs = []; + $this->backupFieldStorageConfig(); + $bundles = $this->getFieldBundles(); + foreach ($bundles as $bundle => $label) { + $this->reporter->reportInfo("Backing up data for field {$this->fieldName} on bundle {$bundle} ({$label})..."); + $this->backupFieldConfig($bundle); + $this->backupFormDisplayConfig($bundle); + $this->backupViewDisplayConfig($bundle); + } + $this->deleteFieldStorageConfig(); + $this->fieldPurger->purge(); + $this->alterFieldStorageConfig($this->getDestinationType()); + $this->createFieldStorageConfig($this->workingFieldStorageConfig); + $this->alterFieldConfigs($this->getDestinationType()); + $this->createFieldConfigs($this->workingFieldConfigs); + foreach ($bundles as $bundle => $label) { + $this->alterFormDisplayConfig($bundle); + $this->alterViewDisplayConfig($bundle); + $this->updateFormDisplayConfig($bundle); + $this->updateViewDisplayConfig($bundle); + } + // $this->database + // ->restorePrimaryFieldTable($this->entityType, $this->fieldName, TRUE); + // $this->database + // ->restoreFieldRevisionTable($this->entityType, $this->fieldName, TRUE); + } + + /** + * Backup the field storage config. + */ + public function backupFieldStorageConfig(): void { + $this->reporter->reportInfo("Backing up field storage config for field {$this->fieldName}..."); + $fieldStorageConfig = $this->getFieldStorageConfig(); + $fieldStorageConfigArray = $fieldStorageConfig->toArray(); + $this->fieldStorageConfigBackup = $fieldStorageConfigArray; + $this->workingFieldStorageConfig = $fieldStorageConfigArray; + } + + /** + * Alter the field storage config. + * + * @param string $destinationType + * The destination type. + * @param string $destinationSettings + * The destination settings. + */ + public function alterFieldStorageConfig(string $destinationType, array $destinationSettings = []): void { + $this->reporter->reportInfo("Altering field storage config for field {$this->fieldName}..."); + $this->workingFieldStorageConfig['type'] = $destinationType; + $this->workingFieldStorageConfig['settings'] = $destinationSettings; + } + + /** + * Restore the field storage config. + * + * @param array $fieldStorageConfig + * The field storage config. + */ + public function restoreFieldStorageConfig(array $fieldStorageConfig): void { + $this->reporter->reportInfo("Restoring field storage config for field {$this->fieldName}..."); + $this->workingFieldStorageConfig = $fieldStorageConfig; + } + + /** + * Backup the field config. + * + * @param string $bundle + * The bundle. + */ + public function backupFieldConfig(string $bundle): void { + $this->reporter->reportInfo("Backing up field config for field {$this->fieldName} on bundle {$bundle}..."); + $fieldConfig = $this->getFieldConfig($bundle); + $fieldConfigArray = $fieldConfig->toArray(); + $this->fieldConfigBackups[$bundle] = $fieldConfigArray; + $this->workingFieldConfigs[$bundle] = $fieldConfigArray; + } + + /** + * Alter the field configs. + * + * @param string $destinationType + * The destination type. + * @param string $destinationSettings + * The destination settings. + */ + public function alterFieldConfigs(string $destinationType, array $destinationSettings = []): void { + $this->reporter->reportInfo("Altering field configs for field {$this->fieldName}..."); + foreach ($this->workingFieldConfigs as $bundle => $fieldConfig) { + $this->alterFieldConfig($bundle, $destinationType, $destinationSettings); + } + } + + /** + * Alter the field config. + * + * @param string $bundle + * The bundle. + * @param string $destinationType + * The destination type. + * @param string $destinationSettings + * The destination settings. + */ + public function alterFieldConfig(string $bundle, string $destinationType, array $destinationSettings = []): void { + $this->reporter->reportInfo("Altering field config for field {$this->fieldName} on bundle {$bundle}..."); + $this->workingFieldConfigs[$bundle]['type'] = $destinationType; + $this->workingFieldConfigs[$bundle]['settings'] = $destinationSettings; + } + + /** + * Backup the form display config. + * + * @param string $bundle + * The bundle. + */ + public function backupFormDisplayConfig(string $bundle): void { + $this->reporter->reportInfo("Backing up form display config for field {$this->fieldName} on bundle {$bundle}..."); + $formModeOptions = $this->getFormModeOptions($bundle); + foreach ($formModeOptions as $formMode => $options) { + $formDisplayConfig = $this->getFormDisplayConfig($bundle, $formMode); + $formDisplayConfigArray = $formDisplayConfig->toArray(); + $this->formDisplayConfigBackups[$bundle][$formMode] = $formDisplayConfigArray; + $this->workingFormDisplayConfigs[$bundle][$formMode] = $formDisplayConfigArray; + } + } + + /** + * Alter the form display config. + * + * @param string $bundle + * The bundle. + */ + public function alterFormDisplayConfig(string $bundle): void { + $this->reporter->reportInfo("Altering form display config for field {$this->fieldName} on bundle {$bundle}..."); + $formModeOptions = $this->getFormModeOptions($bundle); + foreach ($formModeOptions as $formMode => $options) { + $this->alterFormDisplayConfigForMode($bundle, $formMode); + } + } + + /** + * Alter the form display config for a specific mode. + * + * @param string $bundle + * The bundle. + * @param string $formMode + * The form mode. + */ + public function alterFormDisplayConfigForMode(string $bundle, string $formMode): void { + $this->reporter->reportInfo("Altering form display config for field {$this->fieldName} on bundle {$bundle} for form mode {$formMode}..."); + $savedConfig = $this->workingFormDisplayConfigs[$bundle][$formMode]['content']; + $fieldName = $this->fieldName; + // If not present, then it is disabled and we don't need to care about it. + if (!isset($savedConfig[$fieldName])) { + return; + } + $desiredType = $this->getDestinationType() . '_textfield_with_counter'; + // This already has the desired configuration. + if ($savedConfig[$fieldName]['type'] === $desiredType) { + return; + } + $savedConfig[$fieldName]['type'] = $desiredType; + $savedConfig[$fieldName]['settings']['maxlength'] = $this->workingFieldStorageConfig['settings']['max_length']; + $this->workingFormDisplayConfigs[$bundle][$formMode]['content'] = $savedConfig; + } + + /** + * Update the form display config. + * + * @param string $bundle + * The bundle. + */ + public function updateFormDisplayConfig(string $bundle): void { + $this->reporter->reportInfo("Updating form display config for field {$this->fieldName} on bundle {$bundle}..."); + $formModeOptions = $this->getFormModeOptions($bundle); + foreach ($formModeOptions as $formMode => $options) { + $this->updateFormDisplayConfigForMode($bundle, $formMode, $this->workingFormDisplayConfigs[$bundle][$formMode]['content']); + } + } + + /** + * Update the form display config for a specific mode. + * + * @param string $bundle + * The bundle. + * @param string $formMode + * The form mode. + * @param array $config + * The config. + */ + public function updateFormDisplayConfigForMode(string $bundle, string $formMode, array $config): void { + $this->reporter->reportInfo("Updating form display config for field {$this->fieldName} on bundle {$bundle} for form mode {$formMode}..."); + $this->getEntityDisplayRepository() + ->getFormDisplay($this->entityType, $bundle, $formMode) + ->setComponent($this->fieldName, $config) + ->save(); + } + + /** + * Backup the view display config. + * + * @param string $bundle + * The bundle. + */ + public function backupViewDisplayConfig(string $bundle): void { + $this->reporter->reportInfo("Backing up view display config for field {$this->fieldName} on bundle {$bundle}..."); + $viewModeOptions = $this->getViewModeOptions($bundle); + foreach ($viewModeOptions as $viewMode => $options) { + $viewDisplayConfig = $this->getViewDisplayConfig($bundle, $viewMode); + $viewDisplayConfigArray = $viewDisplayConfig->toArray(); + $this->viewDisplayConfigBackups[$bundle][$viewMode] = $viewDisplayConfigArray; + $this->workingViewDisplayConfigs[$bundle][$viewMode] = $viewDisplayConfigArray; + } + } + + /** + * Alter the view display config. + * + * @param string $bundle + * The bundle. + */ + public function alterViewDisplayConfig(string $bundle): void { + $this->reporter->reportInfo("Altering view display config for field {$this->fieldName} on bundle {$bundle}..."); + $viewModeOptions = $this->getViewModeOptions($bundle); + foreach ($viewModeOptions as $viewMode => $options) { + $this->alterViewDisplayConfigForMode($bundle, $viewMode); + } + } + + /** + * Alter the view display config for a specific mode. + * + * @param string $bundle + * The bundle. + * @param string $viewMode + * The view mode. + */ + public function alterViewDisplayConfigForMode(string $bundle, string $viewMode): void { + $this->reporter->reportInfo("Altering view display config for field {$this->fieldName} on bundle {$bundle} for view mode {$viewMode}..."); + $savedConfig = $this->workingViewDisplayConfigs[$bundle][$viewMode]; + print_r($savedConfig); + $fieldName = $this->fieldName; + // If not present, then it is disabled and we don't need to care about it. + if (!isset($savedConfig[$fieldName])) { + return; + } + // We're just replacing the old settings, so this is basically a no-op. + $this->workingViewDisplayConfigs[$bundle][$viewMode] = $savedConfig; + } + + /** + * Update the view display config. + * + * @param string $bundle + * The bundle. + */ + public function updateViewDisplayConfig(string $bundle): void { + $this->reporter->reportInfo("Updating view display config for field {$this->fieldName} on bundle {$bundle}..."); + $viewModeOptions = $this->getViewModeOptions($bundle); + foreach ($viewModeOptions as $viewMode => $options) { + $this->updateViewDisplayConfigForMode($bundle, $viewMode, $this->workingViewDisplayConfigs[$bundle][$viewMode]['content']); + } + } + + /** + * Update the view display config for a specific mode. + * + * @param string $bundle + * The bundle. + * @param string $viewMode + * The view mode. + * @param array $config + * The config. + */ + public function updateViewDisplayConfigForMode(string $bundle, string $viewMode, array $config): void { + $this->reporter->reportInfo("Updating view display config for field {$this->fieldName} on bundle {$bundle} for view mode {$viewMode}..."); + $this->getEntityDisplayRepository() + ->getViewDisplay($this->entityType, $bundle, $viewMode) + ->setComponent($this->fieldName, $config) + ->save(); + } + + /** + * Rolls back a migration. + * + * @throws \Drupal\va_gov_live_field_migration\Exception\MigrationRollbackException + * If the migration rollback fails. + */ + public function rollback() { + throw new MigrationRollbackException('Not implemented.'); + } + + /** + * Verifies the migration. + * + * @throws \Drupal\va_gov_live_field_migration\Exception\MigrationVerificationException + * If the migration verification fails. + */ + public function verify() { + throw new MigrationVerificationException('Not implemented.'); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/MigratorInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/MigratorInterface.php new file mode 100644 index 0000000000..8cd1e83d56 --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/MigratorInterface.php @@ -0,0 +1,34 @@ +getReporter()->reportInfo("Dropping table {$table}..."); + $this->getDatabase()->dropTable($table); + } + + /** + * {@inheritDoc} + */ + public function backupFieldTables(): void { + $entityType = $this->getEntityType(); + $fieldName = $this->getFieldName(); + $this->getReporter()->reportInfo("Backing up tables for field {$fieldName} on entity {$entityType}..."); + $this->getDatabase()->backupPrimaryFieldTable($entityType, $fieldName); + $this->getDatabase()->backupFieldRevisionTable($entityType, $fieldName); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Traits/EntityDisplayOperationsInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Traits/EntityDisplayOperationsInterface.php new file mode 100644 index 0000000000..fd1267f1b4 --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Traits/EntityDisplayOperationsInterface.php @@ -0,0 +1,61 @@ +getEntityType(); + return $this->getEntityDisplayRepository()->getFormModeOptionsByBundle($entityType, $bundle); + } + + /** + * {@inheritDoc} + */ + public function getFormDisplayConfig(string $bundle, string $formMode): EntityFormDisplayInterface { + $entityType = $this->getEntityType(); + return $this->getEntityDisplayRepository()->getFormDisplay($entityType, $bundle, $formMode); + } + + /** + * {@inheritDoc} + */ + public function getViewModeOptions(string $bundle): array { + $entityType = $this->getEntityType(); + return $this->getEntityDisplayRepository()->getViewModeOptionsByBundle($entityType, $bundle); + } + + /** + * {@inheritDoc} + */ + public function getViewDisplayConfig(string $bundle, string $viewMode): EntityViewDisplayInterface { + $entityType = $this->getEntityType(); + return $this->getEntityDisplayRepository()->getViewDisplay($entityType, $bundle, $viewMode); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Traits/FieldStorageOperationsInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Traits/FieldStorageOperationsInterface.php new file mode 100644 index 0000000000..3f94fff0a9 --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Traits/FieldStorageOperationsInterface.php @@ -0,0 +1,68 @@ +getEntityType(); + $fieldName = $this->getFieldName(); + $this->reporter->reportInfo("Verifying field {$fieldName} on entity {$entityType}..."); + $fieldStorageDefinition = $this->getEntityFieldManager()->getFieldStorageDefinitions($entityType)[$fieldName]; + if ($fieldStorageDefinition == NULL) { + throw new MigrationFieldNotFoundException("Field $fieldName not found on $entityType."); + } + if ($fieldStorageDefinition->getType() !== $fieldType) { + throw new MigrationFieldWrongTypeException("Field $fieldName on $entityType is not of type $fieldType."); + } + } + + /** + * {@inheritDoc} + */ + public function getFieldStorageConfig(): FieldStorageConfigInterface { + $entityType = $this->getEntityType(); + $fieldName = $this->getFieldName(); + $fieldStorageConfigStorage = $this->getEntityTypeManager()->getStorage('field_storage_config'); + $fieldStorageConfig = $fieldStorageConfigStorage->load($entityType . '.' . $fieldName); + return $fieldStorageConfig; + } + + /** + * {@inheritDoc} + */ + public function getFieldConfig(string $bundle): FieldConfigInterface { + $entityType = $this->getEntityType(); + $fieldName = $this->getFieldName(); + $fieldConfigStorage = $this->getEntityTypeManager()->getStorage('field_config'); + $fieldConfig = $fieldConfigStorage->load($entityType . '.' . $bundle . '.' . $fieldName); + return $fieldConfig; + } + + /** + * {@inheritDoc} + */ + public function getFieldBundles(): array { + $entityType = $this->getEntityType(); + $fieldName = $this->getFieldName(); + $fieldStorageConfig = $this->getFieldStorageConfig($entityType, $fieldName); + return $fieldStorageConfig->getBundles(); + } + + /** + * {@inheritDoc} + */ + public function deleteFieldStorageConfig(): void { + $entityType = $this->getEntityType(); + $fieldName = $this->getFieldName(); + $this->reporter->reportInfo("Deleting field storage config for field {$fieldName} on entity {$entityType}..."); + $fieldStorageConfig = $this->getFieldStorageConfig($entityType, $fieldName); + $fieldStorageConfig->delete(); + } + + /** + * {@inheritDoc} + */ + public function createFieldStorageConfig(array $fieldStorageConfig): void { + $entityType = $this->getEntityType(); + $fieldName = $this->getFieldName(); + $this->reporter->reportInfo("Creating field storage config for field {$fieldName} on entity {$entityType}..."); + $fieldStorageConfigStorage = $this->getEntityTypeManager()->getStorage('field_storage_config'); + $fieldStorageConfig = $fieldStorageConfigStorage->create($fieldStorageConfig); + $fieldStorageConfig->save(); + } + + /** + * {@inheritDoc} + */ + public function createFieldConfigs(array $fieldConfigs): void { + $entityType = $this->getEntityType(); + $fieldName = $this->getFieldName(); + $this->reporter->reportInfo("Creating field configs for field {$fieldName} on entity {$entityType}..."); + $fieldConfigStorage = $this->getEntityTypeManager()->getStorage('field_config'); + foreach ($fieldConfigs as $fieldConfig) { + $fieldConfig = $fieldConfigStorage->create($fieldConfig); + $fieldConfig->save(); + } + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Traits/MigrationStatusInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Traits/MigrationStatusInterface.php new file mode 100644 index 0000000000..995df37a5f --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Migrator/Traits/MigrationStatusInterface.php @@ -0,0 +1,54 @@ +getEntityType(); + $fieldName = $this->getFieldName(); + $status = $this->getState()->getStatus($this->getPluginId(), $entityType, $fieldName); + if (!$status) { + $status = $this->getState()->createStatus($this->getPluginId(), $entityType, $fieldName); + } + return $status; + } + + /** + * {@inheritDoc} + */ + public function setMigrationStatusObject(StatusInterface $status): void { + $this->getState()->setStatus($status); + } + + /** + * {@inheritDoc} + */ + public function deleteMigrationStatusObject(): void { + $entityType = $this->getEntityType(); + $fieldName = $this->getFieldName(); + $this->getState()->deleteStatus($this->getPluginId(), $entityType, $fieldName); + } + + /** + * {@inheritDoc} + */ + public function getMigrationStatus(): string { + return $this->getMigrationStatusObject()->getStatus(); + } + + /** + * {@inheritDoc} + */ + public function updateMigrationStatus(string $status): void { + $statusObject = $this->getMigrationStatusObject(); + $statusObject->setStatus($status); + $this->setMigrationStatusObject($statusObject); + } + + /** + * {@inheritDoc} + */ + public function deleteMigrationStatus(): void { + $this->deleteMigrationStatusObject(); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/FieldProvider/Issue14995.php b/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/FieldProvider/Issue14995.php new file mode 100644 index 0000000000..c326e5e47d --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/FieldProvider/Issue14995.php @@ -0,0 +1,148 @@ + [ + 'field_phone_label', + 'field_alert_heading', + 'field_text_expander', + 'field_error_message', + 'field_section_header', + 'field_question', + 'field_email_label', + 'field_button_label', + 'field_loading_message', + 'field_short_phrase_with_a_number', + 'field_title', + 'field_link_summary', + ], + 'node' => [ + 'field_teaser_text', + 'field_description', + 'field_home_page_hub_label', + ], + ]; + + const VALID_FIELD_TYPES = [ + 'string', + 'text', + ]; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected EntityFieldManagerInterface $entityFieldManager; + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + TranslationInterface $stringTranslation, + EntityFieldManagerInterface $entityFieldManager + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $stringTranslation); + $this->entityFieldManager = $entityFieldManager; + } + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + public static function create( + ContainerInterface $container, + array $configuration, + $plugin_id, + $plugin_definition + ) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('string_translation'), + $container->get('entity_field.manager') + ); + } + + /** + * Determine whether this field is still a valid target. + * + * @param string $entityType + * The entity type. + * @param string $fieldName + * The field name. + * + * @return bool + * Whether this field is still a valid target. + */ + public function isStillValid(string $entityType, string $fieldName) : bool { + $fieldStorage = $this->entityFieldManager->getFieldStorageDefinitions($entityType)[$fieldName]; + if ($fieldStorage === NULL) { + return FALSE; + } + return in_array($fieldStorage->getType(), static::VALID_FIELD_TYPES); + } + + /** + * Determine whether this field exists on a specified bundle. + * + * @param string $entityType + * The entity type. + * @param string $fieldName + * The field name. + * @param string $bundle + * The bundle. + * + * @return bool + * Whether this field exists on a specified bundle. + */ + public function fieldExistsOnBundle(string $entityType, string $fieldName, string $bundle) : bool { + $fieldConfig = $this->entityFieldManager->getFieldDefinitions($entityType, $bundle)[$fieldName]; + return $fieldConfig !== NULL; + } + + /** + * {@inheritDoc} + */ + public function getFields(string $entityType, string $bundle = NULL) : array { + $fields = static::FIELDS[$entityType] ?? []; + if ($bundle !== NULL) { + $fields = array_filter($fields, function ($field) use ($entityType, $bundle) { + return $this->fieldExistsOnBundle($entityType, $field, $bundle); + }); + } + $fields = array_filter($fields, function ($field) use ($entityType) { + return $this->isStillValid($entityType, $field); + }); + return $fields; + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/FieldProvider/TestEmptyList.php b/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/FieldProvider/TestEmptyList.php new file mode 100644 index 0000000000..f78fbdf01b --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/FieldProvider/TestEmptyList.php @@ -0,0 +1,24 @@ +migratorFactory->getStringToStringLongMigrator($entityType, $fieldName); + $migrator->run(); + } + + /** + * {@inheritDoc} + */ + public function rollbackMigration(string $entityType, string $fieldName) : void { + $migrator = $this->migratorFactory->getStringToStringLongMigrator($entityType, $fieldName); + $migrator->rollback(); + } + + /** + * {@inheritDoc} + */ + public function verifyMigration(string $entityType, string $fieldName) : void { + $migrator = $this->migratorFactory->getStringToStringLongMigrator($entityType, $fieldName); + $migrator->verify(); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/Migration/TestException.php b/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/Migration/TestException.php new file mode 100644 index 0000000000..dd1926ac19 --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/Migration/TestException.php @@ -0,0 +1,43 @@ +reporter->reportInfo("Successfully completed a migration for $entityType $fieldName."); + } + + /** + * {@inheritDoc} + */ + public function rollbackMigration(string $entityType, string $fieldName) : void { + $this->reporter->reportInfo("Successfully rolled back a migration for $entityType $fieldName."); + } + + /** + * {@inheritDoc} + */ + public function verifyMigration(string $entityType, string $fieldName) : void { + $this->reporter->reportInfo("Successfully verified back a migration for $entityType $fieldName."); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/Migration/TextToStringLong.php b/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/Migration/TextToStringLong.php new file mode 100644 index 0000000000..3c191060ea --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Plugin/Migration/TextToStringLong.php @@ -0,0 +1,41 @@ +migrationFactory->getTextToStringLongMigrator($entityType, $fieldName); + $migrator->run(); + } + + /** + * {@inheritDoc} + */ + public function rollbackMigration(string $entityType, string $fieldName) : void { + $migrator = $this->migrationFactory->getTextToStringLongMigrator($entityType, $fieldName); + $migrator->rollback(); + } + + /** + * {@inheritDoc} + */ + public function verifyMigration(string $entityType, string $fieldName) : void { + $migrator = $this->migrationFactory->getTextToStringLongMigrator($entityType, $fieldName); + $migrator->verify(); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Reporter/Reporter.php b/docroot/modules/custom/va_gov_live_field_migration/src/Reporter/Reporter.php new file mode 100644 index 0000000000..4b8a416f6e --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Reporter/Reporter.php @@ -0,0 +1,87 @@ +messenger = $messenger; + $this->logger = $loggerFactory->get('va_gov_live_field_migration'); + } + + /** + * Check if we're in a Drush environment. + * + * @return bool + * TRUE if in a Drush environment, FALSE otherwise. + */ + protected function isDrushEnvironment() : bool { + return class_exists(Drush::class); + } + + /** + * {@inheritDoc} + */ + public function reportInfo(string $message) : void { + if ($this->isDrushEnvironment()) { + Drush::service('output')->writeln($message); + } + else { + $this->messenger->addStatus($message); + } + $this->logger->info($message); + } + + /** + * {@inheritDoc} + */ + public function reportError(string $message, \Throwable $exception = NULL) : void { + if ($this->isDrushEnvironment()) { + Drush::service('output')->writeln("$message"); + } + else { + $this->messenger->addError($message); + } + $this->logger->error($message); + if ($exception) { + // When we are on Drupal 10.0.0, we can use the following: + // Error::logException($this->logger, $exception); + // Until then, we have to do this: + watchdog_exception('va_gov_live_field_migration', $exception); + } + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/Reporter/ReporterInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/Reporter/ReporterInterface.php new file mode 100644 index 0000000000..4414c753de --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/Reporter/ReporterInterface.php @@ -0,0 +1,34 @@ +coreState = $coreState; + } + + /** + * {@inheritDoc} + */ + public function getStatus(string $migrationId, string $entityType, string $fieldName): StatusInterface { + $key = Key::getKey($migrationId, $entityType, $fieldName); + $status = $this->coreState->get($key); + if ($status === NULL) { + throw new StatusNotFoundException("Status not found for key: $key"); + } + return Status::fromJson($status); + } + + /** + * {@inheritDoc} + */ + public function createStatus(string $migrationId, string $entityType, string $fieldName): StatusInterface { + $status = new Status($migrationId, $entityType, $fieldName); + $this->setStatus($status); + return $this->getStatus($migrationId, $entityType, $fieldName); + } + + /** + * {@inheritDoc} + */ + public function setStatus(StatusInterface $status): void { + $this->coreState->set($status->getKey(), $status->toJson()); + } + + /** + * {@inheritDoc} + */ + public function deleteStatus(string $migrationId, string $entityType, string $fieldName): void { + $key = Key::getKey($migrationId, $entityType, $fieldName); + $this->coreState->delete($key); + } + +} diff --git a/docroot/modules/custom/va_gov_live_field_migration/src/State/StateInterface.php b/docroot/modules/custom/va_gov_live_field_migration/src/State/StateInterface.php new file mode 100644 index 0000000000..5b90b0f4ec --- /dev/null +++ b/docroot/modules/custom/va_gov_live_field_migration/src/State/StateInterface.php @@ -0,0 +1,65 @@ +assertInstanceOf(FieldProviderPluginManager::class, $pluginManager); + $this->assertInstanceOf(FieldProviderPluginManagerInterface::class, $pluginManager); + } + +} diff --git a/tests/phpunit/va_gov_live_field_migration/functional/FieldProvider/Resolver/ResolverTest.php b/tests/phpunit/va_gov_live_field_migration/functional/FieldProvider/Resolver/ResolverTest.php new file mode 100644 index 0000000000..dad6b61cee --- /dev/null +++ b/tests/phpunit/va_gov_live_field_migration/functional/FieldProvider/Resolver/ResolverTest.php @@ -0,0 +1,30 @@ +assertInstanceOf(Resolver::class, $resolver); + $this->assertInstanceOf(ResolverInterface::class, $resolver); + } + +} diff --git a/tests/phpunit/va_gov_live_field_migration/functional/Migration/Plugin/MigrationPluginManagerTest.php b/tests/phpunit/va_gov_live_field_migration/functional/Migration/Plugin/MigrationPluginManagerTest.php new file mode 100644 index 0000000000..7da9bd225a --- /dev/null +++ b/tests/phpunit/va_gov_live_field_migration/functional/Migration/Plugin/MigrationPluginManagerTest.php @@ -0,0 +1,30 @@ +assertInstanceOf(MigrationPluginManager::class, $pluginManager); + $this->assertInstanceOf(MigrationPluginManagerInterface::class, $pluginManager); + } + +} diff --git a/tests/phpunit/va_gov_live_field_migration/functional/Migration/Resolver/ResolverTest.php b/tests/phpunit/va_gov_live_field_migration/functional/Migration/Resolver/ResolverTest.php new file mode 100644 index 0000000000..0c9d179b78 --- /dev/null +++ b/tests/phpunit/va_gov_live_field_migration/functional/Migration/Resolver/ResolverTest.php @@ -0,0 +1,30 @@ +assertInstanceOf(Resolver::class, $resolver); + $this->assertInstanceOf(ResolverInterface::class, $resolver); + } + +} diff --git a/tests/phpunit/va_gov_live_field_migration/functional/Migration/Runner/RunnerTest.php b/tests/phpunit/va_gov_live_field_migration/functional/Migration/Runner/RunnerTest.php new file mode 100644 index 0000000000..6016b6b33a --- /dev/null +++ b/tests/phpunit/va_gov_live_field_migration/functional/Migration/Runner/RunnerTest.php @@ -0,0 +1,30 @@ +assertInstanceOf(Runner::class, $runner); + $this->assertInstanceOf(RunnerInterface::class, $runner); + } + +} diff --git a/tests/phpunit/va_gov_live_field_migration/functional/Reporter/ReporterTest.php b/tests/phpunit/va_gov_live_field_migration/functional/Reporter/ReporterTest.php new file mode 100644 index 0000000000..68b9d210ac --- /dev/null +++ b/tests/phpunit/va_gov_live_field_migration/functional/Reporter/ReporterTest.php @@ -0,0 +1,30 @@ +assertInstanceOf(Reporter::class, $reporter); + $this->assertInstanceOf(ReporterInterface::class, $reporter); + } + +} diff --git a/tests/phpunit/va_gov_live_field_migration/functional/State/StateTest.php b/tests/phpunit/va_gov_live_field_migration/functional/State/StateTest.php new file mode 100644 index 0000000000..ddc18e9d7f --- /dev/null +++ b/tests/phpunit/va_gov_live_field_migration/functional/State/StateTest.php @@ -0,0 +1,63 @@ +assertInstanceOf(State::class, $state); + $this->assertInstanceOf(StateInterface::class, $state); + } + + /** + * Test that the service throws an exception when the status is not found. + */ + public function testStatusNotFoundException() { + $this->expectException(StatusNotFoundException::class); + $state = \Drupal::service('va_gov_live_field_migration.state'); + $state->deleteStatus('test_migration', 'node', 'field_test'); + $state->getStatus('test_migration', 'node', 'field_test'); + } + + /** + * Test that the service can set and get status. + * + * @covers ::setStatus + * @covers ::getStatus + */ + public function testSetAndGetStatus() { + $state = \Drupal::service('va_gov_live_field_migration.state'); + $status = new Status('node', 'field_test', 'test_migration', 'not_started'); + $this->assertEquals('test_migration', $status->getMigrationId()); + $this->assertEquals('node', $status->getEntityType()); + $this->assertEquals('field_test', $status->getFieldName()); + $this->assertEquals(StatusInterface::DEFAULT_STATUS, $status->getStatus()); + $this->assertEquals('{"entityType":"node","fieldName":"field_test","migrationId":"test_migration","status":"not_started"}', $status->toJson()); + $status->setStatus('started'); + $state->setStatus($status); + $status = $state->getStatus('test_migration', 'node', 'field_test'); + $this->assertEquals('started', $status->getStatus()); + } + +} diff --git a/tests/phpunit/va_gov_live_field_migration/unit/Migration/Status/StatusTest.php b/tests/phpunit/va_gov_live_field_migration/unit/Migration/Status/StatusTest.php new file mode 100644 index 0000000000..6ed85baab0 --- /dev/null +++ b/tests/phpunit/va_gov_live_field_migration/unit/Migration/Status/StatusTest.php @@ -0,0 +1,89 @@ +assertInstanceOf(Status::class, $status); + $this->assertEquals('node', $status->getEntityType()); + $this->assertEquals('field_test', $status->getFieldName()); + $this->assertEquals('test_migration', $status->getMigrationId()); + $this->assertEquals('test', $status->getStatus()); + } + + /** + * Test that we can construct a status from arguments, including a default. + */ + public function testConstructWithDefault() { + $status = new Status('node', 'field_test', 'test_migration'); + $this->assertInstanceOf(Status::class, $status); + $this->assertEquals('node', $status->getEntityType()); + $this->assertEquals('field_test', $status->getFieldName()); + $this->assertEquals('test_migration', $status->getMigrationId()); + $this->assertEquals(StatusInterface::DEFAULT_STATUS, $status->getStatus()); + } + + /** + * Test that we can set and get the status. + * + * @covers ::setStatus + * @covers ::getStatus + */ + public function testSetAndGetStatus() { + $status = new Status('node', 'field_test', 'test_migration'); + $this->assertEquals(StatusInterface::DEFAULT_STATUS, $status->getStatus()); + $status->setStatus('test'); + $this->assertEquals('test', $status->getStatus()); + } + + /** + * Test that we can get the key. + * + * @covers ::getKey + */ + public function testGetKey() { + $status = new Status('node', 'field_test', 'test_migration'); + $this->assertEquals('va_gov_live_field_migration__test_migration__node__field_test', $status->getKey()); + } + + /** + * Test that we can serialize and deserialize the status. + * + * @covers ::jsonSerialize + * @covers ::fromJson + * @covers ::toJson + */ + public function testSerialize() { + $status = new Status('node', 'field_test', 'test_migration'); + $this->assertEquals('{"entityType":"node","fieldName":"field_test","migrationId":"test_migration","status":"not_started"}', $status->toJson()); + $status = Status::fromJson('{"entityType":"node","fieldName":"field_test","migrationId":"test_migration","status":"not_started"}'); + $this->assertEquals('node', $status->getEntityType()); + $this->assertEquals('field_test', $status->getFieldName()); + $this->assertEquals('test_migration', $status->getMigrationId()); + $this->assertEquals(StatusInterface::DEFAULT_STATUS, $status->getStatus()); + } + +} diff --git a/tests/phpunit/va_gov_live_field_migration/unit/Reporter/ReporterTest.php b/tests/phpunit/va_gov_live_field_migration/unit/Reporter/ReporterTest.php new file mode 100644 index 0000000000..2fd5b33222 --- /dev/null +++ b/tests/phpunit/va_gov_live_field_migration/unit/Reporter/ReporterTest.php @@ -0,0 +1,90 @@ +prophesize(MessengerInterface::class); + $messenger = $messengerProphecy->reveal(); + $loggerProphecy = $this->prophesize(LoggerInterface::class); + $logger = $loggerProphecy->reveal(); + $loggerChannelFactoryProphecy = $this->prophesize(LoggerChannelFactoryInterface::class); + $loggerChannelFactoryProphecy->get('va_gov_live_field_migration')->willReturn($logger); + $loggerChannelFactory = $loggerChannelFactoryProphecy->reveal(); + $reporter = new Reporter($messenger, $loggerChannelFactory); + $this->assertInstanceOf(Reporter::class, $reporter); + } + + /** + * Test that the reporter object will handle an error correctly. + * + * @covers ::reportError + */ + public function testReportError() { + $messengerProphecy = $this->prophesize(MessengerInterface::class); + $messengerProphecy->addError('message')->shouldBeCalled(); + $messenger = $messengerProphecy->reveal(); + $loggerProphecy = $this->prophesize(LoggerInterface::class); + $loggerProphecy->error('message', Argument::cetera())->shouldBeCalled(); + $loggerProphecy->log(Argument::cetera())->shouldBeCalled(); + $logger = $loggerProphecy->reveal(); + $stringTranslationProphecy = $this->prophesize(TranslationInterface::class); + $stringTranslationProphecy->translateString(Argument::any())->will(function ($args) { + return $args[0]->getUntranslatedString(); + }); + $stringTranslationService = $stringTranslationProphecy->reveal(); + $containerProphecy = $this->prophesize(ContainerInterface::class); + $containerProphecy->get('string_translation')->willReturn($stringTranslationService); + $loggerChannelFactoryProphecy = $this->prophesize(LoggerChannelFactoryInterface::class); + $loggerChannelFactoryProphecy->get('va_gov_live_field_migration')->willReturn($logger); + $loggerChannelFactory = $loggerChannelFactoryProphecy->reveal(); + $containerProphecy->get('logger.factory')->willReturn($loggerChannelFactory); + $container = $containerProphecy->reveal(); + \Drupal::setContainer($container); + $reporter = new Reporter($messenger, $loggerChannelFactory); + $reporter->reportError('message', new \Exception('test')); + } + + /** + * Test that the reporter object will handle an info message correctly. + * + * @covers ::reportInfo + */ + public function testReportInfo() { + $messengerProphecy = $this->prophesize(MessengerInterface::class); + $messengerProphecy->addStatus('message')->shouldBeCalled(); + $messenger = $messengerProphecy->reveal(); + $loggerProphecy = $this->prophesize(LoggerInterface::class); + $loggerProphecy->info('message', Argument::cetera())->shouldBeCalled(); + $logger = $loggerProphecy->reveal(); + $loggerChannelFactoryProphecy = $this->prophesize(LoggerChannelFactoryInterface::class); + $loggerChannelFactoryProphecy->get('va_gov_live_field_migration')->willReturn($logger); + $loggerChannelFactory = $loggerChannelFactoryProphecy->reveal(); + $reporter = new Reporter($messenger, $loggerChannelFactory); + $reporter->reportInfo('message'); + } + +}