├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
└── src
├── Bridge
├── FormFactory
│ ├── ConsoleFormFactory.php
│ └── ConsoleFormWithDefaultValuesAndOptionsFactory.php
├── Interaction
│ ├── CollectionInteractor.php
│ ├── CompoundInteractor.php
│ ├── DelegatingInteractor.php
│ ├── Exception
│ │ ├── CanNotInteractWithForm.php
│ │ ├── FormNotReadyForInteraction.php
│ │ └── NoNeedToInteractWithForm.php
│ ├── FieldInteractor.php
│ ├── FieldWithNoInteractionInteractor.php
│ ├── FormInteractor.php
│ └── NonInteractiveRootInteractor.php
└── Transformer
│ ├── AbstractChoiceTransformer.php
│ ├── AbstractTextInputBasedTransformer.php
│ ├── AbstractTransformer.php
│ ├── CheckboxTransformer.php
│ ├── ChoiceTransformer.php
│ ├── DateTimeTransformer.php
│ ├── Exception
│ └── CouldNotResolveTransformer.php
│ ├── FormToQuestionTransformer.php
│ ├── IntegerTransformer.php
│ ├── NumberTransformer.php
│ ├── PasswordTransformer.php
│ ├── TextTransformer.php
│ ├── TransformerResolver.php
│ └── TypeAncestryBasedTransformerResolver.php
├── Bundle
├── RegisterHelpersPass.php
├── RegisterTransformersPass.php
├── SymfonyConsoleFormBundle.php
├── SymfonyConsoleFormExtension.php
├── helpers.yml
└── services.yml
├── Console
├── Command
│ ├── DynamicFormBasedCommand.php
│ ├── FormBasedCommand.php
│ ├── FormBasedCommandCapabilities.php
│ ├── FormBasedCommandWithDefault.php
│ └── InteractiveFormCommand.php
├── EventListener
│ ├── HandleFormBasedCommandEventListener.php
│ ├── RegisterHelpersEventListener.php
│ └── SetInputDefinitionOfFormBasedCommandEventListener.php
├── Formatter
│ └── Format.php
├── Helper
│ ├── FormHelper.php
│ ├── HelperCollection.php
│ └── Question
│ │ └── AlwaysReturnKeyOfChoiceQuestion.php
└── Input
│ ├── CachedInputDefinitionFactory.php
│ ├── FormBasedInputDefinitionFactory.php
│ └── InputDefinitionFactory.php
└── Form
├── ConsoleFormTypeExtension.php
├── EventListener
└── UseInputOptionsAsEventDataEventSubscriber.php
└── FormUtil.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v2.5.0
4 |
5 | - Support for 'translation_domain' option for labels (#27).
6 |
7 | ## v2.2.1
8 |
9 | - Forms now accept pre-filled data (#21).
10 | - You will be asked to fill in form fields until you have submitted valid data for all of them (#21).
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Matthias Noback
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Symfony Console Form
2 |
3 | By Matthias Noback
4 |
5 | This package contains a Symfony bundle and some tools which enable you to use Symfony Form types to define and
6 | interactively process user input from the CLI.
7 |
8 | # Installation
9 |
10 | composer require matthiasnoback/symfony-console-form
11 |
12 | Enable `Matthias\SymfonyConsoleForm\Bundle\SymfonyConsoleFormBundle` in the kernel of your Symfony application.
13 | ```php
14 | add(
49 | 'name',
50 | 'text',
51 | [
52 | 'label' => 'Your name',
53 | 'required' => true,
54 | 'data' => 'Matthias'
55 | ]
56 | )
57 | ...
58 | ;
59 | }
60 |
61 | public function setDefaultOptions(OptionsResolverInterface $resolver)
62 | {
63 | $resolver->setDefaults(['data_class' => 'Matthias\SymfonyConsoleForm\Tests\Data\Demo']);
64 | }
65 |
66 | public function getName()
67 | {
68 | return 'test';
69 | }
70 | }
71 | ```
72 |
73 | The corresponding `Demo` class looks like this:
74 |
75 | ```php
76 | setName('form:demo');
100 | }
101 |
102 | protected function execute(InputInterface $input, OutputInterface $output)
103 | {
104 | $formHelper = $this->getHelper('form');
105 | /** @var FormHelper $formHelper */
106 |
107 | $formData = $formHelper->interactUsingForm(DemoType::class, $input, $output);
108 |
109 | // $formData is the valid and populated form data object/array
110 | ...
111 | }
112 | }
113 | ```
114 |
115 | 
116 |
117 | When you provide command-line options with the names of the form fields, those values will be used as default values.
118 |
119 | If you add the `--no-interaction` option when running the command, it will submit the form using the input options you provided.
120 |
121 | If the submitted data is invalid the command will fail.
122 |
123 |
124 | ## Using simpler forms with custom names
125 |
126 | ```php
127 | 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 | 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 |
192 | # TODO
193 |
194 | - Maybe: provide a way to submit a form at once, possibly using a JSON-encoded array
195 | - Add more functional tests
196 | - Show form label of root form
197 | - Show nesting in form hierarchy using breadcrumbs
198 | - When these things have been provided, release this as a package (or multiple packages for stand-alone use)
199 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matthiasnoback/symfony-console-form",
3 | "type": "library",
4 | "description": "Use Symfony forms for Console command input",
5 | "keywords": ["Symfony", "console", "form"],
6 | "homepage": "http://github.com/matthiasnoback/symfony-console-form",
7 | "license": "MIT",
8 | "authors": [
9 | {
10 | "name": "Matthias Noback",
11 | "email": "matthiasnoback@gmail.com",
12 | "homepage": "http://php-and-symfony.matthiasnoback.nl"
13 | }
14 | ],
15 | "autoload": {
16 | "psr-4": {
17 | "Matthias\\SymfonyConsoleForm\\": "src/"
18 | }
19 | },
20 | "autoload-dev": {
21 | "psr-4": {
22 | "Matthias\\SymfonyConsoleForm\\Tests\\": "test/"
23 | }
24 | },
25 | "require": {
26 | "php": ">=8.0",
27 | "symfony/form": "~5.4 || ~6.0 || ~7.0",
28 | "symfony/console": "~5.4 || ~6.0 || ~7.0",
29 | "symfony/translation": "~5.4 || ~6.0 || ~7.0"
30 | },
31 | "require-dev": {
32 | "beberlei/assert": "~2.1",
33 | "behat/behat": "^3.6",
34 | "symfony/finder": "~5.4 || ~6.0 || ~7.0",
35 | "symfony/framework-bundle": "~5.4 || ~6.0 || ~7.0",
36 | "symfony/validator": "~5.4 || ~6.0 || ~7.0",
37 | "symfony/yaml": "~5.4 || ~6.0 || ~7.0",
38 | "symfony/security-csrf": "~5.4 || ~6.0 || ~7.0",
39 | "phpunit/phpunit": "^9.5",
40 | "symplify/easy-coding-standard": "^9.4",
41 | "symplify/coding-standard": "^9.4",
42 | "symfony/intl": "~5.4 || ~6.0 || ~7.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Bridge/FormFactory/ConsoleFormFactory.php:
--------------------------------------------------------------------------------
1 | formFactory = $formFactory;
29 | $this->formRegistry = $formRegistry;
30 | }
31 |
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 |
47 | public function create(string $formType, InputInterface $input, array $options = []): FormInterface
48 | {
49 | $options = $this->addDefaultOptions($options);
50 |
51 | $formBuilder = $this->formFactory->createBuilder($formType, null, $options);
52 |
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()) {
65 | /** @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();
76 | if (!$input->hasOption($name)) {
77 | return;
78 | }
79 |
80 | $providedValue = $input->getOption($name);
81 | if ($providedValue === null) {
82 | return;
83 | }
84 |
85 | $value = $providedValue;
86 | try {
87 | foreach ($formBuilder->getViewTransformers() as $viewTransformer) {
88 | $value = $viewTransformer->reverseTransform($value);
89 | }
90 | foreach ($formBuilder->getModelTransformers() as $modelTransformer) {
91 | $value = $modelTransformer->reverseTransform($value);
92 | }
93 | } catch (TransformationFailedException) {
94 | }
95 |
96 | $formBuilder->setData($value);
97 | }
98 | }
99 |
100 | private function addDefaultOptions(array $options): array
101 | {
102 | $defaultOptions = [];
103 | // hack to prevent validation error "The CSRF token is invalid."
104 | foreach ($this->formRegistry->getExtensions() as $extension) {
105 | foreach ($extension->getTypeExtensions(FormType::class) as $typeExtension) {
106 | if ($typeExtension instanceof FormTypeCsrfExtension) {
107 | $defaultOptions['csrf_protection'] = false;
108 | }
109 | }
110 | }
111 |
112 | return array_replace(
113 | $defaultOptions,
114 | $options
115 | );
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/Bridge/Interaction/CollectionInteractor.php:
--------------------------------------------------------------------------------
1 | formInteractor = $formInteractor;
31 | }
32 |
33 | /**
34 | * @throws CanNotInteractWithForm If the input isn't interactive
35 | * @throws FormNotReadyForInteraction If the "collection" form hasn't the option "allow_add"
36 | *
37 | * @return array
38 | */
39 | public function interactWith(
40 | FormInterface $form,
41 | HelperSet $helperSet,
42 | InputInterface $input,
43 | OutputInterface $output
44 | ) {
45 | if (!$input->isInteractive()) {
46 | throw new CanNotInteractWithForm('This interactor only works with interactive input');
47 | }
48 |
49 | if (!FormUtil::isTypeInAncestry($form, CollectionType::class)) {
50 | throw new CanNotInteractWithForm('Expected a "collection" form');
51 | }
52 |
53 | if (!$form->getConfig()->getOption('allow_add') && empty($form->getData())) {
54 | throw new FormNotReadyForInteraction(
55 | 'The "collection" form should have the option "allow_add" or have existing entries'
56 | );
57 | }
58 |
59 | $this->printHeader($form, $output);
60 |
61 | $submittedData = [];
62 | $prototype = $form->getConfig()->getAttribute('prototype');
63 | $originalData = $prototype->getData();
64 |
65 | $askIfEntryNeedsToBeSubmitted = function ($entryNumber) use ($helperSet, $input, $output) {
66 | return $this->askIfExistingEntryShouldBeAdded($helperSet, $input, $output, $entryNumber);
67 | };
68 |
69 | foreach ((array) $form->getData() as $key => $entryData) {
70 | $this->printEditEntryHeader($key, $output);
71 | $prototype->setData($entryData);
72 |
73 | $submittedEntry = $this->formInteractor->interactWith($prototype, $helperSet, $input, $output);
74 | if (!$form->getConfig()->getOption('allow_delete') || $askIfEntryNeedsToBeSubmitted($key)) {
75 | $submittedData[] = $submittedEntry;
76 | }
77 | }
78 |
79 | if ($form->getConfig()->getOption('allow_add')) {
80 | // reset the prototype
81 | $prototype->setData($originalData);
82 | $key = count($submittedData) - 1;
83 | while ($this->askIfContinueToAdd($helperSet, $input, $output)) {
84 | $this->printAddEntryHeader(++$key, $output);
85 | $submittedData[] = $this->formInteractor->interactWith($prototype, $helperSet, $input, $output);
86 | }
87 | }
88 |
89 | return $submittedData;
90 | }
91 |
92 | private function askIfContinueToAdd(
93 | HelperSet $helperSet,
94 | InputInterface $input,
95 | OutputInterface $output
96 | ): string {
97 | return $this->questionHelper($helperSet)->ask(
98 | $input,
99 | $output,
100 | new ConfirmationQuestion(
101 | Format::forQuestion('Add another entry to this collection?', 'n'),
102 | false
103 | )
104 | );
105 | }
106 |
107 | private function questionHelper(HelperSet $helperSet): QuestionHelper
108 | {
109 | $helper = $helperSet->get('question');
110 |
111 | if (!$helper instanceof QuestionHelper) {
112 | throw new RuntimeException('HelperSet does not contain valid QuestionHelper');
113 | }
114 |
115 | return $helper;
116 | }
117 |
118 | private function printHeader(FormInterface $form, OutputInterface $output): void
119 | {
120 | $output->writeln(
121 | strtr(
122 | '{label}',
123 | [
124 | '{label}' => FormUtil::label($form),
125 | ]
126 | )
127 | );
128 | }
129 |
130 | private function printEditEntryHeader(int $entryNumber, OutputInterface $output): void
131 | {
132 | $output->writeln(
133 | strtr(
134 | 'Edit entry {entryNumber}',
135 | [
136 | '{entryNumber}' => $entryNumber,
137 | ]
138 | )
139 | );
140 | }
141 |
142 | private function printAddEntryHeader(int $entryNumber, OutputInterface $output): void
143 | {
144 | $output->writeln(
145 | strtr(
146 | 'Add entry {entryNumber}',
147 | [
148 | '{entryNumber}' => $entryNumber,
149 | ]
150 | )
151 | );
152 | }
153 |
154 | private function askIfExistingEntryShouldBeAdded(
155 | HelperSet $helperSet,
156 | InputInterface $input,
157 | OutputInterface $output,
158 | int $entryNumber
159 | ): string {
160 | return $this->questionHelper($helperSet)->ask(
161 | $input,
162 | $output,
163 | new ConfirmationQuestion(
164 | Format::forQuestion(
165 | strtr(
166 | 'Add entry {entryNumber} to the submitted entries?',
167 | [
168 | '{entryNumber}' => $entryNumber,
169 | ]
170 | ),
171 | 'y'
172 | ),
173 | true
174 | )
175 | );
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/Bridge/Interaction/CompoundInteractor.php:
--------------------------------------------------------------------------------
1 | formInteractor = $formInteractor;
26 | }
27 |
28 | /**
29 | * @throws CanNotInteractWithForm If the input isn't interactive or a compound form
30 | *
31 | * @return array
32 | */
33 | public function interactWith(
34 | FormInterface $form,
35 | HelperSet $helperSet,
36 | InputInterface $input,
37 | OutputInterface $output
38 | ) {
39 | if (!$input->isInteractive()) {
40 | throw new CanNotInteractWithForm('This interactor only works with interactive input');
41 | }
42 |
43 | if (!FormUtil::isCompound($form)) {
44 | throw new CanNotInteractWithForm('Expected a compound form');
45 | }
46 |
47 | $submittedData = [];
48 |
49 | foreach ($form->all() as $name => $field) {
50 | try {
51 | $submittedData[$name] = $this->formInteractor->interactWith($field, $helperSet, $input, $output);
52 | } catch (NoNeedToInteractWithForm $exception) {
53 | continue;
54 | }
55 | }
56 |
57 | return $submittedData;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Bridge/Interaction/DelegatingInteractor.php:
--------------------------------------------------------------------------------
1 | delegates[] = $interactor;
21 | }
22 |
23 | /**
24 | * @throws CanNotInteractWithForm If no delegate was able to interact with this form
25 | *
26 | * @return mixed
27 | */
28 | public function interactWith(
29 | FormInterface $form,
30 | HelperSet $helperSet,
31 | InputInterface $input,
32 | OutputInterface $output
33 | ) {
34 | foreach ($this->delegates as $interactor) {
35 | try {
36 | return $interactor->interactWith($form, $helperSet, $input, $output);
37 | } catch (CanNotInteractWithForm $exception) {
38 | continue;
39 | }
40 | }
41 |
42 | throw new CanNotInteractWithForm('No delegate was able to interact with this form');
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Bridge/Interaction/Exception/CanNotInteractWithForm.php:
--------------------------------------------------------------------------------
1 | transformerResolver = $transformerResolver;
27 | }
28 |
29 | /**
30 | * @throws CanNotInteractWithForm The input isn't interactive
31 | *
32 | * @return mixed
33 | */
34 | public function interactWith(
35 | FormInterface $form,
36 | HelperSet $helperSet,
37 | InputInterface $input,
38 | OutputInterface $output
39 | ) {
40 | if (!$input->isInteractive()) {
41 | throw new CanNotInteractWithForm('This interactor only works with interactive input');
42 | }
43 |
44 | $question = $this->transformerResolver->resolve($form)->transform($form);
45 |
46 | return $this->questionHelper($helperSet)->ask($input, $output, $question);
47 | }
48 |
49 | private function questionHelper(HelperSet $helperSet): QuestionHelper
50 | {
51 | $helper = $helperSet->get('question');
52 |
53 | if (!$helper instanceof QuestionHelper) {
54 | throw new RuntimeException('HelperSet does not contain valid QuestionHelper');
55 | }
56 |
57 | return $helper;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Bridge/Interaction/FieldWithNoInteractionInteractor.php:
--------------------------------------------------------------------------------
1 | isRoot()) {
26 | throw new CanNotInteractWithForm('This interactor only works with root forms');
27 | }
28 |
29 | if ($input->isInteractive()) {
30 | throw new CanNotInteractWithForm('This interactor only works with non-interactive input');
31 | }
32 |
33 | /*
34 | * We need to adjust the input values for repeated types by copying the provided value to both of the
35 | * repeated fields.
36 | *
37 | * The fix was provided by @craigh
38 | *
39 | * P.S. If we need to add another fix like this, we should move this out to dedicated "input fixer" classes.
40 | */
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)) {
59 | $input->setOption($name, [
60 | $config->getOption('first_name') => $input->getOption($name),
61 | $config->getOption('second_name') => $input->getOption($name),
62 | ]);
63 | }
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Bridge/Transformer/AbstractChoiceTransformer.php:
--------------------------------------------------------------------------------
1 | getData();
15 | if (is_array($defaultValue)) {
16 | $defaultValue = implode(',', $defaultValue);
17 | }
18 |
19 | return $defaultValue;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Bridge/Transformer/AbstractTextInputBasedTransformer.php:
--------------------------------------------------------------------------------
1 | questionFrom($form), $this->defaultValueFrom($form));
15 | }
16 |
17 | protected function defaultValueFrom(FormInterface $form)
18 | {
19 | return $form->getViewData();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Bridge/Transformer/AbstractTransformer.php:
--------------------------------------------------------------------------------
1 | translator = $translator;
23 | }
24 |
25 | abstract protected function defaultValueFrom(FormInterface $form);
26 |
27 | protected function questionFrom(FormInterface $form): string
28 | {
29 | $translationDomain = $this->translationDomainFrom($form);
30 | $question = $this->translator->trans(FormUtil::label($form), [], $translationDomain);
31 | $help = FormUtil::help($form);
32 | if ($help !== null) {
33 | $help = $this->translator->trans($help, FormUtil::helpTranslationParameters($form), $translationDomain);
34 | }
35 |
36 | return $this->formattedQuestion($question, $this->defaultValueFrom($form), $help);
37 | }
38 |
39 | protected function translationDomainFrom(FormInterface $form): ?string
40 | {
41 | while ((null === $domain = $form->getConfig()->getOption('translation_domain')) && $form->getParent()) {
42 | $form = $form->getParent();
43 | }
44 |
45 | return $domain;
46 | }
47 |
48 | /**
49 | * @param mixed $defaultValue
50 | * @return string
51 | */
52 | protected function formattedQuestion(string $question, $defaultValue, ?string $help = null): string
53 | {
54 | return Format::forQuestion($question, $defaultValue, $help);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Bridge/Transformer/CheckboxTransformer.php:
--------------------------------------------------------------------------------
1 | questionFrom($form), $this->defaultValueFrom($form));
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Bridge/Transformer/ChoiceTransformer.php:
--------------------------------------------------------------------------------
1 | createView();
15 |
16 | $question = new AlwaysReturnKeyOfChoiceQuestion($this->questionFrom($form), $formView->vars['choices'], $this->defaultValueFrom($form));
17 |
18 | if ($form->getConfig()->getOption('multiple')) {
19 | $question->setMultiselect(true);
20 | }
21 |
22 | return $question;
23 | }
24 |
25 | protected function defaultValueFrom(FormInterface $form)
26 | {
27 | $defaultValue = parent::defaultValueFrom($form);
28 |
29 | /*
30 | * $defaultValue can be anything, since it's the form's (default) data. For the ChoiceType form type the default
31 | * value may be derived from the choice_label option, which transforms the data to a string. We look for the
32 | * choice matching the default data and return its calculated value.
33 | */
34 | $formView = $form->createView();
35 | foreach ($formView->vars['choices'] as $choiceView) {
36 | /** @var ChoiceView $choiceView */
37 | if ($choiceView->data == $defaultValue) {
38 | return $choiceView->value;
39 | }
40 | }
41 |
42 | return $defaultValue;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Bridge/Transformer/DateTimeTransformer.php:
--------------------------------------------------------------------------------
1 | getViewData();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Bridge/Transformer/Exception/CouldNotResolveTransformer.php:
--------------------------------------------------------------------------------
1 | setHidden(true);
14 |
15 | return $question;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Bridge/Transformer/TextTransformer.php:
--------------------------------------------------------------------------------
1 | transformers[$formType] = $transformer;
19 | }
20 |
21 | /**
22 | * @throws CouldNotResolveTransformer
23 | */
24 | public function resolve(FormInterface $form): FormToQuestionTransformer
25 | {
26 | $types = FormUtil::typeAncestry($form);
27 |
28 | foreach ($types as $type) {
29 | if (isset($this->transformers[$type])) {
30 | return $this->transformers[$type];
31 | }
32 | }
33 |
34 | throw new CouldNotResolveTransformer(
35 | sprintf(
36 | 'Could not find a transformer for any of these types (%s)',
37 | implode(', ', $types)
38 | )
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Bundle/RegisterHelpersPass.php:
--------------------------------------------------------------------------------
1 | helperCollectionId = $helperCollectionId;
25 | $this->tagName = $tagName;
26 | }
27 |
28 | /**
29 | * @throws LogicException
30 | */
31 | public function process(ContainerBuilder $container): void
32 | {
33 | if (!$container->has($this->helperCollectionId)) {
34 | throw new LogicException(
35 | sprintf(
36 | 'Helper collection service "%s" is not defined',
37 | $this->helperCollectionId
38 | )
39 | );
40 | }
41 |
42 | $helperCollection = $container->findDefinition($this->helperCollectionId);
43 |
44 | foreach ($container->findTaggedServiceIds($this->tagName) as $helperServiceId => $tags) {
45 | $helperCollection->addMethodCall('set', [new Reference($helperServiceId)]);
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Bundle/RegisterTransformersPass.php:
--------------------------------------------------------------------------------
1 | getDefinition('matthias_symfony_console.transformer_resolver');
14 | foreach ($container->findTaggedServiceIds('form_to_question_transformer') as $serviceId => $tags) {
15 | foreach ($tags as $tagAttributes) {
16 | $formType = $tagAttributes['form_type'];
17 | $formQuestionHelper->addMethodCall('addTransformer', [$formType, new Reference($serviceId)]);
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Bundle/SymfonyConsoleFormBundle.php:
--------------------------------------------------------------------------------
1 | addCompilerPass(new RegisterTransformersPass());
18 |
19 | $container->addCompilerPass(
20 | new RegisterHelpersPass(
21 | 'matthias_symfony_console_form.helper_collection',
22 | 'console_helper'
23 | )
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Bundle/SymfonyConsoleFormExtension.php:
--------------------------------------------------------------------------------
1 | load('services.yml');
16 | $loader->load('helpers.yml');
17 | }
18 |
19 | public function getAlias(): string
20 | {
21 | return 'symfony_console_form';
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Bundle/helpers.yml:
--------------------------------------------------------------------------------
1 | services:
2 | matthias_symfony_console_form.register_helpers_event_listener:
3 | class: Matthias\SymfonyConsoleForm\Console\EventListener\RegisterHelpersEventListener
4 | public: true
5 | arguments:
6 | - "@matthias_symfony_console_form.helper_collection"
7 | tags:
8 | - { name: kernel.event_listener, event: console.command, method: onConsoleCommand, priority: 1000 }
9 |
10 | matthias_symfony_console_form.helper_collection:
11 | class: Matthias\SymfonyConsoleForm\Console\Helper\HelperCollection
12 | public: false
13 |
--------------------------------------------------------------------------------
/src/Bundle/services.yml:
--------------------------------------------------------------------------------
1 | parameters:
2 | matthias_symfony_console_form.cache_directory: "%kernel.cache_dir%/matthias_symfony_console_form"
3 |
4 | services:
5 | console_form_helper:
6 | alias: matthias_symfony_console_form.form_helper
7 |
8 | matthias_symfony_console_form.form_helper:
9 | class: Matthias\SymfonyConsoleForm\Console\Helper\FormHelper
10 | public: false
11 | arguments:
12 | - "@matthias_symfony_console_form.console_form_factory"
13 | - "@matthias_symfony_console_form.delegating_interactor"
14 | tags:
15 | - { name: console_helper }
16 |
17 | matthias_symfony_console_form.input_definition_factory:
18 | class: Matthias\SymfonyConsoleForm\Console\Input\CachedInputDefinitionFactory
19 | public: false
20 | arguments:
21 | - "@matthias_symfony_console_form.real_input_definition_factory"
22 | - "%matthias_symfony_console_form.cache_directory%"
23 | - "%kernel.debug%"
24 |
25 | matthias_symfony_console_form.real_input_definition_factory:
26 | class: Matthias\SymfonyConsoleForm\Console\Input\FormBasedInputDefinitionFactory
27 | public: false
28 | arguments:
29 | - "@form.factory"
30 |
31 | matthias_symfony_console_form.abstract_transformer:
32 | abstract: true
33 | arguments: [ "@translator" ]
34 |
35 | matthias_symfony_console_form.text_transformer:
36 | class: Matthias\SymfonyConsoleForm\Bridge\Transformer\TextTransformer
37 | parent: matthias_symfony_console_form.abstract_transformer
38 | public: false
39 | tags:
40 | - { name: form_to_question_transformer, form_type: Symfony\Component\Form\Extension\Core\Type\TextType }
41 |
42 | matthias_symfony_console_form.integer_transformer:
43 | class: Matthias\SymfonyConsoleForm\Bridge\Transformer\IntegerTransformer
44 | parent: matthias_symfony_console_form.abstract_transformer
45 | public: false
46 | tags:
47 | - { name: form_to_question_transformer, form_type: Symfony\Component\Form\Extension\Core\Type\IntegerType }
48 |
49 | matthias_symfony_console_form.number_transformer:
50 | class: Matthias\SymfonyConsoleForm\Bridge\Transformer\NumberTransformer
51 | parent: matthias_symfony_console_form.abstract_transformer
52 | public: false
53 | tags:
54 | - { name: form_to_question_transformer, form_type: Symfony\Component\Form\Extension\Core\Type\NumberType }
55 |
56 | matthias_symfony_console_form.date_time_transformer:
57 | class: Matthias\SymfonyConsoleForm\Bridge\Transformer\DateTimeTransformer
58 | parent: matthias_symfony_console_form.abstract_transformer
59 | public: false
60 | tags:
61 | - { name: form_to_question_transformer, form_type: Symfony\Component\Form\Extension\Core\Type\TimeType }
62 | - { name: form_to_question_transformer, form_type: Symfony\Component\Form\Extension\Core\Type\DateType }
63 | - { name: form_to_question_transformer, form_type: Symfony\Component\Form\Extension\Core\Type\DateTimeType }
64 |
65 | matthias_symfony_console_form.password_transformer:
66 | class: Matthias\SymfonyConsoleForm\Bridge\Transformer\PasswordTransformer
67 | parent: matthias_symfony_console_form.abstract_transformer
68 | public: false
69 | tags:
70 | - { name: form_to_question_transformer, form_type: Symfony\Component\Form\Extension\Core\Type\PasswordType }
71 |
72 | matthias_symfony_console_form.choice_transformer:
73 | class: Matthias\SymfonyConsoleForm\Bridge\Transformer\ChoiceTransformer
74 | parent: matthias_symfony_console_form.abstract_transformer
75 | public: false
76 | tags:
77 | - { name: form_to_question_transformer, form_type: Symfony\Component\Form\Extension\Core\Type\ChoiceType }
78 | - { name: form_to_question_transformer, form_type: Symfony\Component\Form\Extension\Core\Type\CountryType }
79 |
80 | matthias_symfony_console_form.checkbox_transformer:
81 | class: Matthias\SymfonyConsoleForm\Bridge\Transformer\CheckboxTransformer
82 | parent: matthias_symfony_console_form.abstract_transformer
83 | public: false
84 | tags:
85 | - { name: form_to_question_transformer, form_type: Symfony\Component\Form\Extension\Core\Type\CheckboxType }
86 |
87 | matthias_symfony_console_form.delegating_interactor:
88 | class: Matthias\SymfonyConsoleForm\Bridge\Interaction\DelegatingInteractor
89 | public: false
90 | calls:
91 | # more specific interactors (by form type acenstry) should be higher in this list
92 | - [addInteractor, ["@matthias_symfony_console_form.field_with_no_interaction_interactor"]]
93 | - [addInteractor, ["@matthias_symfony_console_form.non_interactive_root_interactor"]]
94 | - [addInteractor, ["@matthias_symfony_console_form.collection_interactor"]]
95 | - [addInteractor, ["@matthias_symfony_console_form.compound_interactor"]]
96 | - [addInteractor, ["@matthias_symfony_console_form.field_interactor"]]
97 |
98 | matthias_symfony_console_form.compound_interactor:
99 | class: Matthias\SymfonyConsoleForm\Bridge\Interaction\CompoundInteractor
100 | public: false
101 | arguments:
102 | - "@matthias_symfony_console_form.delegating_interactor"
103 |
104 | matthias_symfony_console_form.field_with_no_interaction_interactor:
105 | class: Matthias\SymfonyConsoleForm\Bridge\Interaction\FieldWithNoInteractionInteractor
106 | public: false
107 |
108 | matthias_symfony_console_form.non_interactive_root_interactor:
109 | class: Matthias\SymfonyConsoleForm\Bridge\Interaction\NonInteractiveRootInteractor
110 | public: false
111 | arguments:
112 | - "@matthias_symfony_console_form.delegating_interactor"
113 |
114 | matthias_symfony_console_form.collection_interactor:
115 | class: Matthias\SymfonyConsoleForm\Bridge\Interaction\CollectionInteractor
116 | public: false
117 | arguments:
118 | - "@matthias_symfony_console_form.delegating_interactor"
119 |
120 | matthias_symfony_console_form.field_interactor:
121 | class: Matthias\SymfonyConsoleForm\Bridge\Interaction\FieldInteractor
122 | public: false
123 | arguments:
124 | - "@matthias_symfony_console.transformer_resolver"
125 |
126 | matthias_symfony_console.transformer_resolver:
127 | class: Matthias\SymfonyConsoleForm\Bridge\Transformer\TypeAncestryBasedTransformerResolver
128 | public: false
129 |
130 | matthias_symfony_console_form.handle_form_based_command_event_listener:
131 | class: Matthias\SymfonyConsoleForm\Console\EventListener\HandleFormBasedCommandEventListener
132 | public: true
133 | arguments:
134 | - "@matthias_symfony_console_form.form_helper"
135 | tags:
136 | - { name: kernel.event_listener, event: console.command, method: onConsoleCommand, priority: 200 }
137 |
138 | matthias_symfony_console_form.set_input_definition_of_form_based_command_event_listener:
139 | class: Matthias\SymfonyConsoleForm\Console\EventListener\SetInputDefinitionOfFormBasedCommandEventListener
140 | public: true
141 | arguments:
142 | - "@matthias_symfony_console_form.input_definition_factory"
143 | tags:
144 | - { name: kernel.event_listener, event: console.command, method: onConsoleCommand, priority: 2000 }
145 |
146 | matthias_symfony_console_form.console_form_type_extension:
147 | class: Matthias\SymfonyConsoleForm\Form\ConsoleFormTypeExtension
148 | public: true
149 | tags:
150 | - { name: form.type_extension, alias: form, extended_type: Symfony\Component\Form\Extension\Core\Type\FormType }
151 |
152 | matthias_symfony_console_form.console_form_factory:
153 | class: Matthias\SymfonyConsoleForm\Bridge\FormFactory\ConsoleFormWithDefaultValuesAndOptionsFactory
154 | public: false
155 | arguments:
156 | - "@form.factory"
157 | - "@form.registry"
158 |
--------------------------------------------------------------------------------
/src/Console/Command/DynamicFormBasedCommand.php:
--------------------------------------------------------------------------------
1 | formType = $formType;
20 | $this->commandName = $commandName;
21 |
22 | parent::__construct();
23 | }
24 |
25 | public function formType(): string
26 | {
27 | return $this->formType;
28 | }
29 |
30 | protected function configure(): void
31 | {
32 | $this->setName('form:' . $this->commandName);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Console/Command/FormBasedCommand.php:
--------------------------------------------------------------------------------
1 | formData = $data;
20 | }
21 |
22 | /**
23 | * @return mixed
24 | */
25 | protected function formData()
26 | {
27 | if ($this->formData === null) {
28 | if (!($this instanceof FormBasedCommand)) {
29 | throw new LogicException(
30 | 'Your command should be an instance of FormBasedCommand'
31 | );
32 | }
33 |
34 | throw new LogicException('For some reason, no form data was set');
35 | }
36 |
37 | return $this->formData;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Console/Command/FormBasedCommandWithDefault.php:
--------------------------------------------------------------------------------
1 | formQuestionHelper = $formQuestionHelper;
20 | }
21 |
22 | public function onConsoleCommand(ConsoleCommandEvent $event): void
23 | {
24 | $command = $event->getCommand();
25 | if (!($command instanceof FormBasedCommand)) {
26 | return;
27 | }
28 |
29 | $input = $event->getInput();
30 | $output = $event->getOutput();
31 | $defaultData = null;
32 | if ($command instanceof FormBasedCommandWithDefault) {
33 | $defaultData = $command->getFormDefault();
34 | }
35 |
36 | $formData = $this->formQuestionHelper->interactUsingForm(
37 | $command->formType(),
38 | $input,
39 | $output,
40 | [],
41 | $defaultData
42 | );
43 |
44 | $command->setFormData($formData);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Console/EventListener/RegisterHelpersEventListener.php:
--------------------------------------------------------------------------------
1 | helperCollection = $helperCollection;
19 | }
20 |
21 | public function onConsoleCommand(ConsoleCommandEvent $event): void
22 | {
23 | $command = $event->getCommand();
24 |
25 | if ($command === null) {
26 | throw new RuntimeException('Received ConsoleCommandEvent without Command instance!');
27 | }
28 |
29 | $helperSet = $command->getHelperSet();
30 |
31 | $this->helperCollection->addTo($helperSet);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Console/EventListener/SetInputDefinitionOfFormBasedCommandEventListener.php:
--------------------------------------------------------------------------------
1 | inputDefinitionFactory = $inputDefinitionFactory;
25 | }
26 |
27 | public function onConsoleCommand(ConsoleCommandEvent $event): void
28 | {
29 | $command = $event->getCommand();
30 | if ($command instanceof HelpCommand) {
31 | $command = $this->getCommandFromHelpCommand($command);
32 | }
33 |
34 | if (!($command instanceof FormBasedCommand)) {
35 | return;
36 | }
37 |
38 | $inputDefinition = $this->inputDefinitionFactory->createForFormType($command->formType());
39 | $this->setInputDefinition($command, $event->getInput(), $inputDefinition);
40 | }
41 |
42 | private function setInputDefinition(Command $command, InputInterface $input, InputDefinition $inputDefinition): void
43 | {
44 | $command->setDefinition($inputDefinition);
45 | $command->mergeApplicationDefinition();
46 | $input->bind($command->getDefinition());
47 | }
48 |
49 | private function getCommandFromHelpCommand(HelpCommand $helpCommand): ?Command
50 | {
51 | // hackish way of retrieving the command for which help was asked
52 | $reflectionObject = new ReflectionObject($helpCommand);
53 | $commandProperty = $reflectionObject->getProperty('command');
54 | $commandProperty->setAccessible(true);
55 |
56 | return $commandProperty->getValue($helpCommand);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Console/Formatter/Format.php:
--------------------------------------------------------------------------------
1 | (string)$defaultValue]
15 | ) : '';
16 | $help = $help !== null ? '' . $help . '' . "\n" : '';
17 | return strtr(
18 | '{help}{question}{default}: ',
19 | [
20 | '{help}' => $help,
21 | '{question}' => $question,
22 | '{default}' => (string)$default,
23 | ]
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Console/Helper/FormHelper.php:
--------------------------------------------------------------------------------
1 | formFactory = $formFactory;
26 | $this->formInteractor = $formInteractor;
27 | }
28 |
29 | public function getName(): string
30 | {
31 | return 'form';
32 | }
33 |
34 | /**
35 | * @param mixed $data
36 | *
37 | * @return mixed
38 | */
39 | public function interactUsingNamedForm(
40 | ?string $name,
41 | string $formType,
42 | InputInterface $input,
43 | OutputInterface $output,
44 | array $options = [],
45 | $data = null
46 | ) {
47 | $validFormFields = [];
48 |
49 | do {
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 | }
58 |
59 | // if we are rerunning the form for invalid data we don't need the fields that are already valid.
60 | foreach ($validFormFields as $validFormField) {
61 | $form->remove($validFormField);
62 | }
63 |
64 | $submittedData = $this->formInteractor->interactWith($form, $this->getHelperSet(), $input, $output);
65 |
66 | $form->submit($submittedData, false);
67 |
68 | // save the current data
69 | $data = $form->getData();
70 |
71 | if (!$form->isValid()) {
72 | $formErrors = $form->getErrors(true, false);
73 | $output->write(sprintf('Invalid data provided: %s', $formErrors));
74 | if ($this->noErrorsCanBeFixed($formErrors)) {
75 | $violationPaths = $this->constraintViolationPaths($formErrors);
76 | $hint = (count($violationPaths) > 0 ? ' (Violations on unused fields: ' . implode(', ', $violationPaths) . ')' : '');
77 | throw new RuntimeException(
78 | 'Errors out of the form\'s scope - do you have validation constraints on properties not used in the form?'
79 | . $hint
80 | );
81 | }
82 | array_map(
83 | function (FormInterface $formField) use (&$validFormFields) {
84 | if ($formField->isValid()) {
85 | $validFormFields[] = $formField->getName();
86 | }
87 | },
88 | $form->all()
89 | );
90 |
91 | if (!$input->isInteractive()) {
92 | throw new RuntimeException('There were form errors.');
93 | }
94 | }
95 | } while (!$form->isValid());
96 |
97 | return $data;
98 | }
99 |
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 |
115 | protected function noErrorsCanBeFixed(FormErrorIterator $errors): bool
116 | {
117 | // none of the remaining errors is related to a value of a form field
118 | return $errors->count() > 0 &&
119 | count(array_filter(iterator_to_array($errors), function ($error) {
120 | return $error instanceof FormErrorIterator;
121 | })) === 0;
122 | }
123 |
124 | protected function constraintViolationPaths(FormErrorIterator $errors): array
125 | {
126 | $paths = [];
127 | foreach ($errors as $error) {
128 | if (!$error instanceof FormError) {
129 | continue;
130 | }
131 | $cause = $error->getCause();
132 | if (!$cause instanceof ConstraintViolationInterface) {
133 | continue;
134 | }
135 | $paths[] = $cause->getPropertyPath();
136 | }
137 |
138 | return $paths;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/Console/Helper/HelperCollection.php:
--------------------------------------------------------------------------------
1 | helpers[] = $helper;
18 | }
19 |
20 | public function addTo(HelperSet $helperSet): void
21 | {
22 | foreach ($this->helpers as $helper) {
23 | $helperSet->set($helper);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Console/Helper/Question/AlwaysReturnKeyOfChoiceQuestion.php:
--------------------------------------------------------------------------------
1 | assertFlatChoiceViewsArray($choiceViews);
34 |
35 | $this->choiceViews = $choiceViews;
36 |
37 | parent::__construct($question, $this->prepareChoices(), $default);
38 |
39 | $this->setAutocompleterValues($this->prepareAutocompleteValues());
40 | }
41 |
42 | /**
43 | * @param bool $multiselect
44 | */
45 | public function setMultiselect(bool $multiselect): static
46 | {
47 | $this->_multiselect = $multiselect;
48 |
49 | return parent::setMultiselect($multiselect);
50 | }
51 |
52 | /**
53 | * @param string $errorMessage
54 | */
55 | public function setErrorMessage(string $errorMessage): static
56 | {
57 | $this->_errorMessage = $errorMessage;
58 |
59 | return parent::setErrorMessage($errorMessage);
60 | }
61 |
62 | /**
63 | * @return callable|null
64 | */
65 | public function getValidator(): ?callable
66 | {
67 | return function ($selected) {
68 | // Collapse all spaces.
69 | $selectedChoices = str_replace(' ', '', $selected);
70 |
71 | if ($this->_multiselect) {
72 | // Check for a separated comma values
73 | if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices)) {
74 | throw new InvalidArgumentException(sprintf($this->_errorMessage, $selected));
75 | }
76 | $selectedChoices = explode(',', $selectedChoices);
77 | } else {
78 | $selectedChoices = [$selected];
79 | }
80 |
81 | $selectedKeys = [];
82 |
83 | foreach ($selectedChoices as $selectedValue) {
84 | $selectedKeys[] = $this->resolveChoiceViewValue($selectedValue);
85 | }
86 |
87 | if ($this->_multiselect) {
88 | return $selectedKeys;
89 | }
90 |
91 | return current($selectedKeys);
92 | };
93 | }
94 |
95 | /**
96 | * @param string $selectedValue The selected value
97 | *
98 | * @throws InvalidArgumentException
99 | *
100 | * @return string The corresponding value of the ChoiceView
101 | */
102 | private function resolveChoiceViewValue($selectedValue)
103 | {
104 | foreach ($this->choiceViews as $choiceView) {
105 | if (in_array($selectedValue, [$choiceView->data, $choiceView->value, $choiceView->label])) {
106 | return $choiceView->value;
107 | }
108 | }
109 |
110 | throw new InvalidArgumentException(sprintf($this->_errorMessage, $selectedValue));
111 | }
112 |
113 | /**
114 | * @return array
115 | */
116 | private function prepareChoices()
117 | {
118 | $choices = [];
119 | foreach ($this->choiceViews as $choiceView) {
120 | $label = $choiceView->label;
121 | $data = $choiceView->data;
122 | if ($data != $choiceView->value && $this->canBeConvertedToString($data)) {
123 | $label .= ' (' . $data . ')';
124 | }
125 |
126 | $choices[$choiceView->value] = $label;
127 | }
128 |
129 | return $choices;
130 | }
131 |
132 | /**
133 | * @return array
134 | */
135 | private function prepareAutocompleteValues()
136 | {
137 | $autocompleteValues = [];
138 |
139 | foreach ($this->choiceViews as $choiceView) {
140 | $autocompleteValues[] = $choiceView->value;
141 |
142 | $data = $choiceView->data;
143 | if ($this->canBeConvertedToString($data)) {
144 | $autocompleteValues[] = (string)$data;
145 | }
146 |
147 | $autocompleteValues[] = $choiceView->label;
148 | }
149 |
150 | return $autocompleteValues;
151 | }
152 |
153 | /**
154 | * @param array $choiceViews
155 | *
156 | * @throws InvalidArgumentException
157 | */
158 | private function assertFlatChoiceViewsArray(array $choiceViews)
159 | {
160 | foreach ($choiceViews as $choiceView) {
161 | if (!$choiceView instanceof ChoiceView) {
162 | throw new InvalidArgumentException('Only a flat choice hierarchy is supported');
163 | }
164 | }
165 | }
166 |
167 | /**
168 | * @param mixed $value
169 | * @return bool
170 | */
171 | private function canBeConvertedToString($value)
172 | {
173 | return $value === null || is_scalar($value) || (\is_object($value) && method_exists($value, '__toString'));
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/Console/Input/CachedInputDefinitionFactory.php:
--------------------------------------------------------------------------------
1 | inputDefinitionFactory = $inputDefinitionFactory;
29 | $this->cacheDirectory = $cacheDirectory;
30 | $this->debug = $debug;
31 | }
32 |
33 | public function createForFormType(string $formType, array &$resources = []): InputDefinition
34 | {
35 | $cache = $this->configCacheFor($formType);
36 |
37 | if ($cache->isFresh()) {
38 | return $this->inputDefinitionFromCache($cache->getPath());
39 | }
40 |
41 | return $this->freshInputDefinition($formType, $cache, $resources);
42 | }
43 |
44 | protected function configCacheFor(string $formType): ConfigCache
45 | {
46 | return new ConfigCache($this->cacheDirectory . '/' . $formType, $this->debug);
47 | }
48 |
49 | /**
50 | * @param string $cacheFile
51 | */
52 | private function inputDefinitionFromCache(string $cacheFile): InputDefinition
53 | {
54 | $unserialized = unserialize(file_get_contents($cacheFile));
55 |
56 | if (!$unserialized instanceof InputDefinition) {
57 | throw new RuntimeException('Expected to get an object of type InputDefinition from the cache');
58 | }
59 |
60 | return $unserialized;
61 | }
62 |
63 | private function freshInputDefinition(string $formType, ConfigCache $cache, array &$resources): InputDefinition
64 | {
65 | $inputDefinition = $this->inputDefinitionFactory->createForFormType($formType, $resources);
66 | $cache->write(serialize($inputDefinition), $resources);
67 |
68 | return $inputDefinition;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Console/Input/FormBasedInputDefinitionFactory.php:
--------------------------------------------------------------------------------
1 | formFactory = $formFactory;
24 | }
25 |
26 | public function createForFormType(string $formType, array &$resources = []): InputDefinition
27 | {
28 | $resources[] = new FileResource(__FILE__);
29 |
30 | $form = $this->formFactory->create($formType);
31 |
32 | $actualFormType = $form->getConfig()->getType()->getInnerType();
33 | $reflection = new ReflectionObject($actualFormType);
34 | $resources[] = new FileResource($reflection->getFileName());
35 |
36 | $inputDefinition = new InputDefinition();
37 |
38 | $this->addFormToInputDefinition($inputDefinition, $form);
39 |
40 | return $inputDefinition;
41 | }
42 |
43 | private function addFormToInputDefinition(InputDefinition $inputDefinition, FormInterface $form, ?string $name = null): void
44 | {
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);
50 | }
51 | } else {
52 | $name = $name ?? $form->getName();
53 | $type = InputOption::VALUE_REQUIRED;
54 | $default = $this->resolveDefaultValue($form);
55 | $description = FormUtil::label($form);
56 |
57 | $inputDefinition->addOption(new InputOption($name, null, $type, $description, $default));
58 | }
59 | }
60 |
61 | private function resolveDefaultValue(FormInterface $field): string | bool | int | float | array | null
62 | {
63 | $default = $field->getConfig()->getOption('data', null);
64 |
65 | if (is_scalar($default) || is_null($default)) {
66 | return $default;
67 | }
68 |
69 | return null;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Console/Input/InputDefinitionFactory.php:
--------------------------------------------------------------------------------
1 | addEventSubscriber(new UseInputOptionsAsEventDataEventSubscriber());
15 | }
16 |
17 | public function getExtendedType(): string
18 | {
19 | return FormType::class;
20 | }
21 |
22 | public static function getExtendedTypes(): iterable
23 | {
24 | return [FormType::class];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Form/EventListener/UseInputOptionsAsEventDataEventSubscriber.php:
--------------------------------------------------------------------------------
1 | 'onPreSubmit',
18 | ];
19 | }
20 |
21 | public function onPreSubmit(FormEvent $event): void
22 | {
23 | $input = $event->getData();
24 | if (!($input instanceof InputInterface)) {
25 | return;
26 | }
27 |
28 | $event->setData($this->convertInputToSubmittedData($input, $event->getForm()));
29 | }
30 |
31 | private function convertInputToSubmittedData(InputInterface $input, FormInterface $form, ?string $name = null): mixed
32 | {
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 | $subValue = $this->convertInputToSubmittedData($input, $field, $subName);
43 | if ($subValue !== null) {
44 | $submittedData[$childName] = $subValue;
45 | }
46 | }
47 | }
48 | if (empty($submittedData)) {
49 | $submittedData = null;
50 | }
51 | } else {
52 | $name = $name ?? $form->getName();
53 | if ($input->hasOption($name)) {
54 | return $input->getOption($name);
55 | }
56 | }
57 |
58 | return $submittedData;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Form/FormUtil.php:
--------------------------------------------------------------------------------
1 | getConfig()->getType(), $types);
14 |
15 | return $types;
16 | }
17 |
18 | public static function typeAncestryForType(?ResolvedFormTypeInterface $formType, array &$types)
19 | {
20 | if (!($formType instanceof ResolvedFormTypeInterface)) {
21 | return;
22 | }
23 |
24 | $types[] = get_class($formType->getInnerType());
25 |
26 | self::typeAncestryForType($formType->getParent(), $types);
27 | }
28 |
29 | public static function isTypeInAncestry(FormInterface $form, string $type): bool
30 | {
31 | return in_array($type, self::typeAncestry($form));
32 | }
33 |
34 | public static function type(FormInterface $form): string
35 | {
36 | return get_class($form->getConfig()->getType()->getInnerType());
37 | }
38 |
39 | public static function label(FormInterface $form): string
40 | {
41 | $label = $form->getConfig()->getOption('label');
42 |
43 | if (!$label) {
44 | $label = self::humanize($form->getName());
45 | }
46 |
47 | return $label;
48 | }
49 |
50 | public static function help(FormInterface $form): ?string
51 | {
52 | return $form->getConfig()->getOption('help');
53 | }
54 |
55 | public static function helpTranslationParameters(FormInterface $form): array
56 | {
57 | return $form->getConfig()->getOption('help_translation_parameters');
58 | }
59 |
60 | public static function isCompound(FormInterface $form): bool
61 | {
62 | return $form->getConfig()->getCompound();
63 | }
64 |
65 | /**
66 | * Copied from Symfony\Component\Form method humanize.
67 | */
68 | private static function humanize($text): string
69 | {
70 | return ucfirst(trim(strtolower(preg_replace(array('/([A-Z])/', '/[_\s]+/'), array('_$1', ' '), $text))));
71 | }
72 | }
73 |
--------------------------------------------------------------------------------