
| Current Path : /var/www/html/konvbav/vendor/drush/drush/src/Commands/field/ |
Linux ift1.ift-informatik.de 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64 |
| Current File : /var/www/html/konvbav/vendor/drush/drush/src/Commands/field/FieldCreateCommands.php |
<?php
declare(strict_types=1);
namespace Drush\Commands\field;
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\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\WidgetPluginManager;
use Drupal\Core\Url;
use Drupal\field\FieldConfigInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\DependencyInjection\ContainerInterface;
use function dt;
use function t;
class FieldCreateCommands extends DrushCommands implements CustomEventAwareInterface
{
use EntityTypeBundleAskTrait;
use CustomEventAwareTrait;
use EntityTypeBundleValidationTrait;
const CREATE = 'field:create';
protected ContentTranslationManagerInterface $contentTranslationManager;
public function __construct(
protected FieldTypePluginManagerInterface $fieldTypePluginManager,
protected WidgetPluginManager $widgetPluginManager,
protected SelectionPluginManagerInterface $selectionPluginManager,
protected EntityTypeManagerInterface $entityTypeManager,
protected EntityTypeBundleInfoInterface $entityTypeBundleInfo,
protected ModuleHandlerInterface $moduleHandler,
protected EntityFieldManagerInterface $entityFieldManager
) {
}
public static function create(ContainerInterface $container): self
{
$commandHandler = new static(
$container->get('plugin.manager.field.field_type'),
$container->get('plugin.manager.field.widget'),
$container->get('plugin.manager.entity_reference_selection'),
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
$container->get('module_handler'),
$container->get('entity_field.manager')
);
if ($container->has('content_translation.manager')) {
$commandHandler->setContentTranslationManager($container->get('content_translation.manager'));
}
return $commandHandler;
}
public function setContentTranslationManager(ContentTranslationManagerInterface $manager = null): void
{
if ($manager) {
$this->contentTranslationManager = $manager;
}
}
/**
* Create a new field
*
* @see \Drupal\field_ui\Form\FieldConfigEditForm
* @see \Drupal\field_ui\Form\FieldStorageConfigEditForm
*/
#[CLI\Command(name: self::CREATE, aliases: ['field-create', 'fc'])]
#[CLI\Argument(name: 'entityType', description: 'The machine name of the entity type')]
#[CLI\Argument(name: 'bundle', description: 'The machine name of the bundle')]
#[CLI\Option(name: 'field-name', description: 'A unique machine-readable name containing letters, numbers, and underscores.')]
#[CLI\Option(name: 'field-label', description: 'The field label')]
#[CLI\Option(name: 'field-description', description: 'Instructions to present to the user below this field on the editing form.')]
#[CLI\Option(name: 'field-type', description: 'The field type')]
#[CLI\Option(name: 'field-widget', description: 'The field widget')]
#[CLI\Option(name: 'is-required', description: 'Whether the field is required')]
#[CLI\Option(name: 'is-translatable', description: 'Whether the field is translatable')]
#[CLI\Option(name: 'cardinality', description: 'The allowed number of values')]
#[CLI\Option(name: 'target-type', description: 'The target entity type. Only necessary for entity reference fields.')]
#[CLI\Option(name: 'target-bundle', description: 'The target bundle(s). Only necessary for entity reference fields.')]
#[CLI\Option(name: 'existing', description: 'Re-use an existing field.')]
#[CLI\Option(name: 'existing-field-name', description: 'The name of an existing field you want to re-use. Only used in non-interactive context.')]
#[CLI\Option(name: 'show-machine-names', description: 'Show machine names instead of labels in option lists.')]
#[CLI\Usage(name: self::CREATE, description: 'Create a field by answering the prompts.')]
#[CLI\Usage(name: 'field-create taxonomy_term tag', description: 'Create a field and fill in the remaining information through prompts.')]
#[CLI\Usage(name: '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', description: 'Create a field in a completely non-interactive way.')]
#[CLI\Complete(method_name_or_callable: 'complete')]
#[CLI\Version(version: '11.0')]
public function fieldCreate(?string $entityType = null, ?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_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-field-name' => InputOption::VALUE_OPTIONAL,
'existing' => false,
]): void
{
$this->input->setArgument('entityType', $entityType = $entityType ?? $this->askEntityType());
$this->validateEntityType($entityType);
$this->input->setArgument('bundle', $bundle = $bundle ?? $this->askBundle());
$this->validateBundle($entityType, $bundle);
if ($this->input->getOption('existing') || $this->input->getOption('existing-field-name')) {
$this->ensureOption('existing-field-name', [$this, 'askExistingFieldName'], false);
if (!$fieldName = $this->input->getOption('existing-field-name')) {
throw new \InvalidArgumentException(
dt('There are no existing fields that can be added.')
);
}
if (!$this->fieldStorageExists($fieldName, $entityType)) {
throw new \InvalidArgumentException(
dt("Field storage with name '!fieldName' does not yet exist. Call this command without the --existing option first.", [
'!fieldName' => $fieldName,
])
);
}
$fieldStorage = $this->entityFieldManager->getFieldStorageDefinitions($entityType)[$fieldName];
if ($this->fieldExists($fieldName, $entityType, $bundle)) {
throw new \InvalidArgumentException(
dt("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'], true);
$this->ensureOption('field-description', [$this, 'askFieldDescription'], false);
$this->ensureOption('field-widget', [$this, 'askFieldWidget'], false);
$this->ensureOption('is-required', [$this, 'askRequired'], false);
$this->ensureOption('is-translatable', [$this, 'askTranslatable'], false);
} else {
$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(
dt("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'], false);
$this->ensureOption('field-type', [$this, 'askFieldType'], true);
$this->ensureOption('field-widget', [$this, 'askFieldWidget'], false);
$this->ensureOption('is-required', [$this, 'askRequired'], false);
$this->ensureOption('is-translatable', [$this, 'askTranslatable'], false);
$this->ensureOption('cardinality', [$this, 'askCardinality'], true);
$this->createFieldStorage();
}
// 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);
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->getCompletionType() === CompletionInput::TYPE_ARGUMENT_VALUE) {
if ($input->getCompletionName() === 'entityType') {
$suggestions->suggestValues(array_keys($this->getFieldableEntityTypes()));
}
if ($input->getCompletionName() === 'bundle') {
$entityTypeId = $input->getArgument('entityType');
$bundleInfo = $this->entityTypeBundleInfo->getBundleInfo($entityTypeId);
$suggestions->suggestValues(array_keys($bundleInfo));
}
}
if ($input->getCompletionType() === CompletionInput::TYPE_OPTION_VALUE) {
if ($input->getCompletionName() === 'existing-field-name') {
$entityTypeId = $input->getArgument('entityType');
$bundle = $input->getArgument('bundle');
$showMachineNames = (bool) $input->getOption('show-machine-names');
if ($entityTypeId && $bundle) {
$choices = $this->getExistingFieldStorageOptions($entityTypeId, $bundle, $showMachineNames);
$suggestions->suggestValues(array_keys($choices));
}
}
if ($input->getCompletionName() === 'field-name') {
$fieldLabel = $input->getOption('field-label');
$bundle = $input->getArgument('bundle');
if ($fieldLabel && $bundle) {
$suggestion = $this->generateFieldName($fieldLabel, $bundle);
$suggestions->suggestValue($suggestion);
}
}
if ($input->getCompletionName() === 'field-type') {
$fieldTypes = $this->fieldTypePluginManager->getDefinitions();
$suggestions->suggestValues(array_keys($fieldTypes));
}
if ($input->getCompletionName() === 'field-widget') {
$fieldType = $input->getOption('field-type');
if ($fieldType) {
$fieldWidgets = $this->widgetPluginManager->getOptions($fieldType);
$suggestions->suggestValues(array_keys($fieldWidgets));
}
}
}
}
protected function askExistingFieldName(): ?string
{
$entityType = $this->input->getArgument('entityType');
$bundle = $this->input->getArgument('bundle');
$showMachineNames = (bool) $this->input->getOption('show-machine-names');
$choices = $this->getExistingFieldStorageOptions($entityType, $bundle, $showMachineNames);
if (empty($choices)) {
return null;
}
return $this->io()->choice('Choose an existing field', $choices);
}
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, $bundle);
}
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(): string
{
return $this->io()->askRequired('Field label');
}
protected function askFieldDescription(): ?string
{
return $this->io()->ask('Field description');
}
protected function askFieldType(): string
{
$definitions = $this->fieldTypePluginManager->getDefinitions();
$choices = [];
foreach ($definitions as $definition) {
if (isset($definition['no_ui']) && $definition['no_ui'] === true) {
continue;
}
$label = $this->input->getOption('show-machine-names') ? $definition['id'] : $definition['label']->render();
$choices[$definition['id']] = $label;
}
return $this->io()->choice('Field type', $choices);
}
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);
if ($widgets === []) {
$this->io()->comment('No widgets available for this field type. Skipping option.');
return null;
}
foreach ($widgets as $name => $label) {
$label = $this->input->getOption('show-machine-names') ? $name : $label->render();
$choices[$name] = $label;
}
$default = $this->input->getOption('show-machine-names') ? key($choices) : current($choices);
return $this->io()->choice('Field widget', $choices, $default);
}
protected function askRequired(): bool
{
return $this->io()->confirm('Required', false);
}
protected function askTranslatable(): bool
{
if (!$this->hasContentTranslation()) {
return false;
}
return $this->io()->confirm('Translatable', false);
}
protected function askCardinality(): int
{
$fieldType = $this->input->getOption('field-type');
$definition = $this->fieldTypePluginManager->getDefinition($fieldType);
// Some field types choose to enforce a fixed cardinality.
if (isset($definition['cardinality'])) {
return $definition['cardinality'];
}
$choices = ['Limited', 'Unlimited'];
$cardinality = $this->io()->choice(
'Allowed number of values',
array_combine($choices, $choices),
0
);
$limit = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
while ($cardinality === 'Limited' && $limit < 1) {
$limit = (int) $this->io()->ask('Allowed number of values', '1');
}
return $limit;
}
protected function createField(): FieldConfigInterface
{
$values = [
'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'),
];
// Command files may customize $values as desired.
$handlers = $this->getCustomEventHandlers('field-create-field-config');
foreach ($handlers as $handler) {
$values = $handler($values, $this->input);
}
$field = $this->entityTypeManager
->getStorage('field_config')
->create($values);
$field->save();
return $field;
}
protected function createFieldStorage(): FieldStorageConfigInterface
{
$values = [
'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,
];
// Command files may customize $values as desired.
$handlers = $this->getCustomEventHandlers('field-create-field-storage');
foreach ($handlers as $handler) {
$values = $handler($values, $this->input);
}
/** @var FieldStorageConfigInterface $fieldStorage */
$fieldStorage = $this->entityTypeManager
->getStorage('field_storage_config')
->create($values);
$fieldStorage->save();
return $fieldStorage;
}
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 && $context === 'form') {
$values['type'] = $fieldWidget;
}
// Command files may customize $values as desired.
$handlers = $this->getCustomEventHandlers(sprintf('field-create-%s-display', $context));
foreach ($handlers as $handler) {
$handler($values);
}
$storage = $this->getEntityDisplay($context);
if (!$storage instanceof EntityDisplayInterface) {
$this->logger()->info(
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' => sprintf('%s.%s.default', $entityType, $bundle),
'targetEntityType' => $entityType,
'bundle' => $bundle,
'mode' => 'default',
'status' => true,
]);
$storage->save();
}
$storage->setComponent($fieldName, $values)->save();
}
protected function getEntityDisplay(string $context): ?EntityDisplayInterface
{
$entityType = $this->input->getArgument('entityType');
$bundle = $this->input->getArgument('bundle');
return $this->entityTypeManager
->getStorage(sprintf('entity_%s_display', $context))
->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'",
$field->get('field_name'),
$field->get('entity_type'),
$field->get('bundle')
)
);
/** @var EntityTypeInterface $entityType */
$entityType = $this->entityTypeManager->getDefinition($field->get('entity_type'));
$routeName = sprintf('entity.field_config.%s_field_edit_form', $entityType->id());
$routeParams = [
'field_config' => $field->id(),
$entityType->getBundleEntityType() => $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, string $bundle): string
{
// 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_%s', $bundle, $machineName);
// Maximum 32 characters
$machineName = substr($machineName, 0, 32);
return $machineName;
}
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 getExistingFieldStorageOptions(string $entityType, string $bundle, bool $showMachineNames): array
{
$fieldTypes = $this->fieldTypePluginManager->getDefinitions();
$options = [];
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 = $showMachineNames
? $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;
}
protected function hasContentTranslation(): bool
{
$entityType = $this->input->getArgument('entityType');
$bundle = $this->input->getArgument('bundle');
return $this->moduleHandler->moduleExists('content_translation')
&& $this->contentTranslationManager->isEnabled($entityType, $bundle);
}
protected function ensureOption(string $name, callable $asker, bool $required): void
{
$value = $this->input->getOption($name);
if ($value === null && $this->input->isInteractive()) {
$value = $asker();
}
if ($required && $value === null) {
throw new \InvalidArgumentException(dt('The !optionName option is required.', [
'!optionName' => $name,
]));
}
$this->input->setOption($name, $value);
}
}