Skip to content

Commit d06ebd6

Browse files
allow non-compound form and nested values
1 parent e649823 commit d06ebd6

10 files changed

+208
-56
lines changed

features/non-interactive.feature

+32
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,35 @@ Feature: It is possible to interactively fill in a form from the CLI
8383
[0] => blue
8484
)
8585
"""
86+
87+
Scenario: Nested form type in non-interactive mode
88+
When I run a command non-interactively with parameters
89+
| Parameter | Value |
90+
| command | form:nested |
91+
| --user[name] | mario |
92+
| --user[lastname] | rossi |
93+
| --user[password] | test |
94+
| --anotherUser[name] | luigi |
95+
| --anotherUser[lastname] | verdi |
96+
| --anotherUser[password] | test2 |
97+
| --color | blue |
98+
Then the command has finished successfully
99+
And the output should contain
100+
"""
101+
Array
102+
(
103+
[user] => Array
104+
(
105+
[name] => mario
106+
[lastname] => rossi
107+
[password] => test
108+
)
109+
[anotherUser] => Array
110+
(
111+
[name] => luigi
112+
[lastname] => verdi
113+
[password] => test2
114+
)
115+
[color] => blue
116+
)
117+
"""

src/Bridge/FormFactory/ConsoleFormFactory.php

+2
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
interface ConsoleFormFactory
99
{
1010
public function create(string $formType, InputInterface $input, array $options = []): FormInterface;
11+
12+
public function createNamed(string $name, string $formType, InputInterface $input, array $options = []): FormInterface;
1113
}

src/Bridge/FormFactory/ConsoleFormWithDefaultValuesAndOptionsFactory.php

+43-10
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
use Symfony\Component\Form\Exception\TransformationFailedException;
77
use Symfony\Component\Form\Extension\Core\Type\FormType;
88
use Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension;
9+
use Symfony\Component\Form\FormBuilderInterface;
910
use Symfony\Component\Form\FormFactoryInterface;
1011
use Symfony\Component\Form\FormInterface;
1112
use Symfony\Component\Form\FormRegistryInterface;
12-
use Symfony\Component\Form\Test\FormBuilderInterface;
1313

1414
final class ConsoleFormWithDefaultValuesAndOptionsFactory implements ConsoleFormFactory
1515
{
@@ -29,39 +29,72 @@ public function __construct(FormFactoryInterface $formFactory, FormRegistryInter
2929
$this->formRegistry = $formRegistry;
3030
}
3131

32+
public function createNamed(
33+
string $name,
34+
string $formType,
35+
InputInterface $input,
36+
array $options = []
37+
): FormInterface {
38+
$options = $this->addDefaultOptions($options);
39+
40+
$formBuilder = $this->formFactory->createNamedBuilder($name, $formType, null, $options);
41+
42+
$this->createChild($formBuilder, $input, $options);
43+
44+
return $formBuilder->getForm();
45+
}
46+
3247
public function create(string $formType, InputInterface $input, array $options = []): FormInterface
3348
{
3449
$options = $this->addDefaultOptions($options);
3550

3651
$formBuilder = $this->formFactory->createBuilder($formType, null, $options);
3752

38-
foreach ($formBuilder as $name => $childBuilder) {
53+
$this->createChild($formBuilder, $input, $options);
54+
55+
return $formBuilder->getForm();
56+
}
57+
58+
protected function createChild(
59+
FormBuilderInterface $formBuilder,
60+
InputInterface $input,
61+
array $options,
62+
?string $name = null
63+
): void {
64+
if ($formBuilder->getCompound()) {
3965
/** @var FormBuilderInterface $childBuilder */
66+
foreach ($formBuilder as $childName => $childBuilder) {
67+
$this->createChild(
68+
$childBuilder,
69+
$input,
70+
$options,
71+
$name === null ? $childName : $name . '[' . $childName . ']'
72+
);
73+
}
74+
} else {
75+
$name = $name ?? $formBuilder->getName();
4076
if (!$input->hasOption($name)) {
41-
continue;
77+
return;
4278
}
4379

4480
$providedValue = $input->getOption($name);
4581
if ($providedValue === null) {
46-
continue;
82+
return;
4783
}
4884

4985
$value = $providedValue;
50-
5186
try {
52-
foreach ($childBuilder->getViewTransformers() as $viewTransformer) {
87+
foreach ($formBuilder->getViewTransformers() as $viewTransformer) {
5388
$value = $viewTransformer->reverseTransform($value);
5489
}
55-
foreach ($childBuilder->getModelTransformers() as $modelTransformer) {
90+
foreach ($formBuilder->getModelTransformers() as $modelTransformer) {
5691
$value = $modelTransformer->reverseTransform($value);
5792
}
5893
} catch (TransformationFailedException) {
5994
}
6095

61-
$childBuilder->setData($value);
96+
$formBuilder->setData($value);
6297
}
63-
64-
return $formBuilder->getForm();
6598
}
6699

67100
private function addDefaultOptions(array $options): array

src/Bridge/Interaction/NonInteractiveRootInteractor.php

+20-10
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,36 @@ public function interactWith(
3131
}
3232

3333
/*
34-
* We need to adjust the input values for repeated types by copying the provided value to both of the repeated
35-
* fields. We only loop through the top-level fields, since there are no command options for deeper lying fields
36-
* anyway.
34+
* We need to adjust the input values for repeated types by copying the provided value to both of the
35+
* repeated fields.
3736
*
3837
* The fix was provided by @craigh
3938
*
4039
* P.S. If we need to add another fix like this, we should move this out to dedicated "input fixer" classes.
4140
*/
42-
foreach ($form->all() as $child) {
43-
$config = $child->getConfig();
44-
$name = $child->getName();
45-
if ($config->getType()->getInnerType() instanceof RepeatedType && $input->hasOption($name)) {
41+
$this->fixInputForField($input, $form);
42+
43+
// use the original input as the submitted data
44+
return $input;
45+
}
46+
47+
private function fixInputForField(InputInterface $input, FormInterface $form, ?string $name = null): void
48+
{
49+
$config = $form->getConfig();
50+
$isRepeatedField = $config->getType()->getInnerType() instanceof RepeatedType;
51+
if (!$isRepeatedField && $config->getCompound()) {
52+
foreach ($form->all() as $childName => $field) {
53+
$subName = $name === null ? $childName : $name . '[' . $childName . ']';
54+
$this->fixInputForField($input, $field, $subName);
55+
}
56+
} else {
57+
$name = $name ?? $form->getName();
58+
if ($isRepeatedField && $input->hasOption($name)) {
4659
$input->setOption($name, [
4760
$config->getOption('first_name') => $input->getOption($name),
4861
$config->getOption('second_name') => $input->getOption($name),
4962
]);
5063
}
5164
}
52-
53-
// use the original input as the submitted data
54-
return $input;
5565
}
5666
}

src/Console/Helper/FormHelper.php

+25-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ public function getName(): string
3636
*
3737
* @return mixed
3838
*/
39-
public function interactUsingForm(
39+
public function interactUsingNamedForm(
40+
?string $name,
4041
string $formType,
4142
InputInterface $input,
4243
OutputInterface $output,
@@ -46,8 +47,14 @@ public function interactUsingForm(
4647
$validFormFields = [];
4748

4849
do {
49-
$form = $this->formFactory->create($formType, $input, $options);
50-
$form->setData($data);
50+
if ($name === null) {
51+
$form = $this->formFactory->create($formType, $input, $options);
52+
} else {
53+
$form = $this->formFactory->createNamed($name, $formType, $input, $options);
54+
}
55+
if ($data !== null) {
56+
$form->setData($data);
57+
}
5158

5259
// if we are rerunning the form for invalid data we don't need the fields that are already valid.
5360
foreach ($validFormFields as $validFormField) {
@@ -90,6 +97,21 @@ function (FormInterface $formField) use (&$validFormFields) {
9097
return $data;
9198
}
9299

100+
/**
101+
* @param mixed $data
102+
*
103+
* @return mixed
104+
*/
105+
public function interactUsingForm(
106+
string $formType,
107+
InputInterface $input,
108+
OutputInterface $output,
109+
array $options = [],
110+
$data = null
111+
) {
112+
return $this->interactUsingNamedForm(null, $formType, $input, $output, $options, $data);
113+
}
114+
93115
protected function noErrorsCanBeFixed(FormErrorIterator $errors): bool
94116
{
95117
// none of the remaining errors is related to a value of a form field

src/Console/Input/FormBasedInputDefinitionFactory.php

+13-27
Original file line numberDiff line numberDiff line change
@@ -35,41 +35,27 @@ public function createForFormType(string $formType, array &$resources = []): Inp
3535

3636
$inputDefinition = new InputDefinition();
3737

38-
if (!$form->getConfig()->getCompound()) {
39-
$this->addFormToInputDefinition($form->getName(), $form, $inputDefinition);
40-
}
41-
42-
foreach ($form->all() as $name => $field) {
43-
$this->addFormToInputDefinition($name, $field, $inputDefinition);
44-
}
38+
$this->addFormToInputDefinition($inputDefinition, $form);
4539

4640
return $inputDefinition;
4741
}
4842

49-
private function addFormToInputDefinition(string $name, FormInterface $form, InputDefinition $inputDefinition): void
50-
{
51-
if (!$this->isFormFieldSupported($form)) {
52-
return;
53-
}
54-
55-
$type = InputOption::VALUE_REQUIRED;
56-
$default = $this->resolveDefaultValue($form);
57-
$description = FormUtil::label($form);
58-
59-
$inputDefinition->addOption(new InputOption($name, null, $type, $description, $default));
60-
}
61-
62-
private function isFormFieldSupported(FormInterface $field): bool
43+
private function addFormToInputDefinition(InputDefinition $inputDefinition, FormInterface $form, ?string $name = null): void
6344
{
64-
if ($field->getConfig()->getCompound()) {
65-
if ($field->getConfig()->getType()->getInnerType() instanceof RepeatedType) {
66-
return true;
45+
$repeatedField = $form->getConfig()->getType()->getInnerType() instanceof RepeatedType;
46+
if (!$repeatedField && $form->getConfig()->getCompound()) {
47+
foreach ($form->all() as $childName => $field) {
48+
$subName = $name === null ? $childName : $name . '[' . $childName . ']';
49+
$this->addFormToInputDefinition($inputDefinition, $field, $subName);
6750
}
51+
} else {
52+
$name = $name ?? $form->getName();
53+
$type = InputOption::VALUE_REQUIRED;
54+
$default = $this->resolveDefaultValue($form);
55+
$description = FormUtil::label($form);
6856

69-
return false;
57+
$inputDefinition->addOption(new InputOption($name, null, $type, $description, $default));
7058
}
71-
72-
return true;
7359
}
7460

7561
private function resolveDefaultValue(FormInterface $field): string | bool | int | float | array | null

src/Form/EventListener/UseInputOptionsAsEventDataEventSubscriber.php

+17-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Symfony\Component\Console\Input\InputInterface;
66
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
7+
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
78
use Symfony\Component\Form\FormEvent;
89
use Symfony\Component\Form\FormEvents;
910
use Symfony\Component\Form\FormInterface;
@@ -27,14 +28,24 @@ public function onPreSubmit(FormEvent $event): void
2728
$event->setData($this->convertInputToSubmittedData($input, $event->getForm()));
2829
}
2930

30-
private function convertInputToSubmittedData(InputInterface $input, FormInterface $form): array
31+
private function convertInputToSubmittedData(InputInterface $input, FormInterface $form, ?string $name = null): mixed
3132
{
32-
$submittedData = [];
33-
34-
// we don't need to do this recursively, since command options are one-dimensional (or are they?)
35-
foreach ($form->all() as $name => $field) {
33+
$submittedData = null;
34+
if ($form->getConfig()->getCompound()) {
35+
$submittedData = [];
36+
$repeatedField = $form->getConfig()->getType()->getInnerType() instanceof RepeatedType;
37+
foreach ($form->all() as $childName => $field) {
38+
if ($repeatedField) {
39+
$submittedData = $this->convertInputToSubmittedData($input, $field, $name ?? $form->getName());
40+
} else {
41+
$subName = $name === null ? $childName : $name . '[' . $childName . ']';
42+
$submittedData[$childName] = $this->convertInputToSubmittedData($input, $field, $subName);
43+
}
44+
}
45+
} else {
46+
$name = $name ?? $form->getName();
3647
if ($input->hasOption($name)) {
37-
$submittedData[$name] = $input->getOption($name);
48+
return $input->getOption($name);
3849
}
3950
}
4051

test/Form/NestedType.php

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Matthias\SymfonyConsoleForm\Tests\Form;
6+
7+
use Symfony\Component\Form\AbstractType;
8+
use Symfony\Component\Form\FormBuilderInterface;
9+
10+
class NestedType extends AbstractType
11+
{
12+
public function buildForm(FormBuilderInterface $builder, array $options)
13+
{
14+
$builder
15+
->add('user', UserType::class)
16+
->add('anotherUser', UserType::class)
17+
->add('color', NonCompoundColorType::class);
18+
}
19+
}

test/Form/UserType.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Matthias\SymfonyConsoleForm\Tests\Form;
6+
7+
use Symfony\Component\Form\AbstractType;
8+
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
9+
use Symfony\Component\Form\Extension\Core\Type\DateType;
10+
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
11+
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
12+
use Symfony\Component\Form\Extension\Core\Type\TextType;
13+
use Symfony\Component\Form\FormBuilderInterface;
14+
15+
class UserType extends AbstractType
16+
{
17+
public function buildForm(FormBuilderInterface $builder, array $options)
18+
{
19+
$builder
20+
->add('name', TextType::class)
21+
->add('lastname', TextType::class)
22+
->add('password', RepeatedType::class, [
23+
'type' => PasswordType::class,
24+
'invalid_message' => 'The password fields must match.',
25+
'first_options' => ['label' => 'Admin Password'],
26+
'second_options' => ['label' => 'Repeat Password'],
27+
]);
28+
}
29+
}

test/config.yml

+8
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ services:
146146
tags:
147147
- { name: console.command }
148148

149+
nested_command:
150+
class: Matthias\SymfonyConsoleForm\Tests\Command\PrintFormDataCommand
151+
arguments:
152+
- Matthias\SymfonyConsoleForm\Tests\Form\NestedType
153+
- nested
154+
tags:
155+
- { name: console.command }
156+
149157
framework:
150158
form:
151159
csrf_protection: true

0 commit comments

Comments
 (0)