Skip to content

Commit 4a68a18

Browse files
Merge pull request #58 from cristoforocervino/allow-non-compound-and-nested-forms
Allow non-compound forms and nested forms default values from command options
2 parents cd6f185 + 4ed4591 commit 4a68a18

13 files changed

+354
-47
lines changed

README.md

+69
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,75 @@ If you add the `--no-interaction` option when running the command, it will submi
120120

121121
If the submitted data is invalid the command will fail.
122122

123+
124+
## Using simpler forms with custom names
125+
126+
```php
127+
<?php
128+
129+
use Symfony\Component\Console\Command\Command;
130+
use Symfony\Component\Console\Input\InputInterface;
131+
use Symfony\Component\Console\Input\InputOption;
132+
use Symfony\Component\Console\Output\OutputInterface;
133+
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
134+
use Matthias\SymfonyConsoleForm\Console\Helper\FormHelper;
135+
136+
class TestCommand extends Command
137+
{
138+
protected function configure()
139+
{
140+
$this->setName('form:demo');
141+
$this->addOption('custom-option', null, InputOption::VALUE_OPTIONAL, 'Your custom option', 'option1')
142+
}
143+
144+
protected function execute(InputInterface $input, OutputInterface $output)
145+
{
146+
$formHelper = $this->getHelper('form');
147+
/** @var FormHelper $formHelper */
148+
149+
$formData = $formHelper->interactUsingNamedForm('custom-option', ChoiceType::class, $input, $output, [
150+
'label' => 'Your label',
151+
'help' => 'Additional information to help the interaction',
152+
'choices' => [
153+
'Default value label' => 'option1',
154+
'Another value Label' => 'option2',
155+
]
156+
]);
157+
158+
// $formData will be "option1" or "option2" and option "--custom-option" will be used as default value
159+
...
160+
}
161+
}
162+
```
163+
164+
## Nested Forms
165+
166+
If you have a complex compound form, you can define options and reference form children using square brackets:
167+
168+
```php
169+
<?php
170+
171+
use Symfony\Component\Console\Command\Command;
172+
use Symfony\Component\Console\Input\InputInterface;
173+
use Symfony\Component\Console\Output\OutputInterface;
174+
use Matthias\SymfonyConsoleForm\Console\Helper\FormHelper;
175+
176+
class TestCommand extends Command
177+
{
178+
protected function configure()
179+
{
180+
$this
181+
->addOption('user[username]', null, InputOption::VALUE_OPTIONAL)
182+
->addOption('user[email]', null, InputOption::VALUE_OPTIONAL)
183+
->addOption('user[address][street]', null, InputOption::VALUE_OPTIONAL)
184+
->addOption('user[address][city]', null, InputOption::VALUE_OPTIONAL)
185+
->addOption('acceptTerms', null, InputOption::VALUE_OPTIONAL)
186+
;
187+
}
188+
...
189+
}
190+
```
191+
123192
# TODO
124193

125194
- Maybe: provide a way to submit a form at once, possibly using a JSON-encoded array

features/interactive.feature

+21
Original file line numberDiff line numberDiff line change
@@ -378,3 +378,24 @@ Feature: It is possible to interactively fill in a form from the CLI
378378
[street] => foo
379379
)
380380
"""
381+
382+
Scenario: Non-compound form type in interactive mode
383+
When I run the command "form:non_compound_color" and I provide as input "blue" with parameters
384+
| Parameter | Value |
385+
| --color | yellow |
386+
Then the command has finished successfully
387+
And the output should contain
388+
"""
389+
Select color [yellow]:
390+
[red ] Red
391+
[blue ] Blue
392+
[yellow] Yellow
393+
>
394+
"""
395+
And the output should contain
396+
"""
397+
Array
398+
(
399+
[0] => blue
400+
)
401+
"""

features/non-interactive.feature

+46
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,49 @@ Feature: It is possible to interactively fill in a form from the CLI
6969
)
7070
)
7171
"""
72+
73+
Scenario: Non-compound form type in non-interactive mode
74+
When I run a command non-interactively with parameters
75+
| Parameter | Value |
76+
| command | form:non_compound_color |
77+
| --color | blue |
78+
Then the command has finished successfully
79+
And the output should contain
80+
"""
81+
Array
82+
(
83+
[0] => blue
84+
)
85+
"""
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-18
Original file line numberDiff line numberDiff line change
@@ -35,32 +35,27 @@ public function createForFormType(string $formType, array &$resources = []): Inp
3535

3636
$inputDefinition = new InputDefinition();
3737

38-
foreach ($form->all() as $name => $field) {
39-
if (!$this->isFormFieldSupported($field)) {
40-
continue;
41-
}
42-
43-
$type = InputOption::VALUE_REQUIRED;
44-
$default = $this->resolveDefaultValue($field);
45-
$description = FormUtil::label($field);
46-
47-
$inputDefinition->addOption(new InputOption($name, null, $type, $description, $default));
48-
}
38+
$this->addFormToInputDefinition($inputDefinition, $form);
4939

5040
return $inputDefinition;
5141
}
5242

53-
private function isFormFieldSupported(FormInterface $field): bool
43+
private function addFormToInputDefinition(InputDefinition $inputDefinition, FormInterface $form, ?string $name = null): void
5444
{
55-
if ($field->getConfig()->getCompound()) {
56-
if ($field->getConfig()->getType()->getInnerType() instanceof RepeatedType) {
57-
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);
5850
}
51+
} else {
52+
$name = $name ?? $form->getName();
53+
$type = InputOption::VALUE_REQUIRED;
54+
$default = $this->resolveDefaultValue($form);
55+
$description = FormUtil::label($form);
5956

60-
return false;
57+
$inputDefinition->addOption(new InputOption($name, null, $type, $description, $default));
6158
}
62-
63-
return true;
6459
}
6560

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

0 commit comments

Comments
 (0)