From cc23dbe43aab8c034528c84124bd181bc5c3e5de Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Thu, 16 Dec 2021 13:57:21 +0100 Subject: [PATCH] Add field:delete command (#4926) * Add field:delete command * Add validation for the field-name option * Validate whether field-name option is a valid field * Add tests. * Make entity type argument optional * Update tests and fix PHPCS Co-authored-by: Moshe Weitzman --- ...Trait.php => EntityTypeBundleAskTrait.php} | 20 ++- ...hp => EntityTypeBundleValidationTrait.php} | 2 +- .../Commands/core/FieldCreateCommands.php | 78 +-------- .../Commands/core/FieldDeleteCommands.php | 160 ++++++++++++++++++ .../Commands/core/FieldInfoCommands.php | 7 +- src/Drupal/Commands/core/drush.services.yml | 7 + tests/functional/FieldCreateTest.php | 20 ++- 7 files changed, 213 insertions(+), 81 deletions(-) rename src/Drupal/Commands/core/{AskBundleTrait.php => EntityTypeBundleAskTrait.php} (68%) rename src/Drupal/Commands/core/{ValidateEntityTypeTrait.php => EntityTypeBundleValidationTrait.php} (97%) create mode 100644 src/Drupal/Commands/core/FieldDeleteCommands.php diff --git a/src/Drupal/Commands/core/AskBundleTrait.php b/src/Drupal/Commands/core/EntityTypeBundleAskTrait.php similarity index 68% rename from src/Drupal/Commands/core/AskBundleTrait.php rename to src/Drupal/Commands/core/EntityTypeBundleAskTrait.php index 1e692474fd..3a2700ada6 100644 --- a/src/Drupal/Commands/core/AskBundleTrait.php +++ b/src/Drupal/Commands/core/EntityTypeBundleAskTrait.php @@ -11,8 +11,26 @@ * @property EntityTypeBundleInfoInterface $entityTypeBundleInfo * @property EntityTypeManagerInterface $entityTypeManager */ -trait AskBundleTrait +trait EntityTypeBundleAskTrait { + protected function askEntityType(): ?string + { + $entityTypeDefinitions = $this->entityTypeManager->getDefinitions(); + $choices = []; + + foreach ($entityTypeDefinitions as $entityTypeDefinition) { + $choices[$entityTypeDefinition->id()] = $this->input->getOption('show-machine-names') + ? $entityTypeDefinition->id() + : $entityTypeDefinition->getLabel(); + } + + if (!$answer = $this->io()->choice('Entity type', $choices)) { + throw new \InvalidArgumentException(t('The entityType argument is required.')); + } + + return $answer; + } + protected function askBundle(): ?string { $entityTypeId = $this->input->getArgument('entityType'); diff --git a/src/Drupal/Commands/core/ValidateEntityTypeTrait.php b/src/Drupal/Commands/core/EntityTypeBundleValidationTrait.php similarity index 97% rename from src/Drupal/Commands/core/ValidateEntityTypeTrait.php rename to src/Drupal/Commands/core/EntityTypeBundleValidationTrait.php index 7c968efc30..d50c4b5b18 100644 --- a/src/Drupal/Commands/core/ValidateEntityTypeTrait.php +++ b/src/Drupal/Commands/core/EntityTypeBundleValidationTrait.php @@ -7,7 +7,7 @@ /** * @property EntityTypeManagerInterface $entityTypeManager */ -trait ValidateEntityTypeTrait +trait EntityTypeBundleValidationTrait { protected function validateEntityType(string $entityTypeId): void { diff --git a/src/Drupal/Commands/core/FieldCreateCommands.php b/src/Drupal/Commands/core/FieldCreateCommands.php index b9a441cd1c..38bad23c33 100644 --- a/src/Drupal/Commands/core/FieldCreateCommands.php +++ b/src/Drupal/Commands/core/FieldCreateCommands.php @@ -26,7 +26,9 @@ class FieldCreateCommands extends DrushCommands implements CustomEventAwareInterface { + use EntityTypeBundleAskTrait; use CustomEventAwareTrait; + use EntityTypeBundleValidationTrait; /** @var FieldTypePluginManagerInterface */ protected $fieldTypePluginManager; @@ -118,7 +120,7 @@ public function setContentTranslationManager(ContentTranslationManagerInterface * @see \Drupal\field_ui\Form\FieldConfigEditForm * @see \Drupal\field_ui\Form\FieldStorageConfigEditForm */ - public function create(string $entityType, ?string $bundle = null, array $options = [ + public function create(?string $entityType = null, ?string $bundle = null, array $options = [ 'field-name' => InputOption::VALUE_REQUIRED, 'field-label' => InputOption::VALUE_REQUIRED, 'field-description' => InputOption::VALUE_OPTIONAL, @@ -134,6 +136,7 @@ public function create(string $entityType, ?string $bundle = null, array $option 'existing' => false, ]): void { + $this->input->setArgument('entityType', $entityType = $entityType ?? $this->askEntityType()); $this->validateEntityType($entityType); $this->input->setArgument('bundle', $bundle = $bundle ?? $this->askBundle()); @@ -216,41 +219,6 @@ public function create(string $entityType, ?string $bundle = null, array $option $this->logResult($field); } - protected function validateEntityType(string $entityTypeId): void - { - if (!$this->entityTypeManager->hasDefinition($entityTypeId)) { - throw new \InvalidArgumentException( - t("Entity type with id ':entityType' does not exist.", [':entityType' => $entityTypeId]) - ); - } - } - - protected function validateBundle(string $entityTypeId, string $bundle): void - { - if (!$entityTypeDefinition = $this->entityTypeManager->getDefinition($entityTypeId)) { - return; - } - - $bundleEntityType = $entityTypeDefinition->getBundleEntityType(); - - if ($bundleEntityType === null && $bundle === $entityTypeId) { - return; - } - - $bundleDefinition = $this->entityTypeManager - ->getStorage($bundleEntityType) - ->load($bundle); - - if (!$bundleDefinition) { - throw new \InvalidArgumentException( - t("Bundle ':bundle' does not exist on entity type with id ':entityType'.", [ - ':bundle' => $bundle, - ':entityType' => $entityTypeId, - ]) - ); - } - } - protected function askExistingFieldName(): ?string { $entityType = $this->input->getArgument('entityType'); @@ -361,44 +329,6 @@ protected function askTranslatable(): bool return $this->io()->confirm('Translatable', false); } - protected function askBundle(): ?string - { - $entityTypeId = $this->input->getArgument('entityType'); - $entityTypeDefinition = $this->entityTypeManager->getDefinition($entityTypeId); - $bundleEntityType = $entityTypeDefinition->getBundleEntityType(); - $bundleInfo = $this->entityTypeBundleInfo->getBundleInfo($entityTypeId); - $choices = []; - - // If the entity type has one fixed bundle (eg. user), return it. - if ($bundleEntityType === null && count($bundleInfo) === 1) { - return key($bundleInfo); - } - - // If the entity type doesn't have bundles, return null - // TODO Find an example - if ($bundleEntityType === null && count($bundleInfo) === 0) { - return null; - } - - // If the entity type can have multiple bundles but it doesn't have any, throw an error - if ($bundleEntityType !== null && count($bundleInfo) === 0) { - throw new \InvalidArgumentException( - t("Entity type with id ':entityType' does not have any bundles.", [':entityType' => $entityTypeId]) - ); - } - - foreach ($bundleInfo as $bundle => $data) { - $label = $this->input->getOption('show-machine-names') ? $bundle : $data['label']; - $choices[$bundle] = $label; - } - - if (!$this->input->isInteractive() || !$answer = $this->io()->choice('Bundle', $choices)) { - throw new \InvalidArgumentException(t('The bundle argument is required.')); - } - - return $answer; - } - protected function askCardinality(): int { $fieldType = $this->input->getOption('field-type'); diff --git a/src/Drupal/Commands/core/FieldDeleteCommands.php b/src/Drupal/Commands/core/FieldDeleteCommands.php new file mode 100644 index 0000000000..888f74a82b --- /dev/null +++ b/src/Drupal/Commands/core/FieldDeleteCommands.php @@ -0,0 +1,160 @@ +entityTypeManager = $entityTypeManager; + $this->entityTypeBundleInfo = $entityTypeBundleInfo; + } + + /** + * Delete a field + * + * @command field:delete + * @aliases field-delete,fd + * + * @param string $entityType + * The machine name of the entity type + * @param string $bundle + * The machine name of the bundle + * + * @option field-name + * The machine name of the field + * + * @option show-machine-names + * Show machine names instead of labels in option lists. + * + * @usage drush field:delete + * Delete a field by answering the prompts. + * @usage drush field-delete taxonomy_term tag + * Delete a field and fill in the remaining information through prompts. + * @usage drush field-delete taxonomy_term tag --field-name=field_tag_label + * Delete a field in a non-interactive way. + * + * @version 11.0 + * @see \Drupal\field_ui\Form\FieldConfigDeleteForm + */ + public function delete(?string $entityType = null, ?string $bundle = null, array $options = [ + 'field-name' => InputOption::VALUE_REQUIRED, + 'show-machine-names' => InputOption::VALUE_OPTIONAL, + ]): void + { + $this->input->setArgument('entityType', $entityType = $entityType ?? $this->askEntityType()); + $this->validateEntityType($entityType); + + $this->input->setArgument('bundle', $bundle = $bundle ?? $this->askBundle()); + $this->validateBundle($entityType, $bundle); + + $fieldName = $this->input->getOption('field-name') ?? $this->askExisting($entityType, $bundle); + $this->input->setOption('field-name', $fieldName); + + if ($fieldName === '') { + throw new \InvalidArgumentException(dt('The %optionName option is required.', [ + '%optionName' => 'field-name', + ])); + } + + /** @var FieldConfig[] $results */ + $results = $this->entityTypeManager + ->getStorage('field_config') + ->loadByProperties([ + 'field_name' => $fieldName, + 'entity_type' => $entityType, + 'bundle' => $bundle, + ]); + + if ($results === []) { + throw new \InvalidArgumentException( + t("Field with name ':fieldName' does not exist on bundle ':bundle'.", [ + ':fieldName' => $fieldName, + ':bundle' => $bundle, + ]) + ); + } + + $this->deleteFieldConfig(reset($results)); + + // Fields are purged on cron. However field module prevents disabling modules + // when field types they provided are used in a field until it is fully + // purged. In the case that a field has minimal or no content, a single call + // to field_purge_batch() will remove it from the system. Call this with a + // low batch limit to avoid administrators having to wait for cron runs when + // removing fields that meet this criteria. + field_purge_batch(10); + } + + protected function askExisting(string $entityType, string $bundle): string + { + $choices = []; + /** @var FieldConfigInterface[] $fieldConfigs */ + $fieldConfigs = $this->entityTypeManager + ->getStorage('field_config') + ->loadByProperties([ + 'entity_type' => $entityType, + 'bundle' => $bundle, + ]); + + foreach ($fieldConfigs as $fieldConfig) { + $label = $this->input->getOption('show-machine-names') + ? $fieldConfig->get('field_name') + : $fieldConfig->get('label'); + + $choices[$fieldConfig->get('field_name')] = $label; + } + + if ($choices === []) { + throw new \InvalidArgumentException( + t("Bundle ':bundle' has no fields.", [ + ':bundle' => $bundle, + ]) + ); + } + + return $this->io()->choice('Choose a field to delete', $choices); + } + + protected function deleteFieldConfig(FieldConfigInterface $fieldConfig): void + { + $fieldStorage = $fieldConfig->getFieldStorageDefinition(); + $bundles = $this->entityTypeBundleInfo->getBundleInfo($fieldConfig->getTargetEntityTypeId()); + $bundleLabel = $bundles[$fieldConfig->getTargetBundle()]['label']; + + if ($fieldStorage && !$fieldStorage->isLocked()) { + $fieldConfig->delete(); + + // If there is only one bundle left for this field storage, it will be + // deleted too, notify the user about dependencies. + if (count($fieldStorage->getBundles()) <= 1) { + $fieldStorage->delete(); + } + + $message = 'The field :field has been deleted from the :type bundle.'; + } else { + $message = 'There was a problem removing the :field from the :type content type.'; + } + + $this->logger()->success( + t($message, [':field' => $fieldConfig->label(), ':type' => $bundleLabel]) + ); + } +} diff --git a/src/Drupal/Commands/core/FieldInfoCommands.php b/src/Drupal/Commands/core/FieldInfoCommands.php index 8c962d23e0..5560ffcea7 100644 --- a/src/Drupal/Commands/core/FieldInfoCommands.php +++ b/src/Drupal/Commands/core/FieldInfoCommands.php @@ -10,9 +10,9 @@ class FieldInfoCommands extends DrushCommands { - use AskBundleTrait; + use EntityTypeBundleAskTrait; + use EntityTypeBundleValidationTrait; use FieldDefinitionRowsOfFieldsTrait; - use ValidateEntityTypeTrait; /** @var EntityTypeManagerInterface */ protected $entityTypeManager; @@ -66,10 +66,11 @@ public function __construct( * * @version 11.0 */ - public function info(string $entityType, ?string $bundle = null, array $options = [ + public function info(?string $entityType = null, ?string $bundle = null, array $options = [ 'format' => 'table', ]): RowsOfFields { + $this->input->setArgument('entityType', $entityType = $entityType ?? $this->askEntityType()); $this->validateEntityType($entityType); $this->input->setArgument('bundle', $bundle = $bundle ?? $this->askBundle()); diff --git a/src/Drupal/Commands/core/drush.services.yml b/src/Drupal/Commands/core/drush.services.yml index e975013360..2aaac6c711 100644 --- a/src/Drupal/Commands/core/drush.services.yml +++ b/src/Drupal/Commands/core/drush.services.yml @@ -42,6 +42,13 @@ services: - '@entity_type.bundle.info' tags: - { name: drush.command } + field.delete.commands: + class: \Drush\Drupal\Commands\core\FieldDeleteCommands + arguments: + - '@entity_type.manager' + - '@entity_type.bundle.info' + tags: + - { name: drush.command } link.hooks: class: \Drush\Drupal\Commands\core\LinkHooks arguments: diff --git a/tests/functional/FieldCreateTest.php b/tests/functional/FieldCreateTest.php index ca676305af..07e6af8975 100644 --- a/tests/functional/FieldCreateTest.php +++ b/tests/functional/FieldCreateTest.php @@ -27,11 +27,11 @@ public function testFieldCreate() { // Arguments. $this->drush('field:create', [], [], null, null, self::EXIT_ERROR); - $this->assertStringContainsString('Not enough arguments (missing: "entityType")', $this->getErrorOutputRaw()); + $this->assertStringContainsString('The entityType argument is required', $this->getErrorOutputRaw()); $this->drush('field:create', ['foo'], [], null, null, self::EXIT_ERROR); $this->assertStringContainsString('Entity type with id \'foo\' does not exist.', $this->getErrorOutputRaw()); $this->drush('field:create', ['user'], [], null, null, self::EXIT_ERROR); - $this->assertStringNotContainsString('The bundle argument is required.', $this->getErrorOutputRaw()); + $this->assertStringContainsString('The bundle argument is required.', $this->getErrorOutputRaw()); $this->drush('field:create', ['user', 'user'], [], null, null, self::EXIT_ERROR); $this->assertStringNotContainsString('bundle', $this->getErrorOutputRaw()); @@ -76,4 +76,20 @@ public function testFieldInfo() $this->assertFalse($json['translatable']); $this->assertArrayHasKey('beta', $json['target_bundles']); } + + public function testFieldDelete() + { + $this->drush('field:create', ['unish_article', 'alpha'], ['field-label' => 'Test', 'field-name' => 'field_test5', 'field-description' => 'baz', 'field-type' => 'entity_reference', 'is-required' => true, 'field-widget' => 'entity_reference_autocomplete', 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, 'target-type' => 'unish_article', 'target-bundle' => 'beta']); + $this->assertStringContainsString("Successfully created field 'field_test5' on unish_article type with bundle 'alpha'", $this->getSimplifiedErrorOutput()); + + $this->drush('field:delete', ['unish_article'], [], null, null, self::EXIT_ERROR); + $this->assertStringContainsString('The bundle argument is required.', $this->getErrorOutputRaw()); + $this->drush('field:delete', ['unish_article', 'alpha'], [], null, null, self::EXIT_ERROR); + $this->assertStringContainsString('The field-name option is required.', $this->getErrorOutputRaw()); + + $this->drush('field:delete', ['unish_article', 'alpha'], ['field-name' => 'field_testZZZZZ'], null, null, self::EXIT_ERROR); + $this->assertStringContainsString("Field with name 'field_testZZZZZ' does not exist on bundle 'alpha'", $this->getErrorOutputRaw()); + $this->drush('field:delete', ['unish_article', 'alpha'], ['field-name' => 'field_test5']); + $this->assertStringContainsString(" The field Test has been deleted from the Alpha bundle.", $this->getErrorOutputRaw()); + } }