Skip to content

Commit

Permalink
Add field:delete command (#4926)
Browse files Browse the repository at this point in the history
* 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 <weitzman@tejasa.com>
  • Loading branch information
DieterHolvoet and weitzman committed Dec 16, 2021
1 parent 66e7494 commit cc23dbe
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 81 deletions.
Expand Up @@ -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');
Expand Down
Expand Up @@ -7,7 +7,7 @@
/**
* @property EntityTypeManagerInterface $entityTypeManager
*/
trait ValidateEntityTypeTrait
trait EntityTypeBundleValidationTrait
{
protected function validateEntityType(string $entityTypeId): void
{
Expand Down
78 changes: 4 additions & 74 deletions src/Drupal/Commands/core/FieldCreateCommands.php
Expand Up @@ -26,7 +26,9 @@

class FieldCreateCommands extends DrushCommands implements CustomEventAwareInterface
{
use EntityTypeBundleAskTrait;
use CustomEventAwareTrait;
use EntityTypeBundleValidationTrait;

/** @var FieldTypePluginManagerInterface */
protected $fieldTypePluginManager;
Expand Down Expand Up @@ -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,
Expand All @@ -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());
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down
160 changes: 160 additions & 0 deletions src/Drupal/Commands/core/FieldDeleteCommands.php
@@ -0,0 +1,160 @@
<?php

namespace Drush\Drupal\Commands\core;

use Drupal\Core\Entity\EntityTypeBundleInfo;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\FieldConfigInterface;
use Drush\Commands\DrushCommands;
use Symfony\Component\Console\Input\InputOption;

class FieldDeleteCommands extends DrushCommands
{
use EntityTypeBundleAskTrait;
use EntityTypeBundleValidationTrait;

/** @var EntityTypeManagerInterface */
protected $entityTypeManager;
/** @var EntityTypeBundleInfo */
protected $entityTypeBundleInfo;

public function __construct(
EntityTypeManagerInterface $entityTypeManager,
EntityTypeBundleInfo $entityTypeBundleInfo
) {
$this->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])
);
}
}
7 changes: 4 additions & 3 deletions src/Drupal/Commands/core/FieldInfoCommands.php
Expand Up @@ -10,9 +10,9 @@

class FieldInfoCommands extends DrushCommands
{
use AskBundleTrait;
use EntityTypeBundleAskTrait;
use EntityTypeBundleValidationTrait;
use FieldDefinitionRowsOfFieldsTrait;
use ValidateEntityTypeTrait;

/** @var EntityTypeManagerInterface */
protected $entityTypeManager;
Expand Down Expand Up @@ -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());
Expand Down
7 changes: 7 additions & 0 deletions src/Drupal/Commands/core/drush.services.yml
Expand Up @@ -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:
Expand Down
20 changes: 18 additions & 2 deletions tests/functional/FieldCreateTest.php
Expand Up @@ -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());

Expand Down Expand Up @@ -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());
}
}

0 comments on commit cc23dbe

Please sign in to comment.