From b54631a2cde21fbe45ebaa847447e6fb000a05c2 Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Tue, 17 Apr 2018 17:46:30 +0200 Subject: [PATCH 01/16] Add field:create command --- .../Commands/core/FieldCreateCommands.php | 646 ++++++++++++++++++ src/Drupal/Commands/core/drush.services.yml | 12 + 2 files changed, 658 insertions(+) create mode 100644 src/Drupal/Commands/core/FieldCreateCommands.php diff --git a/src/Drupal/Commands/core/FieldCreateCommands.php b/src/Drupal/Commands/core/FieldCreateCommands.php new file mode 100644 index 0000000000..4f24c40f29 --- /dev/null +++ b/src/Drupal/Commands/core/FieldCreateCommands.php @@ -0,0 +1,646 @@ +fieldTypePluginManager = $fieldTypePluginManager; + $this->widgetPluginManager = $widgetPluginManager; + $this->selectionPluginManager = $selectionPluginManager; + $this->entityTypeManager = $entityTypeManager; + $this->entityTypeBundleInfo = $entityTypeBundleInfo; + $this->moduleHandler = $moduleHandler; + $this->entityFieldManager = $entityFieldManager; + } + + /** + * Create a new field + * + * @command field:create + * @aliases field-create,fc + * + * @param string $entityType + * Name of bundle to attach fields to. + * @param string $bundle + * Type of entity (e.g. node, user, comment). + * + * @option field-name + * @option field-label + * @option field-type + * @option field-widget + * @option is-required + * @option cardinality + * @option target-type + * Only necessary for entity reference fields. + * + * @option existing + * Re-use an existing field. + * @option show-machine-names + * Show machine names instead of labels in option lists. + * + * @usage drush field:create + * Create a field by answering the prompts. + * @usage drush field-create taxonomy_term tag + * Create a field and fill in the remaining information through prompts. + * @usage drush field-create taxonomy_term tag --field-name=field_tag_label --field-label=Label --field-type=string --field-widget=string_textfield --is-required=1 --cardinality=2 + * Create a field in a completely non-interactive way. + */ + public function create($entityType, $bundle, $options = [ + 'field-name' => InputOption::VALUE_REQUIRED, + 'field-label' => InputOption::VALUE_REQUIRED, + 'field-type' => InputOption::VALUE_REQUIRED, + 'field-widget' => InputOption::VALUE_REQUIRED, + 'is-required' => InputOption::VALUE_REQUIRED, + 'cardinality' => InputOption::VALUE_REQUIRED, + 'target-type' => InputOption::VALUE_OPTIONAL, + 'show-machine-names' => InputOption::VALUE_OPTIONAL, + 'existing' => false, + ]) + { + $fieldName = $this->input->getOption('field-name'); + $fieldLabel = $this->input->getOption('field-label'); + $fieldType = $this->input->getOption('field-type'); + $fieldWidget = $this->input->getOption('field-widget'); + $isRequired = $this->input->getOption('is-required'); + $cardinality = $this->input->getOption('cardinality'); + $targetType = $this->input->getOption('target-type'); + + if (!$options['existing']) { + $this->createFieldStorage($fieldName, $fieldType, $entityType, $targetType, $cardinality); + } + + $field = $this->createField($fieldName, $fieldLabel, $entityType, $bundle, $isRequired); + $this->createFieldFormDisplay($fieldName, $fieldWidget, $entityType, $bundle); + $this->createFieldViewDisplay($fieldName, $entityType, $bundle); + + $this->logResult($field); + } + + /** + * @hook interact field:create + */ + public function interact(InputInterface $input, OutputInterface $output, AnnotationData $annotationData) + { + $entityType = $this->input->getArgument('entityType'); + $bundle = $this->input->getArgument('bundle'); + + if (empty($bundle) || !$this->entityTypeBundleExists($entityType, $bundle)) { + $this->input->setArgument('bundle', $this->askBundle()); + } + + if ($this->input->getOption('existing')) { + $this->input->setOption( + 'field-name', + $this->input->getOption('field-name') ?? $this->askExisting() + ); + $this->input->setOption( + 'field-label', + $this->input->getOption('field-label') ?? $this->askFieldLabel() + ); + $this->input->setOption( + 'is-required', + $this->input->getOption('is-required') ?? $this->askRequired() + ); + + /** @var \Drupal\Core\Entity\Entity\EntityFormDisplay $formDisplay */ + $formDisplay = $this->entityTypeManager + ->getStorage('entity_form_display') + ->load("$entityType.$bundle.default"); + + if (!$formDisplay || $this->input->getOption('field-widget')) { + return; + } + + $component = $formDisplay->getComponent($this->input->getOption('field-name')); + $this->input->setOption('field-widget', $component['type']); + } else { + $this->input->setOption( + 'field-label', + $this->input->getOption('field-label') ?? $this->askFieldLabel() + ); + $this->input->setOption( + 'field-name', + $this->input->getOption('field-name') ?? $this->askFieldName() + ); + $this->input->setOption( + 'field-type', + $this->input->getOption('field-type') ?? $this->askFieldType() + ); + $this->input->setOption( + 'field-widget', + $this->input->getOption('field-widget') ?? $this->askFieldWidget() + ); + $this->input->setOption( + 'is-required', + (bool) ($this->input->getOption('is-required') ?? $this->askRequired()) + ); + $this->input->setOption( + 'cardinality', + $this->input->getOption('cardinality') ?? $this->askCardinality() + ); + + if ( + $this->input->getOption('field-type') === 'entity_reference' + && !$this->input->getOption('target-type') + ) { + $this->input->setOption('target-type', $this->askReferencedEntityType()); + } + } + } + + /** + * @hook validate field:create + */ + public function validateEntityType(CommandData $commandData) + { + $entityType = $this->input->getArgument('entityType'); + + if (!$this->entityTypeManager->hasDefinition($entityType)) { + throw new \InvalidArgumentException( + t('Entity type with id \':entityType\' does not exist.', [':entityType' => $entityType]) + ); + } + } + + protected function askExisting() + { + $entityType = $this->input->getArgument('entityType'); + $bundle = $this->input->getArgument('bundle'); + $choices = $this->getExistingFieldStorageOptions($entityType, $bundle); + return $this->choice('Choose an existing field', $choices); + } + + protected function askBundle() + { + $entityType = $this->input->getArgument('entityType'); + $bundleInfo = $this->entityTypeBundleInfo->getBundleInfo($entityType); + $choices = []; + + foreach ($bundleInfo as $bundle => $data) { + $label = $this->input->getOption('show-machine-names') ? $bundle : $data['label']; + $choices[$bundle] = $label; + } + + return $this->choice('Bundle', $choices); + } + + protected function askFieldName() + { + $entityType = $this->input->getArgument('entityType'); + $fieldLabel = $this->input->getOption('field-label'); + $fieldName = null; + $machineName = null; + + if (!empty($fieldLabel)) { + $machineName = $this->generateFieldName($fieldLabel); + } + + while (!$fieldName) { + $answer = $this->io()->ask('Field name', $machineName); + + if (!preg_match('/^[_a-z]+[_a-z0-9]*$/', $answer)) { + $this->logger()->error('Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character.'); + continue; + } + + if (strlen($answer) > 32) { + $this->logger()->error('Field name must not be longer than 32 characters.'); + continue; + } + + if ($this->fieldStorageExists($answer, $entityType)) { + $this->logger()->error('A field with this name already exists.'); + continue; + } + + $fieldName = $answer; + } + + return $fieldName; + } + + protected function askFieldLabel() + { + return $this->io()->ask('Field label'); + } + + protected function askFieldType() + { + $definitions = $this->fieldTypePluginManager->getDefinitions(); + $choices = []; + + foreach ($definitions as $definition) { + $label = $this->input->getOption('show-machine-names') ? $definition['id'] : $definition['label']->render(); + $choices[$definition['id']] = $label; + } + + return $this->choice('Field type', $choices); + } + + protected function askFieldWidget() + { + $choices = []; + $fieldType = $this->input->getOption('field-type'); + $widgets = $this->widgetPluginManager->getOptions($fieldType); + + foreach ($widgets as $name => $label) { + $label = $this->input->getOption('show-machine-names') ? $name : $label->render(); + $choices[$name] = $label; + } + + return $this->choice('Field widget', $choices, false, 0); + } + + protected function askRequired() + { + return $this->io()->askQuestion(new ConfirmationQuestion('Required', false)); + } + + protected function askCardinality() + { + $fieldType = $this->input->getOption('field-type'); + $enforcedCardinality = $this->getEnforcedCardinality($fieldType); + + if (!is_null($enforcedCardinality)) { + return $enforcedCardinality; + } + + $choices = ['Limited', 'Unlimited']; + $cardinality = $this->choice( + 'Allowed number of values', + array_combine($choices, $choices), + false, + 0 + ); + + $limit = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED; + while ($cardinality === 'Limited' && $limit < 1) { + $limit = $this->io()->ask('Allowed number of values', 1); + } + + return (int) $limit; + } + + protected function askReferencedEntityType() + { + $definitions = $this->entityTypeManager->getDefinitions(); + $choices = []; + + /** @var \Drupal\Core\Config\Entity\ConfigEntityType $definition */ + foreach ($definitions as $name => $definition) { + $label = $this->input->getOption('show-machine-names') + ? $name + : sprintf('%s: %s', $definition->getGroupLabel()->render(), $definition->getLabel()); + $choices[$name] = $label; + } + + return $this->choice('Referenced entity type', $choices); + } + + protected function askReferencedBundles(FieldDefinitionInterface $fieldDefinition) + { + $choices = []; + $bundleInfo = $this->entityTypeBundleInfo->getBundleInfo( + $fieldDefinition->getFieldStorageDefinition()->getSetting('target_type') + ); + + if (empty($bundleInfo)) { + return null; + } + + foreach ($bundleInfo as $bundle => $info) { + $label = $this->input->getOption('show-machine-names') ? $bundle : $info['label']; + $choices[$bundle] = $label; + } + + $answers = $this->choice('Referenced bundles', $choices, true, 0); + + return [ + 'target_bundles' => array_combine($answers, $answers), + 'sort' => [ + 'field' => '_none', + 'direction' => 'ASC', + ], + 'auto_create' => false, + 'auto_create_bundle' => null, + ]; + } + + protected function createField(string $fieldName, $fieldLabel, string $entityType, string $bundle, bool $isRequired) + { + $values = [ + 'field_name' => $fieldName, + 'entity_type' => $entityType, + 'bundle' => $bundle, + 'translatable' => false, + 'required' => $isRequired, + ]; + + if (!empty($fieldLabel)) { + $values['label'] = $fieldLabel; + } + + /** @var FieldConfig $field */ + $field = $this->entityTypeManager + ->getStorage('field_config') + ->create($values); + + $field->save(); + + $fieldType = $this->getFieldType($fieldName, $entityType, $bundle); + if ($fieldType instanceof EntityReferenceItem && $handlerSettings = $this->askReferencedBundles($field)) { + $field->setSetting('handler_settings', $handlerSettings); + $field->save(); + } + + return $field; + } + + protected function createFieldStorage(string $fieldName, string $fieldType, string $entityType, $targetType, int $cardinality) + { + $values = [ + 'field_name' => $fieldName, + 'entity_type' => $entityType, + 'type' => $fieldType, + 'cardinality' => $cardinality, + // 'translatable' => false, + ]; + + if ($targetType) { + $values['settings']['target_type'] = $targetType; + } + + /** @var FieldStorageConfigInterface $fieldStorage */ + $fieldStorage = $this->entityTypeManager + ->getStorage('field_storage_config') + ->create($values); + + $fieldStorage->save(); + + return $fieldStorage; + } + + protected function createFieldFormDisplay(string $fieldName, $fieldWidget, string $entityType, string $bundle) + { + $values = []; + + if ($fieldWidget) { + $values['type'] = $fieldWidget; + } + + $storage = $this->entityTypeManager + ->getStorage('entity_form_display') + ->load("$entityType.$bundle.default"); + + if (empty($storage)) { + $this->logger()->info( + sprintf('Form display storage not found for %s type \'%s\', creating now.', $entityType, $bundle) + ); + + $storage = $this->createDisplayStorage('form', $entityType, $bundle); + } + + $storage->setComponent($fieldName, $values)->save(); + } + + protected function createFieldViewDisplay(string $fieldName, string $entityType, string $bundle) + { + $values = []; + + $storage = $this->entityTypeManager + ->getStorage('entity_view_display') + ->load("$entityType.$bundle.default"); + + if (empty($storage)) { + $this->logger()->info( + sprintf('View display storage not found for %s type \'%s\', creating now.', $entityType, $bundle) + ); + + $storage = $this->createDisplayStorage('view', $entityType, $bundle); + } + + $storage->setComponent($fieldName, $values)->save(); + } + + protected function createDisplayStorage(string $context, string $entityType, string $bundle) + { + $storageValues = [ + 'id' => "$entityType.$bundle.default", + 'targetEntityType' => $entityType, + 'bundle' => $bundle, + 'mode' => 'default', + 'status' => true, + ]; + + $storage = $this->entityTypeManager + ->getStorage(sprintf('entity_%s_display', $context)) + ->create($storageValues); + + $storage->save(); + + return $storage; + } + + protected function logResult(FieldConfig $field) + { + $this->logger()->success( + sprintf( + 'Successfully created field \'%s\' on %s type with bundle \'%s\'', + $field->get('field_name'), + $field->get('entity_type'), + $field->get('bundle') + ) + ); + + $routeName = "entity.field_config.{$field->get('entity_type')}_field_edit_form"; + $routeParams = [ + 'field_config' => $field->id(), + "{$field->get('entity_type')}_type" => $field->get('bundle'), + ]; + + if ($this->input->getArgument('entityType') === 'taxonomy_term') { + $routeParams['taxonomy_vocabulary'] = $field->get('bundle'); + } + + if ($this->moduleHandler->moduleExists('field_ui')) { + $this->logger()->success( + 'Further customisation can be done at the following url:' + . PHP_EOL + . Url::fromRoute($routeName, $routeParams) + ->setAbsolute(true) + ->toString() + ); + } + } + + protected function generateFieldName(string $source) + { + // Only lowercase alphanumeric characters and underscores + $machineName = preg_replace('/[^_a-z0-9]/i', '_', $source); + // Only lowercase letters and underscores as the first character + $machineName = preg_replace('/^[^_a-z]/i', '_', $machineName); + // Maximum one subsequent underscore + $machineName = preg_replace('/_+/', '_', $machineName); + // Only lowercase + $machineName = strtolower($machineName); + // Add the prefix + $machineName = sprintf('field_%s', $machineName); + // Maximum 32 characters + $machineName = substr($machineName, 0, 32); + + return $machineName; + } + + protected function fieldStorageExists(string $fieldName, string $entityType) + { + $fieldStorageDefinitions = $this->entityFieldManager->getFieldStorageDefinitions($entityType); + return isset($fieldStorageDefinitions[$fieldName]); + } + + protected function entityTypeBundleExists(string $entityType, string $bundleName) + { + return isset($this->entityTypeBundleInfo->getBundleInfo($entityType)[$bundleName]); + } + + protected function getExistingFieldStorageOptions(string $entityType, string $bundle) + { + $options = []; + + // Load the fieldStorages and build the list of options. + $fieldTypes = $this->fieldTypePluginManager->getDefinitions(); + + foreach ($this->entityFieldManager->getFieldStorageDefinitions($entityType) as $fieldName => $fieldStorage) { + // Do not show: + // - non-configurable field storages, + // - locked field storages, + // - field storages that should not be added via user interface, + // - field storages that already have a field in the bundle. + $fieldType = $fieldStorage->getType(); + $label = $this->input->getOption('show-machine-names') + ? $fieldTypes[$fieldType]['id'] + : $fieldTypes[$fieldType]['label']; + + if ( + $fieldStorage instanceof FieldStorageConfigInterface + && !$fieldStorage->isLocked() + && empty($fieldTypes[$fieldType]['no_ui']) + && !in_array($bundle, $fieldStorage->getBundles(), true) + ) { + $options[$fieldName] = sprintf('%s (%s)', $fieldName, $label); + } + } + + asort($options); + + return $options; + } + + /** + * Returns the cardinality enforced by the field type. + * + * Some field types choose to enforce a fixed cardinality. This method + * returns that cardinality or NULL if no cardinality has been enforced. + * + * @param string $entityType + * @return int|null + */ + protected function getEnforcedCardinality(string $entityType) + { + $definition = $this->fieldTypePluginManager->getDefinition($entityType); + return $definition['cardinality'] ?? null; + } + + /** + * @param string $fieldName + * @param string $entityType + * @param string $bundle + * @return \Drupal\Core\Field\FieldItemInterface + */ + protected function getFieldType(string $fieldName, string $entityType, string $bundle) + { + $ids = (object) [ + 'entity_type' => $entityType, + 'bundle' => $bundle, + 'entity_id' => null, + ]; + + $entity = _field_create_entity_from_ids($ids); + $items = $entity->get($fieldName); + $item = $items->first() ?: $items->appendItem(); + + return $item; + } + + /** + * @param string $question + * @param array $choices + * If an associative array is passed, the chosen *key* is returned. + * @param bool $multiSelect + * @param null $default + * @return mixed + */ + protected function choice($question, array $choices, $multiSelect = false, $default = null) + { + $choicesValues = array_values($choices); + $question = new ChoiceQuestion($question, $choicesValues, $default); + $question->setMultiselect($multiSelect); + $return = $this->io()->askQuestion($question); + + if ($multiSelect) { + return array_map( + function ($value) use ($choices) { + return array_search($value, $choices); + }, + $return + ); + } + + return array_search($return, $choices); + } +} diff --git a/src/Drupal/Commands/core/drush.services.yml b/src/Drupal/Commands/core/drush.services.yml index 6dca5b9d28..65c0d65867 100644 --- a/src/Drupal/Commands/core/drush.services.yml +++ b/src/Drupal/Commands/core/drush.services.yml @@ -21,6 +21,18 @@ services: arguments: ['@entity_type.manager'] tags: - { name: drush.command } + field.create.commands: + class: \Drush\Drupal\Commands\core\FieldCreateCommands + arguments: + - '@plugin.manager.field.field_type' + - '@plugin.manager.field.widget' + - '@plugin.manager.entity_reference_selection' + - '@entity_type.manager' + - '@entity_type.bundle.info' + - '@module_handler' + - '@entity_field.manager' + tags: + - { name: drush.command } image.commands: class: \Drush\Drupal\Commands\core\ImageCommands tags: From 70a3b9c97192c5b4dcc2afd6e3f74dd5b21c358e Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Tue, 17 Apr 2018 18:42:03 +0200 Subject: [PATCH 02/16] Code style --- src/Drupal/Commands/core/FieldCreateCommands.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Drupal/Commands/core/FieldCreateCommands.php b/src/Drupal/Commands/core/FieldCreateCommands.php index 4f24c40f29..54bb1daae6 100644 --- a/src/Drupal/Commands/core/FieldCreateCommands.php +++ b/src/Drupal/Commands/core/FieldCreateCommands.php @@ -185,8 +185,7 @@ public function interact(InputInterface $input, OutputInterface $output, Annotat $this->input->getOption('cardinality') ?? $this->askCardinality() ); - if ( - $this->input->getOption('field-type') === 'entity_reference' + if ($this->input->getOption('field-type') === 'entity_reference' && !$this->input->getOption('target-type') ) { $this->input->setOption('target-type', $this->askReferencedEntityType()); @@ -566,8 +565,7 @@ protected function getExistingFieldStorageOptions(string $entityType, string $bu ? $fieldTypes[$fieldType]['id'] : $fieldTypes[$fieldType]['label']; - if ( - $fieldStorage instanceof FieldStorageConfigInterface + if ($fieldStorage instanceof FieldStorageConfigInterface && !$fieldStorage->isLocked() && empty($fieldTypes[$fieldType]['no_ui']) && !in_array($bundle, $fieldStorage->getBundles(), true) From 8af446ee06e74e456a2173aeb8b348dcc6643e96 Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Thu, 7 Oct 2021 11:46:09 +0200 Subject: [PATCH 03/16] Refactor field:create command --- .../Commands/core/FieldCreateCommands.php | 618 +++++++++--------- src/Drupal/Commands/core/drush.services.yml | 24 +- 2 files changed, 339 insertions(+), 303 deletions(-) diff --git a/src/Drupal/Commands/core/FieldCreateCommands.php b/src/Drupal/Commands/core/FieldCreateCommands.php index 54bb1daae6..012eb56b76 100644 --- a/src/Drupal/Commands/core/FieldCreateCommands.php +++ b/src/Drupal/Commands/core/FieldCreateCommands.php @@ -2,53 +2,56 @@ namespace Drush\Drupal\Commands\core; -use Consolidation\AnnotatedCommand\AnnotationData; -use Consolidation\AnnotatedCommand\CommandData; -use Drupal\Core\Entity\EntityFieldManager; -use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager; -use Drupal\Core\Entity\EntityTypeBundleInfo; +use Consolidation\AnnotatedCommand\Events\CustomEventAwareInterface; +use Consolidation\AnnotatedCommand\Events\CustomEventAwareTrait; +use Drupal\content_translation\ContentTranslationManagerInterface; +use Drupal\Core\Entity\Display\EntityDisplayInterface; +use Drupal\Core\Entity\Display\EntityFormDisplayInterface; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Extension\ModuleHandler; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\Core\Field\FieldTypePluginManager; -use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; +use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\Field\WidgetPluginManager; use Drupal\Core\Url; -use Drupal\field\Entity\FieldConfig; +use Drupal\field\FieldConfigInterface; use Drupal\field\FieldStorageConfigInterface; use Drush\Commands\DrushCommands; -use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\ChoiceQuestion; -use Symfony\Component\Console\Question\ConfirmationQuestion; -class FieldCreateCommands extends DrushCommands +class FieldCreateCommands extends DrushCommands implements CustomEventAwareInterface { - /** @var FieldTypePluginManager */ + use CustomEventAwareTrait; + + /** @var FieldTypePluginManagerInterface */ protected $fieldTypePluginManager; /** @var WidgetPluginManager */ protected $widgetPluginManager; - /** @var SelectionPluginManager */ + /** @var SelectionPluginManagerInterface */ protected $selectionPluginManager; /** @var EntityTypeManagerInterface */ protected $entityTypeManager; - /** @var EntityTypeBundleInfo */ + /** @var EntityTypeBundleInfoInterface */ protected $entityTypeBundleInfo; - /** @var EntityFieldManager */ + /** @var EntityFieldManagerInterface */ protected $entityFieldManager; - /** @var ModuleHandler */ + /** @var ModuleHandlerInterface */ protected $moduleHandler; + /** @var ContentTranslationManagerInterface */ + protected $contentTranslationManager; public function __construct( - FieldTypePluginManager $fieldTypePluginManager, + FieldTypePluginManagerInterface $fieldTypePluginManager, WidgetPluginManager $widgetPluginManager, - SelectionPluginManager $selectionPluginManager, + SelectionPluginManagerInterface $selectionPluginManager, EntityTypeManagerInterface $entityTypeManager, - EntityTypeBundleInfo $entityTypeBundleInfo, - ModuleHandler $moduleHandler, - EntityFieldManager $entityFieldManager + EntityTypeBundleInfoInterface $entityTypeBundleInfo, + ModuleHandlerInterface $moduleHandler, + EntityFieldManagerInterface $entityFieldManager ) { $this->fieldTypePluginManager = $fieldTypePluginManager; $this->widgetPluginManager = $widgetPluginManager; @@ -59,25 +62,45 @@ public function __construct( $this->entityFieldManager = $entityFieldManager; } + public function setContentTranslationManager($manager): void + { + $this->contentTranslationManager = $manager; + } + /** * Create a new field * * @command field:create * @aliases field-create,fc * + * @validate-entity-type-argument entityType + * @validate-optional-bundle-argument entityType bundle + * * @param string $entityType - * Name of bundle to attach fields to. + * The machine name of the entity type * @param string $bundle - * Type of entity (e.g. node, user, comment). + * The machine name of the bundle * * @option field-name + * A unique machine-readable name containing letters, numbers, and underscores. * @option field-label + * The field label + * @option field-description + * Instructions to present to the user below this field on the editing form. * @option field-type + * The field type * @option field-widget + * The field widget * @option is-required + * Whether the field is required + * @option is-translatable + * Whether the field is translatable * @option cardinality + * The allowed number of values * @option target-type - * Only necessary for entity reference fields. + * The target entity type. Only necessary for entity reference fields. + * @option target-bundle + * The target bundle(s). Only necessary for entity reference fields. * * @option existing * Re-use an existing field. @@ -90,154 +113,130 @@ public function __construct( * Create a field and fill in the remaining information through prompts. * @usage drush field-create taxonomy_term tag --field-name=field_tag_label --field-label=Label --field-type=string --field-widget=string_textfield --is-required=1 --cardinality=2 * Create a field in a completely non-interactive way. + * + * @see \Drupal\field_ui\Form\FieldConfigEditForm + * @see \Drupal\field_ui\Form\FieldStorageConfigEditForm */ - public function create($entityType, $bundle, $options = [ + public function create(string $entityType, ?string $bundle = null, array $options = [ 'field-name' => InputOption::VALUE_REQUIRED, 'field-label' => InputOption::VALUE_REQUIRED, + 'field-description' => InputOption::VALUE_OPTIONAL, 'field-type' => InputOption::VALUE_REQUIRED, 'field-widget' => InputOption::VALUE_REQUIRED, - 'is-required' => InputOption::VALUE_REQUIRED, + 'is-required' => InputOption::VALUE_OPTIONAL, + 'is-translatable' => InputOption::VALUE_OPTIONAL, 'cardinality' => InputOption::VALUE_REQUIRED, 'target-type' => InputOption::VALUE_OPTIONAL, + 'target-bundle' => InputOption::VALUE_OPTIONAL, 'show-machine-names' => InputOption::VALUE_OPTIONAL, 'existing' => false, - ]) + ]): void { - $fieldName = $this->input->getOption('field-name'); - $fieldLabel = $this->input->getOption('field-label'); - $fieldType = $this->input->getOption('field-type'); - $fieldWidget = $this->input->getOption('field-widget'); - $isRequired = $this->input->getOption('is-required'); - $cardinality = $this->input->getOption('cardinality'); - $targetType = $this->input->getOption('target-type'); - - if (!$options['existing']) { - $this->createFieldStorage($fieldName, $fieldType, $entityType, $targetType, $cardinality); + if (!$this->entityTypeManager->hasDefinition($entityType)) { + throw new \InvalidArgumentException( + t('Entity type with id \':entityType\' does not exist.', [':entityType' => $entityType]) + ); } - $field = $this->createField($fieldName, $fieldLabel, $entityType, $bundle, $isRequired); - $this->createFieldFormDisplay($fieldName, $fieldWidget, $entityType, $bundle); - $this->createFieldViewDisplay($fieldName, $entityType, $bundle); - - $this->logResult($field); - } + $bundle = $this->ensureArgument('bundle', [$this, 'askBundle']); - /** - * @hook interact field:create - */ - public function interact(InputInterface $input, OutputInterface $output, AnnotationData $annotationData) - { - $entityType = $this->input->getArgument('entityType'); - $bundle = $this->input->getArgument('bundle'); - - if (empty($bundle) || !$this->entityTypeBundleExists($entityType, $bundle)) { - $this->input->setArgument('bundle', $this->askBundle()); + if (!$this->entityTypeBundleExists($entityType, $bundle)) { + throw new \InvalidArgumentException( + t('Bundle with id \':bundle\' does not exist on entity type \':entityType\'.', [ + ':bundle' => $bundle, + ':entityType' => $entityType, + ]) + ); } if ($this->input->getOption('existing')) { - $this->input->setOption( - 'field-name', - $this->input->getOption('field-name') ?? $this->askExisting() - ); - $this->input->setOption( - 'field-label', - $this->input->getOption('field-label') ?? $this->askFieldLabel() - ); - $this->input->setOption( - 'is-required', - $this->input->getOption('is-required') ?? $this->askRequired() - ); + $fieldName = $this->ensureOption('field-name', [$this, 'askExisting']); + + if (!$this->fieldStorageExists($fieldName, $entityType)) { + throw new \InvalidArgumentException( + t('Field storage with name \':fieldName\' does not yet exist. Call this command without the --existing option first.', [ + ':fieldName' => $fieldName, + ]) + ); + } - /** @var \Drupal\Core\Entity\Entity\EntityFormDisplay $formDisplay */ - $formDisplay = $this->entityTypeManager - ->getStorage('entity_form_display') - ->load("$entityType.$bundle.default"); + $fieldStorage = $this->entityFieldManager->getFieldStorageDefinitions($entityType)[$fieldName]; - if (!$formDisplay || $this->input->getOption('field-widget')) { - return; + if ($this->fieldExists($fieldName, $entityType, $bundle)) { + throw new \InvalidArgumentException( + t('Field with name \':fieldName\' already exists on bundle \':bundle\'.', [ + ':fieldName' => $fieldName, + ':bundle' => $bundle, + ]) + ); } - $component = $formDisplay->getComponent($this->input->getOption('field-name')); - $this->input->setOption('field-widget', $component['type']); + $this->input->setOption('field-type', $fieldStorage->getType()); + $this->input->setOption('target-type', $fieldStorage->getSetting('target_type')); + + $this->ensureOption('field-label', [$this, 'askFieldLabel']); + $this->ensureOption('field-description', [$this, 'askFieldDescription']); + $this->ensureOption('field-widget', [$this, 'askFieldWidget']); + $this->ensureOption('is-required', [$this, 'askRequired']); + $this->ensureOption('is-translatable', [$this, 'askTranslatable']); } else { - $this->input->setOption( - 'field-label', - $this->input->getOption('field-label') ?? $this->askFieldLabel() - ); - $this->input->setOption( - 'field-name', - $this->input->getOption('field-name') ?? $this->askFieldName() - ); - $this->input->setOption( - 'field-type', - $this->input->getOption('field-type') ?? $this->askFieldType() - ); - $this->input->setOption( - 'field-widget', - $this->input->getOption('field-widget') ?? $this->askFieldWidget() - ); - $this->input->setOption( - 'is-required', - (bool) ($this->input->getOption('is-required') ?? $this->askRequired()) - ); - $this->input->setOption( - 'cardinality', - $this->input->getOption('cardinality') ?? $this->askCardinality() - ); + $this->ensureOption('field-label', [$this, 'askFieldLabel']); + $fieldName = $this->ensureOption('field-name', [$this, 'askFieldName']); + + if ($this->fieldStorageExists($fieldName, $entityType)) { + throw new \InvalidArgumentException( + t('Field storage with name \':fieldName\' already exists. Call this command with the --existing option to add an existing field to a bundle.', [ + ':fieldName' => $fieldName, + ]) + ); + } - if ($this->input->getOption('field-type') === 'entity_reference' - && !$this->input->getOption('target-type') - ) { - $this->input->setOption('target-type', $this->askReferencedEntityType()); + $this->ensureOption('field-description', [$this, 'askFieldDescription']); + $this->ensureOption('field-type', [$this, 'askFieldType']); + $this->ensureOption('field-widget', [$this, 'askFieldWidget']); + $this->ensureOption('is-required', [$this, 'askRequired']); + $this->ensureOption('is-translatable', [$this, 'askTranslatable']); + $this->ensureOption('cardinality', [$this, 'askCardinality']); + + if ($this->input->getOption('field-type') === 'entity_reference') { + $this->ensureOption('target-type', [$this, 'askReferencedEntityType']); } - } - } - /** - * @hook validate field:create - */ - public function validateEntityType(CommandData $commandData) - { - $entityType = $this->input->getArgument('entityType'); + $this->createFieldStorage(); + } - if (!$this->entityTypeManager->hasDefinition($entityType)) { - throw new \InvalidArgumentException( - t('Entity type with id \':entityType\' does not exist.', [':entityType' => $entityType]) - ); + // Command files may set additional options as desired. + $handlers = $this->getCustomEventHandlers('field-create-set-options'); + foreach ($handlers as $handler) { + $handler($this->input); } + + $field = $this->createField(); + $this->createFieldDisplay('form'); + $this->createFieldDisplay('view'); + + $this->logResult($field); } - protected function askExisting() + protected function askExisting(): string { $entityType = $this->input->getArgument('entityType'); $bundle = $this->input->getArgument('bundle'); $choices = $this->getExistingFieldStorageOptions($entityType, $bundle); - return $this->choice('Choose an existing field', $choices); - } - - protected function askBundle() - { - $entityType = $this->input->getArgument('entityType'); - $bundleInfo = $this->entityTypeBundleInfo->getBundleInfo($entityType); - $choices = []; - - foreach ($bundleInfo as $bundle => $data) { - $label = $this->input->getOption('show-machine-names') ? $bundle : $data['label']; - $choices[$bundle] = $label; - } - return $this->choice('Bundle', $choices); + return $this->io()->choice('Choose an existing field', $choices); } - protected function askFieldName() + protected function askFieldName(): string { $entityType = $this->input->getArgument('entityType'); + $bundle = $this->input->getArgument('bundle'); $fieldLabel = $this->input->getOption('field-label'); $fieldName = null; $machineName = null; if (!empty($fieldLabel)) { - $machineName = $this->generateFieldName($fieldLabel); + $machineName = $this->generateFieldName($fieldLabel, $bundle); } while (!$fieldName) { @@ -264,12 +263,17 @@ protected function askFieldName() return $fieldName; } - protected function askFieldLabel() + protected function askFieldLabel(): string { return $this->io()->ask('Field label'); } - protected function askFieldType() + protected function askFieldDescription(): ?string + { + return $this->optionalAsk('Field description'); + } + + protected function askFieldType(): string { $definitions = $this->fieldTypePluginManager->getDefinitions(); $choices = []; @@ -279,11 +283,21 @@ protected function askFieldType() $choices[$definition['id']] = $label; } - return $this->choice('Field type', $choices); + return $this->io()->choice('Field type', $choices); } - protected function askFieldWidget() + protected function askFieldWidget(): string { + $formDisplay = $this->getEntityDisplay('form'); + + if ($formDisplay instanceof EntityFormDisplayInterface) { + $component = $formDisplay->getComponent($this->input->getOption('field-name')); + + if (isset($component['type'])) { + return $component['type']; + } + } + $choices = []; $fieldType = $this->input->getOption('field-type'); $widgets = $this->widgetPluginManager->getOptions($fieldType); @@ -293,45 +307,83 @@ protected function askFieldWidget() $choices[$name] = $label; } - return $this->choice('Field widget', $choices, false, 0); + return $this->io()->choice('Field widget', $choices, 0); + } + + protected function askRequired(): bool + { + return $this->io()->confirm('Required', false); } - protected function askRequired() + protected function askTranslatable(): bool { - return $this->io()->askQuestion(new ConfirmationQuestion('Required', false)); + if (!$this->hasContentTranslation()) { + return false; + } + + return $this->io()->confirm('Translatable', false); } - protected function askCardinality() + protected function askBundle(): ?string + { + $entityTypeId = $this->input->getArgument('entityType'); + $entityTypeDefinition = $this->entityTypeManager->getDefinition($entityTypeId); + $bundleEntityType = $entityTypeDefinition->getBundleEntityType(); + $bundleInfo = $this->entityTypeBundleInfo->getBundleInfo($entityTypeId); + $choices = []; + + if (empty($bundleInfo)) { + if ($bundleEntityType) { + throw new \InvalidArgumentException( + t('Entity type with id \':entityType\' does not have any bundles.', [':entityType' => $entityTypeId]) + ); + } + + return null; + } + + foreach ($bundleInfo as $bundle => $data) { + $label = $this->input->getOption('show-machine-names') ? $bundle : $data['label']; + $choices[$bundle] = $label; + } + + if (!$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'); - $enforcedCardinality = $this->getEnforcedCardinality($fieldType); + $definition = $this->fieldTypePluginManager->getDefinition($fieldType); - if (!is_null($enforcedCardinality)) { - return $enforcedCardinality; + // Some field types choose to enforce a fixed cardinality. + if (isset($definition['cardinality'])) { + return $definition['cardinality']; } $choices = ['Limited', 'Unlimited']; - $cardinality = $this->choice( + $cardinality = $this->io()->choice( 'Allowed number of values', array_combine($choices, $choices), - false, 0 ); $limit = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED; while ($cardinality === 'Limited' && $limit < 1) { - $limit = $this->io()->ask('Allowed number of values', 1); + $limit = (int) $this->io()->ask('Allowed number of values', 1); } - return (int) $limit; + return $limit; } - protected function askReferencedEntityType() + protected function askReferencedEntityType(): string { $definitions = $this->entityTypeManager->getDefinitions(); $choices = []; - /** @var \Drupal\Core\Config\Entity\ConfigEntityType $definition */ foreach ($definitions as $name => $definition) { $label = $this->input->getOption('show-machine-names') ? $name @@ -339,10 +391,10 @@ protected function askReferencedEntityType() $choices[$name] = $label; } - return $this->choice('Referenced entity type', $choices); + return $this->io()->choice('Referenced entity type', $choices); } - protected function askReferencedBundles(FieldDefinitionInterface $fieldDefinition) + protected function askReferencedBundles(FieldDefinitionInterface $fieldDefinition): array { $choices = []; $bundleInfo = $this->entityTypeBundleInfo->getBundleInfo( @@ -350,7 +402,7 @@ protected function askReferencedBundles(FieldDefinitionInterface $fieldDefinitio ); if (empty($bundleInfo)) { - return null; + return []; } foreach ($bundleInfo as $bundle => $info) { @@ -358,63 +410,82 @@ protected function askReferencedBundles(FieldDefinitionInterface $fieldDefinitio $choices[$bundle] = $label; } - $answers = $this->choice('Referenced bundles', $choices, true, 0); - - return [ - 'target_bundles' => array_combine($answers, $answers), - 'sort' => [ - 'field' => '_none', - 'direction' => 'ASC', - ], - 'auto_create' => false, - 'auto_create_bundle' => null, - ]; + return $this->io()->choice('Referenced bundles', $choices, 0); } - protected function createField(string $fieldName, $fieldLabel, string $entityType, string $bundle, bool $isRequired) + protected function createField(): FieldConfigInterface { $values = [ - 'field_name' => $fieldName, - 'entity_type' => $entityType, - 'bundle' => $bundle, - 'translatable' => false, - 'required' => $isRequired, + 'field_name' => $this->input->getOption('field-name'), + 'entity_type' => $this->input->getArgument('entityType'), + 'bundle' => $this->input->getArgument('bundle'), + 'translatable' => $this->input->getOption('is-translatable'), + 'required' => $this->input->getOption('is-required'), + 'field_type' => $this->input->getOption('field-type'), + 'description' => $this->input->getOption('field-description') ?? '', + 'label' => $this->input->getOption('field-label'), ]; - if (!empty($fieldLabel)) { - $values['label'] = $fieldLabel; + // Command files may customize $values as desired. + $handlers = $this->getCustomEventHandlers('field-create-field-config'); + foreach ($handlers as $handler) { + $values = $handler($values, $this->input); } - /** @var FieldConfig $field */ $field = $this->entityTypeManager ->getStorage('field_config') ->create($values); - $field->save(); + if ($this->input->getOption('field-type') === 'entity_reference') { + $targetType = $this->input->getOption('target-type'); + $targetTypeDefinition = $this->entityTypeManager->getDefinition($targetType); + // For the 'target_bundles' setting, a NULL value is equivalent to "allow + // entities from any bundle to be referenced" and an empty array value is + // equivalent to "no entities from any bundle can be referenced". + $targetBundles = null; + + if ($targetTypeDefinition->hasKey('bundle')) { + if ($referencedBundle = $this->input->getOption('target-bundle')) { + $referencedBundles = [$referencedBundle]; + } else { + $referencedBundles = $this->askReferencedBundles($field); + } + + if (!empty($referencedBundles)) { + $targetBundles = array_combine($referencedBundles, $referencedBundles); + } + } - $fieldType = $this->getFieldType($fieldName, $entityType, $bundle); - if ($fieldType instanceof EntityReferenceItem && $handlerSettings = $this->askReferencedBundles($field)) { - $field->setSetting('handler_settings', $handlerSettings); - $field->save(); + $settings = $field->getSetting('handler_settings') ?? []; + $settings['target_bundles'] = $targetBundles; + $field->setSetting('handler_settings', $settings); } + $field->save(); + return $field; } - protected function createFieldStorage(string $fieldName, string $fieldType, string $entityType, $targetType, int $cardinality) + protected function createFieldStorage(): FieldStorageConfigInterface { $values = [ - 'field_name' => $fieldName, - 'entity_type' => $entityType, - 'type' => $fieldType, - 'cardinality' => $cardinality, - // 'translatable' => false, + 'field_name' => $this->input->getOption('field-name'), + 'entity_type' => $this->input->getArgument('entityType'), + 'type' => $this->input->getOption('field-type'), + 'cardinality' => $this->input->getOption('cardinality'), + 'translatable' => true, ]; - if ($targetType) { + if ($targetType = $this->input->getOption('target-type')) { $values['settings']['target_type'] = $targetType; } + // Command files may customize $values as desired. + $handlers = $this->getCustomEventHandlers('field-create-field-storage'); + foreach ($handlers as $handler) { + $handler($values); + } + /** @var FieldStorageConfigInterface $fieldStorage */ $fieldStorage = $this->entityTypeManager ->getStorage('field_storage_config') @@ -425,68 +496,58 @@ protected function createFieldStorage(string $fieldName, string $fieldType, stri return $fieldStorage; } - protected function createFieldFormDisplay(string $fieldName, $fieldWidget, string $entityType, string $bundle) + protected function createFieldDisplay(string $context): void { + $entityType = $this->input->getArgument('entityType'); + $bundle = $this->input->getArgument('bundle'); + $fieldName = $this->input->getOption('field-name'); + $fieldWidget = $this->input->getOption('field-widget'); $values = []; - if ($fieldWidget) { + if ($fieldWidget && $context === 'form') { $values['type'] = $fieldWidget; } - $storage = $this->entityTypeManager - ->getStorage('entity_form_display') - ->load("$entityType.$bundle.default"); - - if (empty($storage)) { - $this->logger()->info( - sprintf('Form display storage not found for %s type \'%s\', creating now.', $entityType, $bundle) - ); - - $storage = $this->createDisplayStorage('form', $entityType, $bundle); + // Command files may customize $values as desired. + $handlers = $this->getCustomEventHandlers("field-create-{$context}-display"); + foreach ($handlers as $handler) { + $handler($values); } - $storage->setComponent($fieldName, $values)->save(); - } - - protected function createFieldViewDisplay(string $fieldName, string $entityType, string $bundle) - { - $values = []; - - $storage = $this->entityTypeManager - ->getStorage('entity_view_display') - ->load("$entityType.$bundle.default"); + $storage = $this->getEntityDisplay($context); - if (empty($storage)) { + if (!$storage instanceof EntityDisplayInterface) { $this->logger()->info( - sprintf('View display storage not found for %s type \'%s\', creating now.', $entityType, $bundle) + sprintf('\'%s\' display storage not found for %s type \'%s\', creating now.', $context, $entityType, $bundle) ); - $storage = $this->createDisplayStorage('view', $entityType, $bundle); + $storage = $this->entityTypeManager + ->getStorage(sprintf('entity_%s_display', $context)) + ->create([ + 'id' => "$entityType.$bundle.default", + 'targetEntityType' => $entityType, + 'bundle' => $bundle, + 'mode' => 'default', + 'status' => true, + ]); + + $storage->save(); } $storage->setComponent($fieldName, $values)->save(); } - protected function createDisplayStorage(string $context, string $entityType, string $bundle) + protected function getEntityDisplay(string $context): ?EntityDisplayInterface { - $storageValues = [ - 'id' => "$entityType.$bundle.default", - 'targetEntityType' => $entityType, - 'bundle' => $bundle, - 'mode' => 'default', - 'status' => true, - ]; + $entityType = $this->input->getArgument('entityType'); + $bundle = $this->input->getArgument('bundle'); - $storage = $this->entityTypeManager + return $this->entityTypeManager ->getStorage(sprintf('entity_%s_display', $context)) - ->create($storageValues); - - $storage->save(); - - return $storage; + ->load("$entityType.$bundle.default"); } - protected function logResult(FieldConfig $field) + protected function logResult(FieldConfigInterface $field): void { $this->logger()->success( sprintf( @@ -497,16 +558,15 @@ protected function logResult(FieldConfig $field) ) ); - $routeName = "entity.field_config.{$field->get('entity_type')}_field_edit_form"; + /** @var EntityTypeInterface $entityType */ + $entityType = $this->entityTypeManager->getDefinition($field->get('entity_type')); + + $routeName = "entity.field_config.{$entityType->id()}_field_edit_form"; $routeParams = [ 'field_config' => $field->id(), - "{$field->get('entity_type')}_type" => $field->get('bundle'), + $entityType->getBundleEntityType() => $field->get('bundle'), ]; - if ($this->input->getArgument('entityType') === 'taxonomy_term') { - $routeParams['taxonomy_vocabulary'] = $field->get('bundle'); - } - if ($this->moduleHandler->moduleExists('field_ui')) { $this->logger()->success( 'Further customisation can be done at the following url:' @@ -518,7 +578,7 @@ protected function logResult(FieldConfig $field) } } - protected function generateFieldName(string $source) + protected function generateFieldName(string $source, string $bundle): string { // Only lowercase alphanumeric characters and underscores $machineName = preg_replace('/[^_a-z0-9]/i', '_', $source); @@ -529,30 +589,36 @@ protected function generateFieldName(string $source) // Only lowercase $machineName = strtolower($machineName); // Add the prefix - $machineName = sprintf('field_%s', $machineName); + $machineName = sprintf('field_%s_%s', $bundle, $machineName); // Maximum 32 characters $machineName = substr($machineName, 0, 32); return $machineName; } - protected function fieldStorageExists(string $fieldName, string $entityType) + protected function fieldExists(string $fieldName, string $entityType, string $bundle): bool + { + $fieldDefinitions = $this->entityFieldManager->getFieldDefinitions($entityType, $bundle); + + return isset($fieldDefinitions[$fieldName]); + } + + protected function fieldStorageExists(string $fieldName, string $entityType): bool { $fieldStorageDefinitions = $this->entityFieldManager->getFieldStorageDefinitions($entityType); + return isset($fieldStorageDefinitions[$fieldName]); } - protected function entityTypeBundleExists(string $entityType, string $bundleName) + protected function entityTypeBundleExists(string $entityType, string $bundleName): bool { return isset($this->entityTypeBundleInfo->getBundleInfo($entityType)[$bundleName]); } - protected function getExistingFieldStorageOptions(string $entityType, string $bundle) + protected function getExistingFieldStorageOptions(string $entityType, string $bundle): array { - $options = []; - - // Load the fieldStorages and build the list of options. $fieldTypes = $this->fieldTypePluginManager->getDefinitions(); + $options = []; foreach ($this->entityFieldManager->getFieldStorageDefinitions($entityType) as $fieldName => $fieldStorage) { // Do not show: @@ -565,7 +631,8 @@ protected function getExistingFieldStorageOptions(string $entityType, string $bu ? $fieldTypes[$fieldType]['id'] : $fieldTypes[$fieldType]['label']; - if ($fieldStorage instanceof FieldStorageConfigInterface + if ( + $fieldStorage instanceof FieldStorageConfigInterface && !$fieldStorage->isLocked() && empty($fieldTypes[$fieldType]['no_ui']) && !in_array($bundle, $fieldStorage->getBundles(), true) @@ -579,66 +646,33 @@ protected function getExistingFieldStorageOptions(string $entityType, string $bu return $options; } - /** - * Returns the cardinality enforced by the field type. - * - * Some field types choose to enforce a fixed cardinality. This method - * returns that cardinality or NULL if no cardinality has been enforced. - * - * @param string $entityType - * @return int|null - */ - protected function getEnforcedCardinality(string $entityType) + protected function hasContentTranslation(): bool { - $definition = $this->fieldTypePluginManager->getDefinition($entityType); - return $definition['cardinality'] ?? null; + $entityType = $this->input->getArgument('entityType'); + $bundle = $this->input->getArgument('bundle'); + + return $this->moduleHandler->moduleExists('content_translation') + && $this->contentTranslationManager->isEnabled($entityType, $bundle); } - /** - * @param string $fieldName - * @param string $entityType - * @param string $bundle - * @return \Drupal\Core\Field\FieldItemInterface - */ - protected function getFieldType(string $fieldName, string $entityType, string $bundle) + protected function ensureArgument(string $name, callable $asker) { - $ids = (object) [ - 'entity_type' => $entityType, - 'bundle' => $bundle, - 'entity_id' => null, - ]; + $value = $this->input->getArgument($name) ?? $asker(); + $this->input->setArgument($name, $value); - $entity = _field_create_entity_from_ids($ids); - $items = $entity->get($fieldName); - $item = $items->first() ?: $items->appendItem(); - - return $item; + return $value; } - /** - * @param string $question - * @param array $choices - * If an associative array is passed, the chosen *key* is returned. - * @param bool $multiSelect - * @param null $default - * @return mixed - */ - protected function choice($question, array $choices, $multiSelect = false, $default = null) + protected function ensureOption(string $name, callable $asker) { - $choicesValues = array_values($choices); - $question = new ChoiceQuestion($question, $choicesValues, $default); - $question->setMultiselect($multiSelect); - $return = $this->io()->askQuestion($question); - - if ($multiSelect) { - return array_map( - function ($value) use ($choices) { - return array_search($value, $choices); - }, - $return - ); - } + $value = $this->input->getOption($name) ?? $asker(); + $this->input->setOption($name, $value); - return array_search($return, $choices); + return $value; + } + + protected function optionalAsk(string $question) + { + return $this->io()->ask($question, null, function ($value) { return $value; }); } } diff --git a/src/Drupal/Commands/core/drush.services.yml b/src/Drupal/Commands/core/drush.services.yml index 65c0d65867..fb93acb015 100644 --- a/src/Drupal/Commands/core/drush.services.yml +++ b/src/Drupal/Commands/core/drush.services.yml @@ -22,17 +22,19 @@ services: tags: - { name: drush.command } field.create.commands: - class: \Drush\Drupal\Commands\core\FieldCreateCommands - arguments: - - '@plugin.manager.field.field_type' - - '@plugin.manager.field.widget' - - '@plugin.manager.entity_reference_selection' - - '@entity_type.manager' - - '@entity_type.bundle.info' - - '@module_handler' - - '@entity_field.manager' - tags: - - { name: drush.command } + class: \Drush\Drupal\Commands\core\FieldCreateCommands + arguments: + - '@plugin.manager.field.field_type' + - '@plugin.manager.field.widget' + - '@plugin.manager.entity_reference_selection' + - '@entity_type.manager' + - '@entity_type.bundle.info' + - '@module_handler' + - '@entity_field.manager' + calls: + - [ setContentTranslationManager, [ '@?content_translation.manager' ] ] + tags: + - { name: drush.command } image.commands: class: \Drush\Drupal\Commands\core\ImageCommands tags: From 469ae36f14efdd4ce3f86d6b38ec2f75d28f827a Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Thu, 7 Oct 2021 11:46:19 +0200 Subject: [PATCH 04/16] Remove empty legacy command --- src/Commands/LegacyCommands.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Commands/LegacyCommands.php b/src/Commands/LegacyCommands.php index 9096e76492..3d382552ed 100644 --- a/src/Commands/LegacyCommands.php +++ b/src/Commands/LegacyCommands.php @@ -129,18 +129,6 @@ public function download() { } - /** - * field-create has been removed. Please try `generate field` command. - * - * @command field:create - * @aliases field-create - * @hidden - * @obsolete - */ - public function field() - { - } - /** * core:execute has been removed. Please try `site:ssh` command. * From d265359753a951d44e65362832fc2c165ffb5340 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 23 Oct 2021 10:37:20 -0400 Subject: [PATCH 05/16] phpcs --- src/Drupal/Commands/core/FieldCreateCommands.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Drupal/Commands/core/FieldCreateCommands.php b/src/Drupal/Commands/core/FieldCreateCommands.php index 012eb56b76..668635cb08 100644 --- a/src/Drupal/Commands/core/FieldCreateCommands.php +++ b/src/Drupal/Commands/core/FieldCreateCommands.php @@ -631,8 +631,7 @@ protected function getExistingFieldStorageOptions(string $entityType, string $bu ? $fieldTypes[$fieldType]['id'] : $fieldTypes[$fieldType]['label']; - if ( - $fieldStorage instanceof FieldStorageConfigInterface + if ($fieldStorage instanceof FieldStorageConfigInterface && !$fieldStorage->isLocked() && empty($fieldTypes[$fieldType]['no_ui']) && !in_array($bundle, $fieldStorage->getBundles(), true) @@ -670,9 +669,11 @@ protected function ensureOption(string $name, callable $asker) return $value; } - + protected function optionalAsk(string $question) { - return $this->io()->ask($question, null, function ($value) { return $value; }); + return $this->io()->ask($question, null, function ($value) { + return $value; + }); } } From 017477c90e77fb44bf72355794d0caa3aa77d021 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 4 Nov 2021 08:56:14 -0400 Subject: [PATCH 06/16] Add validators so the new annotations are functional --- src/Commands/ValidatorsCommands.php | 62 +++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/Commands/ValidatorsCommands.php b/src/Commands/ValidatorsCommands.php index daca150bb4..4ae27ca783 100644 --- a/src/Commands/ValidatorsCommands.php +++ b/src/Commands/ValidatorsCommands.php @@ -13,6 +13,39 @@ class ValidatorsCommands { + /** + * Validate that the entity type passed as argument exists. + * + * @hook validate @validate-entity-type-argument + */ + public function hookValidateEntityType(CommandData $commandData): void + { + $argumentName = $commandData->annotationData()->get('validate-entity-type-argument'); + $entityType = $commandData->input()->getArgument($argumentName); + + $this->validateEntityType($entityType); + } + + /** + * Validate that the bundle passed as argument exists. + * + * @hook validate @validate-optional-bundle-argument + */ + public function hookValidateOptionalBundle(CommandData $commandData): void + { + $annotation = $commandData->annotationData()->get('validate-optional-bundle-argument'); + [$entityTypeArgumentName, $bundleArgumentName] = explode(' ', $annotation); + + $entityType = $commandData->input()->getArgument($entityTypeArgumentName); + $bundle = $commandData->input()->getArgument($bundleArgumentName); + + if (!$bundle) { + return; + } + + $this->validateBundle($entityType, $bundle); + } + /** * Validate that passed entity names are valid. * @see \Drush\Commands\core\ViewsCommands::execute for an example. @@ -132,4 +165,33 @@ public function validatePermissions(CommandData $commandData) return new CommandError($msg); } } + + public function validateEntityType(string $entityTypeId): void + { + if (!\Drupal::entityTypeManager()->hasDefinition($entityTypeId)) { + throw new \InvalidArgumentException( + t("Entity type with id ':entityType' does not exist.", [':entityType' => $entityTypeId]) + ); + } + } + + public function validateBundle(string $entityTypeId, string $bundle): void + { + $entityTypeDefinition = \Drupal::entityTypeManager()->getDefinition($entityTypeId); + + if ($entityTypeDefinition && $bundleEntityType = $entityTypeDefinition->getBundleEntityType()) { + $bundleDefinition = \Drupal::entityTypeManager() + ->getStorage($bundleEntityType) + ->load($bundle); + } + + if (!isset($bundleDefinition)) { + throw new \InvalidArgumentException( + t("Bundle ':bundle' does not exist on entity type with id ':entityType'.", [ + ':bundle' => $bundle, + ':entityType' => $entityTypeId, + ]) + ); + } + } } From ccf1fe28a89c7c2f706aba8db58e29f03296d790 Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Thu, 25 Nov 2021 17:03:49 +0100 Subject: [PATCH 07/16] Fix some issues + small refactor - Fix issues when command is called non-interactively - Improve handling of entity types without bundles --- src/Commands/ValidatorsCommands.php | 63 ------- .../Commands/core/FieldCreateCommands.php | 173 +++++++++++------- 2 files changed, 106 insertions(+), 130 deletions(-) diff --git a/src/Commands/ValidatorsCommands.php b/src/Commands/ValidatorsCommands.php index 4ae27ca783..f6be086645 100644 --- a/src/Commands/ValidatorsCommands.php +++ b/src/Commands/ValidatorsCommands.php @@ -12,40 +12,6 @@ */ class ValidatorsCommands { - - /** - * Validate that the entity type passed as argument exists. - * - * @hook validate @validate-entity-type-argument - */ - public function hookValidateEntityType(CommandData $commandData): void - { - $argumentName = $commandData->annotationData()->get('validate-entity-type-argument'); - $entityType = $commandData->input()->getArgument($argumentName); - - $this->validateEntityType($entityType); - } - - /** - * Validate that the bundle passed as argument exists. - * - * @hook validate @validate-optional-bundle-argument - */ - public function hookValidateOptionalBundle(CommandData $commandData): void - { - $annotation = $commandData->annotationData()->get('validate-optional-bundle-argument'); - [$entityTypeArgumentName, $bundleArgumentName] = explode(' ', $annotation); - - $entityType = $commandData->input()->getArgument($entityTypeArgumentName); - $bundle = $commandData->input()->getArgument($bundleArgumentName); - - if (!$bundle) { - return; - } - - $this->validateBundle($entityType, $bundle); - } - /** * Validate that passed entity names are valid. * @see \Drush\Commands\core\ViewsCommands::execute for an example. @@ -165,33 +131,4 @@ public function validatePermissions(CommandData $commandData) return new CommandError($msg); } } - - public function validateEntityType(string $entityTypeId): void - { - if (!\Drupal::entityTypeManager()->hasDefinition($entityTypeId)) { - throw new \InvalidArgumentException( - t("Entity type with id ':entityType' does not exist.", [':entityType' => $entityTypeId]) - ); - } - } - - public function validateBundle(string $entityTypeId, string $bundle): void - { - $entityTypeDefinition = \Drupal::entityTypeManager()->getDefinition($entityTypeId); - - if ($entityTypeDefinition && $bundleEntityType = $entityTypeDefinition->getBundleEntityType()) { - $bundleDefinition = \Drupal::entityTypeManager() - ->getStorage($bundleEntityType) - ->load($bundle); - } - - if (!isset($bundleDefinition)) { - throw new \InvalidArgumentException( - t("Bundle ':bundle' does not exist on entity type with id ':entityType'.", [ - ':bundle' => $bundle, - ':entityType' => $entityTypeId, - ]) - ); - } - } } diff --git a/src/Drupal/Commands/core/FieldCreateCommands.php b/src/Drupal/Commands/core/FieldCreateCommands.php index 668635cb08..4a68dc0a34 100644 --- a/src/Drupal/Commands/core/FieldCreateCommands.php +++ b/src/Drupal/Commands/core/FieldCreateCommands.php @@ -62,7 +62,7 @@ public function __construct( $this->entityFieldManager = $entityFieldManager; } - public function setContentTranslationManager($manager): void + public function setContentTranslationManager(ContentTranslationManagerInterface $manager): void { $this->contentTranslationManager = $manager; } @@ -73,9 +73,6 @@ public function setContentTranslationManager($manager): void * @command field:create * @aliases field-create,fc * - * @validate-entity-type-argument entityType - * @validate-optional-bundle-argument entityType bundle - * * @param string $entityType * The machine name of the entity type * @param string $bundle @@ -104,6 +101,8 @@ public function setContentTranslationManager($manager): void * * @option existing * Re-use an existing field. + * @option existing-field-name + * The name of an existing field you want to re-use. Only used in non-interactive context. * @option show-machine-names * Show machine names instead of labels in option lists. * @@ -129,32 +128,27 @@ public function create(string $entityType, ?string $bundle = null, array $option 'target-type' => InputOption::VALUE_OPTIONAL, 'target-bundle' => InputOption::VALUE_OPTIONAL, 'show-machine-names' => InputOption::VALUE_OPTIONAL, + 'existing-field-name' => InputOption::VALUE_OPTIONAL, 'existing' => false, ]): void { - if (!$this->entityTypeManager->hasDefinition($entityType)) { - throw new \InvalidArgumentException( - t('Entity type with id \':entityType\' does not exist.', [':entityType' => $entityType]) - ); - } + $this->validateEntityType($entityType); - $bundle = $this->ensureArgument('bundle', [$this, 'askBundle']); + $this->input->setArgument('bundle', $bundle = $bundle ?? $this->askBundle()); + $this->validateBundle($entityType, $bundle); - if (!$this->entityTypeBundleExists($entityType, $bundle)) { - throw new \InvalidArgumentException( - t('Bundle with id \':bundle\' does not exist on entity type \':entityType\'.', [ - ':bundle' => $bundle, - ':entityType' => $entityType, - ]) - ); - } + if ($this->input->getOption('existing') || $this->input->getOption('existing-field-name')) { + $this->ensureOption('existing-field-name', [$this, 'askExistingFieldName'], false); - if ($this->input->getOption('existing')) { - $fieldName = $this->ensureOption('field-name', [$this, 'askExisting']); + if (!$fieldName = $this->input->getOption('existing-field-name')) { + throw new \InvalidArgumentException( + t('There are no existing fields that can be added.') + ); + } if (!$this->fieldStorageExists($fieldName, $entityType)) { throw new \InvalidArgumentException( - t('Field storage with name \':fieldName\' does not yet exist. Call this command without the --existing option first.', [ + t("Field storage with name ':fieldName' does not yet exist. Call this command without the --existing option first.", [ ':fieldName' => $fieldName, ]) ); @@ -164,42 +158,44 @@ public function create(string $entityType, ?string $bundle = null, array $option if ($this->fieldExists($fieldName, $entityType, $bundle)) { throw new \InvalidArgumentException( - t('Field with name \':fieldName\' already exists on bundle \':bundle\'.', [ + t("Field with name ':fieldName' already exists on bundle ':bundle'.", [ ':fieldName' => $fieldName, ':bundle' => $bundle, ]) ); } + $this->input->setOption('field-name', $fieldName); $this->input->setOption('field-type', $fieldStorage->getType()); $this->input->setOption('target-type', $fieldStorage->getSetting('target_type')); - $this->ensureOption('field-label', [$this, 'askFieldLabel']); - $this->ensureOption('field-description', [$this, 'askFieldDescription']); - $this->ensureOption('field-widget', [$this, 'askFieldWidget']); - $this->ensureOption('is-required', [$this, 'askRequired']); - $this->ensureOption('is-translatable', [$this, 'askTranslatable']); + $this->ensureOption('field-label', [$this, 'askFieldLabel'], true); + $this->ensureOption('field-description', [$this, 'askFieldDescription'], false); + $this->ensureOption('field-widget', [$this, 'askFieldWidget'], true); + $this->ensureOption('is-required', [$this, 'askRequired'], false); + $this->ensureOption('is-translatable', [$this, 'askTranslatable'], false); } else { - $this->ensureOption('field-label', [$this, 'askFieldLabel']); - $fieldName = $this->ensureOption('field-name', [$this, 'askFieldName']); + $this->ensureOption('field-label', [$this, 'askFieldLabel'], true); + $this->ensureOption('field-name', [$this, 'askFieldName'], true); + $fieldName = $this->input->getOption('field-name'); if ($this->fieldStorageExists($fieldName, $entityType)) { throw new \InvalidArgumentException( - t('Field storage with name \':fieldName\' already exists. Call this command with the --existing option to add an existing field to a bundle.', [ + t("Field storage with name ':fieldName' already exists. Call this command with the --existing option to add an existing field to a bundle.", [ ':fieldName' => $fieldName, ]) ); } - $this->ensureOption('field-description', [$this, 'askFieldDescription']); - $this->ensureOption('field-type', [$this, 'askFieldType']); - $this->ensureOption('field-widget', [$this, 'askFieldWidget']); - $this->ensureOption('is-required', [$this, 'askRequired']); - $this->ensureOption('is-translatable', [$this, 'askTranslatable']); - $this->ensureOption('cardinality', [$this, 'askCardinality']); + $this->ensureOption('field-description', [$this, 'askFieldDescription'], false); + $this->ensureOption('field-type', [$this, 'askFieldType'], true); + $this->ensureOption('field-widget', [$this, 'askFieldWidget'], true); + $this->ensureOption('is-required', [$this, 'askRequired'], false); + $this->ensureOption('is-translatable', [$this, 'askTranslatable'], false); + $this->ensureOption('cardinality', [$this, 'askCardinality'], true); if ($this->input->getOption('field-type') === 'entity_reference') { - $this->ensureOption('target-type', [$this, 'askReferencedEntityType']); + $this->ensureOption('target-type', [$this, 'askReferencedEntityType'], true); } $this->createFieldStorage(); @@ -218,13 +214,52 @@ public function create(string $entityType, ?string $bundle = null, array $option $this->logResult($field); } - protected function askExisting(): string + 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'); $bundle = $this->input->getArgument('bundle'); $choices = $this->getExistingFieldStorageOptions($entityType, $bundle); - return $this->io()->choice('Choose an existing field', $choices); + if (empty($choices)) { + return null; + } + + return $this->choice('Choose an existing field', $choices); } protected function askFieldName(): string @@ -332,22 +367,30 @@ protected function askBundle(): ?string $bundleInfo = $this->entityTypeBundleInfo->getBundleInfo($entityTypeId); $choices = []; - if (empty($bundleInfo)) { - if ($bundleEntityType) { - throw new \InvalidArgumentException( - t('Entity type with id \':entityType\' does not have any bundles.', [':entityType' => $entityTypeId]) - ); - } + // 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 (!$answer = $this->io()->choice('Bundle', $choices)) { + if (!$this->input->isInteractive() || !$answer = $this->io()->choice('Bundle', $choices)) { throw new \InvalidArgumentException(t('The bundle argument is required.')); } @@ -509,7 +552,7 @@ protected function createFieldDisplay(string $context): void } // Command files may customize $values as desired. - $handlers = $this->getCustomEventHandlers("field-create-{$context}-display"); + $handlers = $this->getCustomEventHandlers(sprintf('field-create-%s-display', $context)); foreach ($handlers as $handler) { $handler($values); } @@ -518,13 +561,13 @@ protected function createFieldDisplay(string $context): void if (!$storage instanceof EntityDisplayInterface) { $this->logger()->info( - sprintf('\'%s\' display storage not found for %s type \'%s\', creating now.', $context, $entityType, $bundle) + sprintf("'%s' display storage not found for %s type '%s', creating now.", $context, $entityType, $bundle) ); $storage = $this->entityTypeManager ->getStorage(sprintf('entity_%s_display', $context)) ->create([ - 'id' => "$entityType.$bundle.default", + 'id' => sprintf('%s.%s.default', $entityType, $bundle), 'targetEntityType' => $entityType, 'bundle' => $bundle, 'mode' => 'default', @@ -544,14 +587,14 @@ protected function getEntityDisplay(string $context): ?EntityDisplayInterface return $this->entityTypeManager ->getStorage(sprintf('entity_%s_display', $context)) - ->load("$entityType.$bundle.default"); + ->load(sprintf('%s.%s.default', $entityType, $bundle)); } protected function logResult(FieldConfigInterface $field): void { $this->logger()->success( sprintf( - 'Successfully created field \'%s\' on %s type with bundle \'%s\'', + "Successfully created field '%s' on %s type with bundle '%s'", $field->get('field_name'), $field->get('entity_type'), $field->get('bundle') @@ -561,7 +604,7 @@ protected function logResult(FieldConfigInterface $field): void /** @var EntityTypeInterface $entityType */ $entityType = $this->entityTypeManager->getDefinition($field->get('entity_type')); - $routeName = "entity.field_config.{$entityType->id()}_field_edit_form"; + $routeName = sprintf('entity.field_config.%s_field_edit_form', $entityType->id()); $routeParams = [ 'field_config' => $field->id(), $entityType->getBundleEntityType() => $field->get('bundle'), @@ -610,11 +653,6 @@ protected function fieldStorageExists(string $fieldName, string $entityType): bo return isset($fieldStorageDefinitions[$fieldName]); } - protected function entityTypeBundleExists(string $entityType, string $bundleName): bool - { - return isset($this->entityTypeBundleInfo->getBundleInfo($entityType)[$bundleName]); - } - protected function getExistingFieldStorageOptions(string $entityType, string $bundle): array { $fieldTypes = $this->fieldTypePluginManager->getDefinitions(); @@ -654,20 +692,21 @@ protected function hasContentTranslation(): bool && $this->contentTranslationManager->isEnabled($entityType, $bundle); } - protected function ensureArgument(string $name, callable $asker) + protected function ensureOption(string $name, callable $asker, bool $required): void { - $value = $this->input->getArgument($name) ?? $asker(); - $this->input->setArgument($name, $value); + $value = $this->input->getOption($name); - return $value; - } + if ($value === null && $this->input->isInteractive()) { + $value = $asker(); + } - protected function ensureOption(string $name, callable $asker) - { - $value = $this->input->getOption($name) ?? $asker(); - $this->input->setOption($name, $value); + if ($required && $value === null) { + throw new \InvalidArgumentException(dt('The %optionName option is required.', [ + '%optionName' => $name, + ])); + } - return $value; + $this->input->setOption($name, $value); } protected function optionalAsk(string $question) From ad3c1ef75e81a85eda3f2d4e036a910551162d4b Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Wed, 24 Nov 2021 15:35:10 +0100 Subject: [PATCH 08/16] Improve various prompts --- .../Commands/core/FieldCreateCommands.php | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/Drupal/Commands/core/FieldCreateCommands.php b/src/Drupal/Commands/core/FieldCreateCommands.php index 4a68dc0a34..df581d88e9 100644 --- a/src/Drupal/Commands/core/FieldCreateCommands.php +++ b/src/Drupal/Commands/core/FieldCreateCommands.php @@ -22,6 +22,7 @@ use Drupal\field\FieldStorageConfigInterface; use Drush\Commands\DrushCommands; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Question\ChoiceQuestion; class FieldCreateCommands extends DrushCommands implements CustomEventAwareInterface { @@ -259,7 +260,7 @@ protected function askExistingFieldName(): ?string return null; } - return $this->choice('Choose an existing field', $choices); + return $this->io()->choice('Choose an existing field', $choices); } protected function askFieldName(): string @@ -275,7 +276,7 @@ protected function askFieldName(): string } while (!$fieldName) { - $answer = $this->io()->ask('Field name', $machineName); + $answer = $this->io()->ask('Field name', $machineName, [static::class, 'validateRequired']); if (!preg_match('/^[_a-z]+[_a-z0-9]*$/', $answer)) { $this->logger()->error('Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character.'); @@ -300,12 +301,12 @@ protected function askFieldName(): string protected function askFieldLabel(): string { - return $this->io()->ask('Field label'); + return $this->io()->ask('Field label', null, [static::class, 'validateRequired']); } protected function askFieldDescription(): ?string { - return $this->optionalAsk('Field description'); + return $this->io()->ask('Field description'); } protected function askFieldType(): string @@ -342,7 +343,7 @@ protected function askFieldWidget(): string $choices[$name] = $label; } - return $this->io()->choice('Field widget', $choices, 0); + return $this->io()->choice('Field widget', $choices); } protected function askRequired(): bool @@ -411,7 +412,7 @@ protected function askCardinality(): int $cardinality = $this->io()->choice( 'Allowed number of values', array_combine($choices, $choices), - 0 + 1 ); $limit = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED; @@ -453,7 +454,10 @@ protected function askReferencedBundles(FieldDefinitionInterface $fieldDefinitio $choices[$bundle] = $label; } - return $this->io()->choice('Referenced bundles', $choices, 0); + $question = (new ChoiceQuestion('Referenced bundles', $choices)) + ->setMultiselect(true); + + return $this->io()->askQuestion($question); } protected function createField(): FieldConfigInterface @@ -709,10 +713,14 @@ protected function ensureOption(string $name, callable $asker, bool $required): $this->input->setOption($name, $value); } - protected function optionalAsk(string $question) + public static function validateRequired(?string $value): string { - return $this->io()->ask($question, null, function ($value) { - return $value; - }); + // FALSE is not considered as empty value because question helper use + // it as negative answer on confirmation questions. + if ($value === NULL || $value === '') { + throw new \UnexpectedValueException('This value is required.'); + } + + return $value; } } From a5a55e004cec8ffd0b13018c08335ca633145a9c Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Wed, 24 Nov 2021 15:52:22 +0100 Subject: [PATCH 09/16] Add LinkHooks --- src/Drupal/Commands/core/LinkHooks.php | 105 ++++++++++++++++++++ src/Drupal/Commands/core/drush.services.yml | 6 ++ 2 files changed, 111 insertions(+) create mode 100644 src/Drupal/Commands/core/LinkHooks.php diff --git a/src/Drupal/Commands/core/LinkHooks.php b/src/Drupal/Commands/core/LinkHooks.php new file mode 100644 index 0000000000..0606e665cc --- /dev/null +++ b/src/Drupal/Commands/core/LinkHooks.php @@ -0,0 +1,105 @@ +moduleHandler = $moduleHandler; + } + + /** @hook option field:create */ + public function hookOption(Command $command, AnnotationData $annotationData): void + { + if (!$this->isInstalled()) { + return; + } + + $command->addOption( + 'link-type', + '', + InputOption::VALUE_REQUIRED, + 'Allowed link type.' + ); + + $command->addOption( + 'allow-link-text', + '', + InputOption::VALUE_REQUIRED, + 'Allow link text.' + ); + } + + /** @hook on-event field-create-set-options */ + public function hookSetOptions(InputInterface $input): void + { + if ( + !$this->isInstalled() + || $input->getOption('field-type') !== 'link' + ) { + return; + } + + $input->setOption( + 'link-type', + $this->input->getOption('link-type') ?? $this->askLinkType() + ); + + $input->setOption( + 'allow-link-text', + $this->input->getOption('allow-link-text') ?? $this->askAllowLinkText() + ); + } + + /** @hook on-event field-create-field-config */ + public function hookFieldConfig(array $values, InputInterface $input): array + { + if ( + !$this->isInstalled() + || $values['field_type'] !== 'link' + ) { + return $values; + } + + $values['settings']['title'] = $input->getOption('allow-link-text'); + $values['settings']['link_type'] = $input->getOption('link-type'); + + return $values; + } + + protected function askLinkType(): int + { + return $this->io()->choice('Allowed link type', [ + LinkItemInterface::LINK_INTERNAL => (string) t('Internal links only'), + LinkItemInterface::LINK_EXTERNAL => (string) t('External links only'), + LinkItemInterface::LINK_GENERIC => (string) t('Both internal and external links'), + ]); + } + + protected function askAllowLinkText(): int + { + return $this->io()->choice('Allow link text', [ + DRUPAL_DISABLED => (string) t('Disabled'), + DRUPAL_OPTIONAL => (string) t('Optional'), + DRUPAL_REQUIRED => (string) t('Required'), + ]); + } + + protected function isInstalled(): bool + { + return $this->moduleHandler->moduleExists('link'); + } +} diff --git a/src/Drupal/Commands/core/drush.services.yml b/src/Drupal/Commands/core/drush.services.yml index fb93acb015..e70ea33f66 100644 --- a/src/Drupal/Commands/core/drush.services.yml +++ b/src/Drupal/Commands/core/drush.services.yml @@ -35,6 +35,12 @@ services: - [ setContentTranslationManager, [ '@?content_translation.manager' ] ] tags: - { name: drush.command } + link.hooks: + class: \Drush\Drupal\Commands\core\LinkHooks + arguments: + - '@module_handler' + tags: + - { name: drush.command } image.commands: class: \Drush\Drupal\Commands\core\ImageCommands tags: From 707d7ef6973f5684c37cdf790e03caf9f81691df Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Wed, 24 Nov 2021 16:00:09 +0100 Subject: [PATCH 10/16] Apply phpcs fixes --- src/Drupal/Commands/core/FieldCreateCommands.php | 2 +- src/Drupal/Commands/core/LinkHooks.php | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Drupal/Commands/core/FieldCreateCommands.php b/src/Drupal/Commands/core/FieldCreateCommands.php index df581d88e9..f43a338f24 100644 --- a/src/Drupal/Commands/core/FieldCreateCommands.php +++ b/src/Drupal/Commands/core/FieldCreateCommands.php @@ -717,7 +717,7 @@ public static function validateRequired(?string $value): string { // FALSE is not considered as empty value because question helper use // it as negative answer on confirmation questions. - if ($value === NULL || $value === '') { + if ($value === null || $value === '') { throw new \UnexpectedValueException('This value is required.'); } diff --git a/src/Drupal/Commands/core/LinkHooks.php b/src/Drupal/Commands/core/LinkHooks.php index 0606e665cc..646e28e2e8 100644 --- a/src/Drupal/Commands/core/LinkHooks.php +++ b/src/Drupal/Commands/core/LinkHooks.php @@ -46,8 +46,7 @@ public function hookOption(Command $command, AnnotationData $annotationData): vo /** @hook on-event field-create-set-options */ public function hookSetOptions(InputInterface $input): void { - if ( - !$this->isInstalled() + if (!$this->isInstalled() || $input->getOption('field-type') !== 'link' ) { return; @@ -67,8 +66,7 @@ public function hookSetOptions(InputInterface $input): void /** @hook on-event field-create-field-config */ public function hookFieldConfig(array $values, InputInterface $input): array { - if ( - !$this->isInstalled() + if (!$this->isInstalled() || $values['field_type'] !== 'link' ) { return $values; From 06c28b74383758a5dfdc24c83200619a53a50b17 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 2 Dec 2021 09:32:42 -0500 Subject: [PATCH 11/16] Add tests, and misc unrelated improvements --- .ddev/config.yaml | 2 +- .ddev/web-build/Dockerfile | 4 ++ .../Commands/core/FieldCreateCommands.php | 2 +- tests/functional/FieldCreateTest.php | 59 +++++++++++++++++++ tests/functional/UserTest.php | 32 +--------- .../resources/unish_article_bundles.php | 11 ++++ tests/unish/CommandUnishTestCase.php | 4 +- tests/unish/CreateEntityType.php | 46 +++++++++++++++ tests/unish/UnishIntegrationTestCase.php | 4 +- 9 files changed, 127 insertions(+), 37 deletions(-) create mode 100644 .ddev/web-build/Dockerfile create mode 100644 tests/functional/FieldCreateTest.php create mode 100644 tests/functional/resources/unish_article_bundles.php create mode 100644 tests/unish/CreateEntityType.php diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 951593e052..d25d9b975a 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -17,4 +17,4 @@ composer_version: "" disable_settings_management: true web_environment: - UNISH_DB_URL=mysql://root:root@db - - PHP_EXTENSIONS_DISABLE=uploadprogress + - DRUSH_OPTIONS_URI=$DDEV_PRIMARY_URL diff --git a/.ddev/web-build/Dockerfile b/.ddev/web-build/Dockerfile new file mode 100644 index 0000000000..48d9a8a222 --- /dev/null +++ b/.ddev/web-build/Dockerfile @@ -0,0 +1,4 @@ +# See https://stackoverflow.com/a/67092243/265501 +ARG BASE_IMAGE +FROM $BASE_IMAGE +RUN echo 'export PATH="$PATH:/var/www/html"' > /etc/bashrc/commandline-addons.bashrc diff --git a/src/Drupal/Commands/core/FieldCreateCommands.php b/src/Drupal/Commands/core/FieldCreateCommands.php index f43a338f24..706dc61c1a 100644 --- a/src/Drupal/Commands/core/FieldCreateCommands.php +++ b/src/Drupal/Commands/core/FieldCreateCommands.php @@ -457,7 +457,7 @@ protected function askReferencedBundles(FieldDefinitionInterface $fieldDefinitio $question = (new ChoiceQuestion('Referenced bundles', $choices)) ->setMultiselect(true); - return $this->io()->askQuestion($question); + return $this->io()->askQuestion($question) ?: []; } protected function createField(): FieldConfigInterface diff --git a/tests/functional/FieldCreateTest.php b/tests/functional/FieldCreateTest.php new file mode 100644 index 0000000000..cf487e3e3d --- /dev/null +++ b/tests/functional/FieldCreateTest.php @@ -0,0 +1,59 @@ +getSites()) { + $this->setUpDrupal(1, true); + // Create a content entity with bundles. + CreateEntityType::createContentEntity($this); + $this->drush('pm-enable', ['text,field_ui,unish_article']); + $this->drush('php:script', ['tests/functional/resources/unish_article_bundles.php']); + } + } + + public function testFieldCreate() + { + // Arguments. + $this->drush('field:create', [], [], null, null, self::EXIT_ERROR); + $this->assertStringContainsString('Not enough arguments (missing: "entityType")', $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->drush('field:create', ['user', 'user'], [], null, null,self::EXIT_ERROR); + $this->assertStringNotContainsString('bundle', $this->getErrorOutputRaw()); + + // New field storage + $this->drush('field:create', ['unish_article', 'alpha'], ['field-label' => 'Test', 'field-name' => 'field_test2', 'field-type' => 'entity_reference', 'field-widget' => 'entity_reference_autocomplete', 'cardinality' => '-1'], null, null, self::EXIT_ERROR); + $this->assertStringContainsString('The target-type option is required.', $this->getErrorOutputRaw()); + /// @todo --target-bundle not yet validated. + // $this->drush('field:create', ['unish_article', 'alpha'], ['field-label' => 'Test', 'field-name' => 'field_test3', 'field-type' => 'entity_reference', 'field-widget' => 'entity_reference_autocomplete', 'cardinality' => '-1', 'target-type' => 'unish_article', 'target-bundle' => 'NO-EXIST']); + // $this->assertStringContainsString('TODO', $this->getErrorOutputRaw()); + $this->drush('field:create', ['unish_article', 'alpha'], ['field-label' => 'Test', 'field-name' => 'field_test3', 'field-type' => 'entity_reference', 'field-widget' => 'entity_reference_autocomplete', 'cardinality' => '-1', 'target-type' => 'unish_article', 'target-bundle' => 'beta']); + $this->assertStringContainsString("Successfully created field 'field_test3' on unish_article type with bundle 'alpha'", $this->getErrorOutputRaw()); + $this->assertStringContainsString("Further customisation can be done at the following url: +http://dev/admin/structure/unish_article_types/manage/alpha/fields/unish_article.alpha.field_test3", $this->getErrorOutputRaw()); + $php = "return Drupal::entityTypeManager()->getStorage('field_config')->load('unish_article.alpha.field_test3')->getSettings()"; + $this->drush('php:eval', [$php], ['format' => 'json']); + $settings = $this->getOutputFromJSON(); + $this->assertSame('unish_article', $settings['target_type']); + $this->assertEquals(['beta' => 'beta'], $settings['handler_settings']['target_bundles']); + $this->drush('field:create', ['unish_article', 'alpha'], ['field-name' => 'field_test3', 'field-label' => 'Body'], null, null, self::EXIT_ERROR); + $this->assertStringContainsString('Field storage with name \'field_test3\' already exists. Call this command with the --existing option to add an existing field to a bundle.', $this->getErrorOutputRaw()); + + // Existing storage + $this->drush('field:create', ['unish_article', 'beta'], ['existing-field-name' => 'field_test3', 'field-label' => 'Body', 'field-widget' => 'text_textarea_with_summary']); + $this->assertStringContainsString('Success', $this->getErrorOutputRaw()); + $this->drush('field:create', ['unish_article', 'beta'], ['existing-field-name' => 'field_test3', 'field-label' => 'Body', 'field-widget' => 'text_textarea_with_summary'], null, null, self::EXIT_ERROR); + $this->assertStringContainsString('Field with name \'field_test3\' already exists on bundle \'beta\'', $this->getErrorOutputRaw()); + } +} diff --git a/tests/functional/UserTest.php b/tests/functional/UserTest.php index 82a716eb93..5e7c18ecf6 100644 --- a/tests/functional/UserTest.php +++ b/tests/functional/UserTest.php @@ -115,37 +115,7 @@ public function testUserLogin() public function testUserCancel() { - $answers = [ - 'name' => 'Unish Article', - 'machine_name' => 'unish_article', - 'description' => 'A test module', - 'package' => 'unish', - 'dependencies' => 'drupal:text', - ]; - $this->drush('generate', ['module'], ['v' => null, 'answer' => $answers, 'destination' => Path::join(self::webroot(), 'modules/contrib')], null, null, self::EXIT_SUCCESS, null, ['SHELL_INTERACTIVE' => 1]); - // Create a content entity type and enable its module. - // Note that only the values below are used. The keys are for documentation. - $answers = [ - 'name' => 'unish_article', - 'entity_type_label' => 'UnishArticle', - 'entity_type_id' => 'unish_article', - 'entity_base_path' => 'admin/content/unish_article', - 'fieldable' => 'no', - 'revisionable' => 'no', - 'translatable' => 'no', - 'bundle' => 'No', - 'canonical page' => 'No', - 'entity template' => 'No', - 'CRUD permissions' => 'No', - 'label base field' => 'Yes', - 'status_base_field' => 'yes', - 'created_base_field' => 'yes', - 'changed_base_field' => 'yes', - 'author_base_field' => 'yes', - 'description_base_field' => 'no', - 'rest_configuration' => 'no', - ]; - $this->drush('generate', ['content-entity'], ['answer' => $answers, 'destination' => Path::join(self::webroot(), 'modules/contrib/unish_article')], null, null, self::EXIT_SUCCESS, null, ['SHELL_INTERACTIVE' => 1]); + CreateEntityType::createContentEntity($this); $this->drush('pm-enable', ['text,unish_article']); // Create one unish_article owned by our example user. $this->drush('php-script', ['create_unish_articles'], ['script-path' => Path::join(__DIR__, 'resources')]); diff --git a/tests/functional/resources/unish_article_bundles.php b/tests/functional/resources/unish_article_bundles.php new file mode 100644 index 0000000000..656f0b154a --- /dev/null +++ b/tests/functional/resources/unish_article_bundles.php @@ -0,0 +1,11 @@ +getStorage('unish_article_type'); +$storage->create([ + 'id' => 'alpha', + 'label' => 'Alpha', +])->save(); +$storage->create([ + 'id' => 'beta', + 'label' => 'Beta', +])->save(); \ No newline at end of file diff --git a/tests/unish/CommandUnishTestCase.php b/tests/unish/CommandUnishTestCase.php index da446c5a0d..7728367506 100644 --- a/tests/unish/CommandUnishTestCase.php +++ b/tests/unish/CommandUnishTestCase.php @@ -77,9 +77,9 @@ public function drushBackground($command, array $args = [], array $options = [], /** * Invoke drush in via execute(). * - * @param command + * @param $command * A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'. - * @param args + * @param $args * Command arguments. * @param $options * An associative array containing options. diff --git a/tests/unish/CreateEntityType.php b/tests/unish/CreateEntityType.php new file mode 100644 index 0000000000..4850b9a8f1 --- /dev/null +++ b/tests/unish/CreateEntityType.php @@ -0,0 +1,46 @@ + 'Unish Article', + 'machine_name' => 'unish_article', + 'description' => 'A test module', + 'package' => 'unish', + 'dependencies' => 'drupal:text', + ]; + $testCase->drush('generate', ['module'], ['verbose' => null, 'answer' => $answers, 'destination' => Path::join($testCase->webroot(), 'modules/contrib')], null, null, $testCase::EXIT_SUCCESS, null, ['SHELL_INTERACTIVE' => 1]); + // Create a content entity type and enable its module. + // Note that only the values below are used. The keys are for documentation. + $answers = [ + 'name' => 'unish_article', + 'entity_type_label' => 'UnishArticle', + 'entity_type_id' => 'unish_article', + 'entity_base_path' => 'admin/content/unish_article', + 'fieldable' => 'yes', + 'revisionable' => 'no', + 'translatable' => 'no', + 'bundle' => 'Yes', + 'canonical page' => 'No', + 'entity template' => 'No', + 'CRUD permissions' => 'No', + 'label base field' => 'Yes', + 'status_base_field' => 'yes', + 'created_base_field' => 'yes', + 'changed_base_field' => 'yes', + 'author_base_field' => 'yes', + 'description_base_field' => 'no', + 'rest_configuration' => 'no', + ]; + $testCase->drush('generate', ['content-entity'], ['answer' => $answers, 'destination' => Path::join($testCase::webroot(), 'modules/contrib/unish_article')], null, null, $testCase::EXIT_SUCCESS, null, ['SHELL_INTERACTIVE' => 1]); + } +} \ No newline at end of file diff --git a/tests/unish/UnishIntegrationTestCase.php b/tests/unish/UnishIntegrationTestCase.php index 3b203bf424..771bc738f6 100644 --- a/tests/unish/UnishIntegrationTestCase.php +++ b/tests/unish/UnishIntegrationTestCase.php @@ -129,7 +129,7 @@ protected function buildCommandLine($command, $args, $options) if (!isset($value)) { $cmd[] = "--$key"; } else { - $cmd[] = "--$key=" . $value; + $cmd[] = "--$key=" . self::escapeshellarg($value); } } } @@ -157,7 +157,7 @@ protected function buildCommandLine($command, $args, $options) if (!isset($value) || $value === true) { $cmd[] = "--$key"; } else { - $cmd[] = "--$key=" . $value; + $cmd[] = "--$key=" . self::escapeshellarg($value); } } } From 82d26c3c4dec6e37e65e7803776645fd98835e17 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 2 Dec 2021 09:44:23 -0500 Subject: [PATCH 12/16] PHPCS --- tests/functional/FieldCreateTest.php | 4 ++-- tests/functional/resources/unish_article_bundles.php | 2 +- tests/unish/CreateEntityType.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/functional/FieldCreateTest.php b/tests/functional/FieldCreateTest.php index cf487e3e3d..6ca57c077d 100644 --- a/tests/functional/FieldCreateTest.php +++ b/tests/functional/FieldCreateTest.php @@ -25,11 +25,11 @@ public function testFieldCreate() // Arguments. $this->drush('field:create', [], [], null, null, self::EXIT_ERROR); $this->assertStringContainsString('Not enough arguments (missing: "entityType")', $this->getErrorOutputRaw()); - $this->drush('field:create', ['foo'], [], null, null,self::EXIT_ERROR); + $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->drush('field:create', ['user', 'user'], [], null, null,self::EXIT_ERROR); + $this->drush('field:create', ['user', 'user'], [], null, null, self::EXIT_ERROR); $this->assertStringNotContainsString('bundle', $this->getErrorOutputRaw()); // New field storage diff --git a/tests/functional/resources/unish_article_bundles.php b/tests/functional/resources/unish_article_bundles.php index 656f0b154a..3b686c6fd0 100644 --- a/tests/functional/resources/unish_article_bundles.php +++ b/tests/functional/resources/unish_article_bundles.php @@ -8,4 +8,4 @@ $storage->create([ 'id' => 'beta', 'label' => 'Beta', -])->save(); \ No newline at end of file +])->save(); diff --git a/tests/unish/CreateEntityType.php b/tests/unish/CreateEntityType.php index 4850b9a8f1..701e5e229e 100644 --- a/tests/unish/CreateEntityType.php +++ b/tests/unish/CreateEntityType.php @@ -43,4 +43,4 @@ public static function createContentEntity($testCase): void ]; $testCase->drush('generate', ['content-entity'], ['answer' => $answers, 'destination' => Path::join($testCase::webroot(), 'modules/contrib/unish_article')], null, null, $testCase::EXIT_SUCCESS, null, ['SHELL_INTERACTIVE' => 1]); } -} \ No newline at end of file +} From 47feef1b7795b8d5bf7c5966973b3dcbdc50d8f8 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 2 Dec 2021 10:27:30 -0500 Subject: [PATCH 13/16] Try getSimplifiedErrorOutput() --- tests/functional/FieldCreateTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/FieldCreateTest.php b/tests/functional/FieldCreateTest.php index 6ca57c077d..42f3596d87 100644 --- a/tests/functional/FieldCreateTest.php +++ b/tests/functional/FieldCreateTest.php @@ -48,7 +48,7 @@ public function testFieldCreate() $this->assertSame('unish_article', $settings['target_type']); $this->assertEquals(['beta' => 'beta'], $settings['handler_settings']['target_bundles']); $this->drush('field:create', ['unish_article', 'alpha'], ['field-name' => 'field_test3', 'field-label' => 'Body'], null, null, self::EXIT_ERROR); - $this->assertStringContainsString('Field storage with name \'field_test3\' already exists. Call this command with the --existing option to add an existing field to a bundle.', $this->getErrorOutputRaw()); + $this->assertStringContainsString('Field storage with name \'field_test3\' already exists. Call this command with the --existing option to add an existing field to a bundle.', $this->getSimplifiedErrorOutput()); // Existing storage $this->drush('field:create', ['unish_article', 'beta'], ['existing-field-name' => 'field_test3', 'field-label' => 'Body', 'field-widget' => 'text_textarea_with_summary']); From 3003fb4a2ef7423ad19d3ea7fee9686de9982500 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 2 Dec 2021 10:48:59 -0500 Subject: [PATCH 14/16] Fix UserCancelTest --- tests/functional/FieldCreateTest.php | 4 +++- tests/functional/UserTest.php | 1 + ...h_article_bundles.php => create_unish_article_bundles.php} | 0 tests/functional/resources/create_unish_articles.php | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) rename tests/functional/resources/{unish_article_bundles.php => create_unish_article_bundles.php} (100%) diff --git a/tests/functional/FieldCreateTest.php b/tests/functional/FieldCreateTest.php index 42f3596d87..f655b46ba9 100644 --- a/tests/functional/FieldCreateTest.php +++ b/tests/functional/FieldCreateTest.php @@ -2,6 +2,8 @@ namespace Unish; +use Webmozart\PathUtil\Path; + /** * @group commands */ @@ -16,7 +18,7 @@ public function setup(): void // Create a content entity with bundles. CreateEntityType::createContentEntity($this); $this->drush('pm-enable', ['text,field_ui,unish_article']); - $this->drush('php:script', ['tests/functional/resources/unish_article_bundles.php']); + $this->drush('php:script', ['create_unish_article_bundles'], ['script-path' => Path::join(__DIR__, 'resources')]); } } diff --git a/tests/functional/UserTest.php b/tests/functional/UserTest.php index 5e7c18ecf6..988c3b7176 100644 --- a/tests/functional/UserTest.php +++ b/tests/functional/UserTest.php @@ -117,6 +117,7 @@ public function testUserCancel() { CreateEntityType::createContentEntity($this); $this->drush('pm-enable', ['text,unish_article']); + $this->drush('php:script', ['create_unish_article_bundles'], ['script-path' => Path::join(__DIR__, 'resources')]); // Create one unish_article owned by our example user. $this->drush('php-script', ['create_unish_articles'], ['script-path' => Path::join(__DIR__, 'resources')]); // Verify that content entity exists. diff --git a/tests/functional/resources/unish_article_bundles.php b/tests/functional/resources/create_unish_article_bundles.php similarity index 100% rename from tests/functional/resources/unish_article_bundles.php rename to tests/functional/resources/create_unish_article_bundles.php diff --git a/tests/functional/resources/create_unish_articles.php b/tests/functional/resources/create_unish_articles.php index 374201c8ce..9f07424ab8 100644 --- a/tests/functional/resources/create_unish_articles.php +++ b/tests/functional/resources/create_unish_articles.php @@ -2,7 +2,8 @@ use Drupal\unish_article\Entity\UnishArticle; -$article = UnishArticle::create(); +/** @var \Drupal\Core\Entity\ContentEntityInterface $article */ +$article = UnishArticle::create(['bundle' => 'alpha']); $article->setOwnerId(2); // $article->setTitle('Unish wins.'); $article->save(); From fe2b8bcb87c22021b8841cd3ca2b0f175888e1e7 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 2 Dec 2021 11:01:43 -0500 Subject: [PATCH 15/16] Less string --- tests/functional/FieldCreateTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/FieldCreateTest.php b/tests/functional/FieldCreateTest.php index f655b46ba9..d2d1ed8fd2 100644 --- a/tests/functional/FieldCreateTest.php +++ b/tests/functional/FieldCreateTest.php @@ -50,7 +50,7 @@ public function testFieldCreate() $this->assertSame('unish_article', $settings['target_type']); $this->assertEquals(['beta' => 'beta'], $settings['handler_settings']['target_bundles']); $this->drush('field:create', ['unish_article', 'alpha'], ['field-name' => 'field_test3', 'field-label' => 'Body'], null, null, self::EXIT_ERROR); - $this->assertStringContainsString('Field storage with name \'field_test3\' already exists. Call this command with the --existing option to add an existing field to a bundle.', $this->getSimplifiedErrorOutput()); + $this->assertStringContainsString('--existing option', $this->getSimplifiedErrorOutput()); // Existing storage $this->drush('field:create', ['unish_article', 'beta'], ['existing-field-name' => 'field_test3', 'field-label' => 'Body', 'field-widget' => 'text_textarea_with_summary']); From e543c3b9547707cbfb1368bb54b17ba4ab07e2da Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Thu, 2 Dec 2021 11:56:47 -0500 Subject: [PATCH 16/16] Revert unneeded test case changes. --- tests/unish/UnishIntegrationTestCase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unish/UnishIntegrationTestCase.php b/tests/unish/UnishIntegrationTestCase.php index 771bc738f6..3b203bf424 100644 --- a/tests/unish/UnishIntegrationTestCase.php +++ b/tests/unish/UnishIntegrationTestCase.php @@ -129,7 +129,7 @@ protected function buildCommandLine($command, $args, $options) if (!isset($value)) { $cmd[] = "--$key"; } else { - $cmd[] = "--$key=" . self::escapeshellarg($value); + $cmd[] = "--$key=" . $value; } } } @@ -157,7 +157,7 @@ protected function buildCommandLine($command, $args, $options) if (!isset($value) || $value === true) { $cmd[] = "--$key"; } else { - $cmd[] = "--$key=" . self::escapeshellarg($value); + $cmd[] = "--$key=" . $value; } } }