· last year · Mar 09, 2025, 07:41 PM
1
2------------------------------------------------------------------------------------------------------------------------
3 ./AbstractRendererEngine.php
4------------------------------------------------------------------------------------------------------------------------
5<?php
6
7/*
8 * This file is part of the Symfony package.
9 *
10 * (c) Fabien Potencier <fabien@symfony.com>
11 *
12 * For the full copyright and license information, please view the LICENSE
13 * file that was distributed with this source code.
14 */
15
16namespace Symfony\Component\Form;
17
18use Symfony\Contracts\Service\ResetInterface;
19
20/**
21 * Default implementation of {@link FormRendererEngineInterface}.
22 *
23 * @author Bernhard Schussek <bschussek@gmail.com>
24 */
25abstract class AbstractRendererEngine implements FormRendererEngineInterface, ResetInterface
26{
27 /**
28 * The variable in {@link FormView} used as cache key.
29 */
30 public const CACHE_KEY_VAR = 'cache_key';
31
32 /**
33 * @var array[]
34 */
35 protected array $themes = [];
36
37 /**
38 * @var bool[]
39 */
40 protected array $useDefaultThemes = [];
41
42 /**
43 * @var array[]
44 */
45 protected array $resources = [];
46
47 /**
48 * @var array<array<int|false>>
49 */
50 private array $resourceHierarchyLevels = [];
51
52 /**
53 * Creates a new renderer engine.
54 *
55 * @param array $defaultThemes The default themes. The type of these
56 * themes is open to the implementation.
57 */
58 public function __construct(
59 protected array $defaultThemes = [],
60 ) {
61 }
62
63 public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void
64 {
65 $cacheKey = $view->vars[self::CACHE_KEY_VAR];
66
67 // Do not cast, as casting turns objects into arrays of properties
68 $this->themes[$cacheKey] = \is_array($themes) ? $themes : [$themes];
69 $this->useDefaultThemes[$cacheKey] = $useDefaultThemes;
70
71 // Unset instead of resetting to an empty array, in order to allow
72 // implementations (like TwigRendererEngine) to check whether $cacheKey
73 // is set at all.
74 unset($this->resources[$cacheKey], $this->resourceHierarchyLevels[$cacheKey]);
75 }
76
77 public function getResourceForBlockName(FormView $view, string $blockName): mixed
78 {
79 $cacheKey = $view->vars[self::CACHE_KEY_VAR];
80
81 if (!isset($this->resources[$cacheKey][$blockName])) {
82 $this->loadResourceForBlockName($cacheKey, $view, $blockName);
83 }
84
85 return $this->resources[$cacheKey][$blockName];
86 }
87
88 public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): mixed
89 {
90 $cacheKey = $view->vars[self::CACHE_KEY_VAR];
91 $blockName = $blockNameHierarchy[$hierarchyLevel];
92
93 if (!isset($this->resources[$cacheKey][$blockName])) {
94 $this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $hierarchyLevel);
95 }
96
97 return $this->resources[$cacheKey][$blockName];
98 }
99
100 public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): int|false
101 {
102 $cacheKey = $view->vars[self::CACHE_KEY_VAR];
103 $blockName = $blockNameHierarchy[$hierarchyLevel];
104
105 if (!isset($this->resources[$cacheKey][$blockName])) {
106 $this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $hierarchyLevel);
107 }
108
109 // If $block was previously rendered loaded with loadTemplateForBlock(), the template
110 // is cached but the hierarchy level is not. In this case, we know that the block
111 // exists at this very hierarchy level, so we can just set it.
112 if (!isset($this->resourceHierarchyLevels[$cacheKey][$blockName])) {
113 $this->resourceHierarchyLevels[$cacheKey][$blockName] = $hierarchyLevel;
114 }
115
116 return $this->resourceHierarchyLevels[$cacheKey][$blockName];
117 }
118
119 /**
120 * Loads the cache with the resource for a given block name.
121 *
122 * @see getResourceForBlock()
123 */
124 abstract protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName): bool;
125
126 /**
127 * Loads the cache with the resource for a specific level of a block hierarchy.
128 *
129 * @see getResourceForBlockHierarchy()
130 */
131 private function loadResourceForBlockNameHierarchy(string $cacheKey, FormView $view, array $blockNameHierarchy, int $hierarchyLevel): bool
132 {
133 $blockName = $blockNameHierarchy[$hierarchyLevel];
134
135 // Try to find a template for that block
136 if ($this->loadResourceForBlockName($cacheKey, $view, $blockName)) {
137 // If loadTemplateForBlock() returns true, it was able to populate the
138 // cache. The only missing thing is to set the hierarchy level at which
139 // the template was found.
140 $this->resourceHierarchyLevels[$cacheKey][$blockName] = $hierarchyLevel;
141
142 return true;
143 }
144
145 if ($hierarchyLevel > 0) {
146 $parentLevel = $hierarchyLevel - 1;
147 $parentBlockName = $blockNameHierarchy[$parentLevel];
148
149 // The next two if statements contain slightly duplicated code. This is by intention
150 // and tries to avoid execution of unnecessary checks in order to increase performance.
151
152 if (isset($this->resources[$cacheKey][$parentBlockName])) {
153 // It may happen that the parent block is already loaded, but its level is not.
154 // In this case, the parent block must have been loaded by loadResourceForBlock(),
155 // which does not check the hierarchy of the block. Subsequently the block must have
156 // been found directly on the parent level.
157 if (!isset($this->resourceHierarchyLevels[$cacheKey][$parentBlockName])) {
158 $this->resourceHierarchyLevels[$cacheKey][$parentBlockName] = $parentLevel;
159 }
160
161 // Cache the shortcuts for further accesses
162 $this->resources[$cacheKey][$blockName] = $this->resources[$cacheKey][$parentBlockName];
163 $this->resourceHierarchyLevels[$cacheKey][$blockName] = $this->resourceHierarchyLevels[$cacheKey][$parentBlockName];
164
165 return true;
166 }
167
168 if ($this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $parentLevel)) {
169 // Cache the shortcuts for further accesses
170 $this->resources[$cacheKey][$blockName] = $this->resources[$cacheKey][$parentBlockName];
171 $this->resourceHierarchyLevels[$cacheKey][$blockName] = $this->resourceHierarchyLevels[$cacheKey][$parentBlockName];
172
173 return true;
174 }
175 }
176
177 // Cache the result for further accesses
178 $this->resources[$cacheKey][$blockName] = false;
179 $this->resourceHierarchyLevels[$cacheKey][$blockName] = false;
180
181 return false;
182 }
183
184 public function reset(): void
185 {
186 $this->themes = [];
187 $this->useDefaultThemes = [];
188 $this->resources = [];
189 $this->resourceHierarchyLevels = [];
190 }
191}
192
193------------------------------------------------------------------------------------------------------------------------
194 ./FormFactory.php
195------------------------------------------------------------------------------------------------------------------------
196<?php
197
198/*
199 * This file is part of the Symfony package.
200 *
201 * (c) Fabien Potencier <fabien@symfony.com>
202 *
203 * For the full copyright and license information, please view the LICENSE
204 * file that was distributed with this source code.
205 */
206
207namespace Symfony\Component\Form;
208
209use Symfony\Component\Form\Extension\Core\Type\FormType;
210use Symfony\Component\Form\Extension\Core\Type\TextType;
211
212class FormFactory implements FormFactoryInterface
213{
214 public function __construct(
215 private FormRegistryInterface $registry,
216 ) {
217 }
218
219 public function create(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
220 {
221 return $this->createBuilder($type, $data, $options)->getForm();
222 }
223
224 public function createNamed(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
225 {
226 return $this->createNamedBuilder($name, $type, $data, $options)->getForm();
227 }
228
229 public function createForProperty(string $class, string $property, mixed $data = null, array $options = []): FormInterface
230 {
231 return $this->createBuilderForProperty($class, $property, $data, $options)->getForm();
232 }
233
234 public function createBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface
235 {
236 return $this->createNamedBuilder($this->registry->getType($type)->getBlockPrefix(), $type, $data, $options);
237 }
238
239 public function createNamedBuilder(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface
240 {
241 if (null !== $data && !\array_key_exists('data', $options)) {
242 $options['data'] = $data;
243 }
244
245 $type = $this->registry->getType($type);
246
247 $builder = $type->createBuilder($this, $name, $options);
248
249 // Explicitly call buildForm() in order to be able to override either
250 // createBuilder() or buildForm() in the resolved form type
251 $type->buildForm($builder, $builder->getOptions());
252
253 return $builder;
254 }
255
256 public function createBuilderForProperty(string $class, string $property, mixed $data = null, array $options = []): FormBuilderInterface
257 {
258 if (null === $guesser = $this->registry->getTypeGuesser()) {
259 return $this->createNamedBuilder($property, TextType::class, $data, $options);
260 }
261
262 $typeGuess = $guesser->guessType($class, $property);
263 $maxLengthGuess = $guesser->guessMaxLength($class, $property);
264 $requiredGuess = $guesser->guessRequired($class, $property);
265 $patternGuess = $guesser->guessPattern($class, $property);
266
267 $type = $typeGuess ? $typeGuess->getType() : TextType::class;
268
269 $maxLength = $maxLengthGuess?->getValue();
270 $pattern = $patternGuess?->getValue();
271
272 if (null !== $pattern) {
273 $options = array_replace_recursive(['attr' => ['pattern' => $pattern]], $options);
274 }
275
276 if (null !== $maxLength) {
277 $options = array_replace_recursive(['attr' => ['maxlength' => $maxLength]], $options);
278 }
279
280 if ($requiredGuess) {
281 $options = array_merge(['required' => $requiredGuess->getValue()], $options);
282 }
283
284 // user options may override guessed options
285 if ($typeGuess) {
286 $attrs = [];
287 $typeGuessOptions = $typeGuess->getOptions();
288 if (isset($typeGuessOptions['attr']) && isset($options['attr'])) {
289 $attrs = ['attr' => array_merge($typeGuessOptions['attr'], $options['attr'])];
290 }
291
292 $options = array_merge($typeGuessOptions, $options, $attrs);
293 }
294
295 return $this->createNamedBuilder($property, $type, $data, $options);
296 }
297}
298
299------------------------------------------------------------------------------------------------------------------------
300 ./AbstractType.php
301------------------------------------------------------------------------------------------------------------------------
302<?php
303
304/*
305 * This file is part of the Symfony package.
306 *
307 * (c) Fabien Potencier <fabien@symfony.com>
308 *
309 * For the full copyright and license information, please view the LICENSE
310 * file that was distributed with this source code.
311 */
312
313namespace Symfony\Component\Form;
314
315use Symfony\Component\Form\Extension\Core\Type\FormType;
316use Symfony\Component\Form\Util\StringUtil;
317use Symfony\Component\OptionsResolver\OptionsResolver;
318
319/**
320 * @author Bernhard Schussek <bschussek@gmail.com>
321 */
322abstract class AbstractType implements FormTypeInterface
323{
324 /**
325 * @return string|null
326 */
327 public function getParent()
328 {
329 return FormType::class;
330 }
331
332 /**
333 * @return void
334 */
335 public function configureOptions(OptionsResolver $resolver)
336 {
337 }
338
339 /**
340 * @return void
341 */
342 public function buildForm(FormBuilderInterface $builder, array $options)
343 {
344 }
345
346 /**
347 * @return void
348 */
349 public function buildView(FormView $view, FormInterface $form, array $options)
350 {
351 }
352
353 /**
354 * @return void
355 */
356 public function finishView(FormView $view, FormInterface $form, array $options)
357 {
358 }
359
360 /**
361 * @return string
362 */
363 public function getBlockPrefix()
364 {
365 return StringUtil::fqcnToBlockPrefix(static::class) ?: '';
366 }
367}
368
369------------------------------------------------------------------------------------------------------------------------
370 ./ResolvedFormTypeInterface.php
371------------------------------------------------------------------------------------------------------------------------
372<?php
373
374/*
375 * This file is part of the Symfony package.
376 *
377 * (c) Fabien Potencier <fabien@symfony.com>
378 *
379 * For the full copyright and license information, please view the LICENSE
380 * file that was distributed with this source code.
381 */
382
383namespace Symfony\Component\Form;
384
385use Symfony\Component\OptionsResolver\OptionsResolver;
386
387/**
388 * A wrapper for a form type and its extensions.
389 *
390 * @author Bernhard Schussek <bschussek@gmail.com>
391 */
392interface ResolvedFormTypeInterface
393{
394 /**
395 * Returns the prefix of the template block name for this type.
396 */
397 public function getBlockPrefix(): string;
398
399 /**
400 * Returns the parent type.
401 */
402 public function getParent(): ?self;
403
404 /**
405 * Returns the wrapped form type.
406 */
407 public function getInnerType(): FormTypeInterface;
408
409 /**
410 * Returns the extensions of the wrapped form type.
411 *
412 * @return FormTypeExtensionInterface[]
413 */
414 public function getTypeExtensions(): array;
415
416 /**
417 * Creates a new form builder for this type.
418 *
419 * @param string $name The name for the builder
420 */
421 public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface;
422
423 /**
424 * Creates a new form view for a form of this type.
425 */
426 public function createView(FormInterface $form, ?FormView $parent = null): FormView;
427
428 /**
429 * Configures a form builder for the type hierarchy.
430 */
431 public function buildForm(FormBuilderInterface $builder, array $options): void;
432
433 /**
434 * Configures a form view for the type hierarchy.
435 *
436 * It is called before the children of the view are built.
437 */
438 public function buildView(FormView $view, FormInterface $form, array $options): void;
439
440 /**
441 * Finishes a form view for the type hierarchy.
442 *
443 * It is called after the children of the view have been built.
444 */
445 public function finishView(FormView $view, FormInterface $form, array $options): void;
446
447 /**
448 * Returns the configured options resolver used for this type.
449 */
450 public function getOptionsResolver(): OptionsResolver;
451}
452
453------------------------------------------------------------------------------------------------------------------------
454 ./ClearableErrorsInterface.php
455------------------------------------------------------------------------------------------------------------------------
456<?php
457
458/*
459 * This file is part of the Symfony package.
460 *
461 * (c) Fabien Potencier <fabien@symfony.com>
462 *
463 * For the full copyright and license information, please view the LICENSE
464 * file that was distributed with this source code.
465 */
466
467namespace Symfony\Component\Form;
468
469/**
470 * A form element whose errors can be cleared.
471 *
472 * @author Colin O'Dell <colinodell@gmail.com>
473 */
474interface ClearableErrorsInterface
475{
476 /**
477 * Removes all the errors of this form.
478 *
479 * @param bool $deep Whether to remove errors from child forms as well
480 *
481 * @return $this
482 */
483 public function clearErrors(bool $deep = false): static;
484}
485
486------------------------------------------------------------------------------------------------------------------------
487 ./FormFactoryBuilderInterface.php
488------------------------------------------------------------------------------------------------------------------------
489<?php
490
491/*
492 * This file is part of the Symfony package.
493 *
494 * (c) Fabien Potencier <fabien@symfony.com>
495 *
496 * For the full copyright and license information, please view the LICENSE
497 * file that was distributed with this source code.
498 */
499
500namespace Symfony\Component\Form;
501
502/**
503 * A builder for FormFactoryInterface objects.
504 *
505 * @author Bernhard Schussek <bschussek@gmail.com>
506 */
507interface FormFactoryBuilderInterface
508{
509 /**
510 * Sets the factory for creating ResolvedFormTypeInterface instances.
511 *
512 * @return $this
513 */
514 public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory): static;
515
516 /**
517 * Adds an extension to be loaded by the factory.
518 *
519 * @return $this
520 */
521 public function addExtension(FormExtensionInterface $extension): static;
522
523 /**
524 * Adds a list of extensions to be loaded by the factory.
525 *
526 * @param FormExtensionInterface[] $extensions The extensions
527 *
528 * @return $this
529 */
530 public function addExtensions(array $extensions): static;
531
532 /**
533 * Adds a form type to the factory.
534 *
535 * @return $this
536 */
537 public function addType(FormTypeInterface $type): static;
538
539 /**
540 * Adds a list of form types to the factory.
541 *
542 * @param FormTypeInterface[] $types The form types
543 *
544 * @return $this
545 */
546 public function addTypes(array $types): static;
547
548 /**
549 * Adds a form type extension to the factory.
550 *
551 * @return $this
552 */
553 public function addTypeExtension(FormTypeExtensionInterface $typeExtension): static;
554
555 /**
556 * Adds a list of form type extensions to the factory.
557 *
558 * @param FormTypeExtensionInterface[] $typeExtensions The form type extensions
559 *
560 * @return $this
561 */
562 public function addTypeExtensions(array $typeExtensions): static;
563
564 /**
565 * Adds a type guesser to the factory.
566 *
567 * @return $this
568 */
569 public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser): static;
570
571 /**
572 * Adds a list of type guessers to the factory.
573 *
574 * @param FormTypeGuesserInterface[] $typeGuessers The type guessers
575 *
576 * @return $this
577 */
578 public function addTypeGuessers(array $typeGuessers): static;
579
580 /**
581 * Builds and returns the factory.
582 */
583 public function getFormFactory(): FormFactoryInterface;
584}
585
586------------------------------------------------------------------------------------------------------------------------
587 ./ResolvedFormType.php
588------------------------------------------------------------------------------------------------------------------------
589<?php
590
591/*
592 * This file is part of the Symfony package.
593 *
594 * (c) Fabien Potencier <fabien@symfony.com>
595 *
596 * For the full copyright and license information, please view the LICENSE
597 * file that was distributed with this source code.
598 */
599
600namespace Symfony\Component\Form;
601
602use Symfony\Component\EventDispatcher\EventDispatcher;
603use Symfony\Component\Form\Exception\UnexpectedTypeException;
604use Symfony\Component\OptionsResolver\Exception\ExceptionInterface;
605use Symfony\Component\OptionsResolver\OptionsResolver;
606
607/**
608 * A wrapper for a form type and its extensions.
609 *
610 * @author Bernhard Schussek <bschussek@gmail.com>
611 */
612class ResolvedFormType implements ResolvedFormTypeInterface
613{
614 /**
615 * @var FormTypeExtensionInterface[]
616 */
617 private array $typeExtensions;
618
619 private OptionsResolver $optionsResolver;
620
621 /**
622 * @param FormTypeExtensionInterface[] $typeExtensions
623 */
624 public function __construct(
625 private FormTypeInterface $innerType,
626 array $typeExtensions = [],
627 private ?ResolvedFormTypeInterface $parent = null,
628 ) {
629 foreach ($typeExtensions as $extension) {
630 if (!$extension instanceof FormTypeExtensionInterface) {
631 throw new UnexpectedTypeException($extension, FormTypeExtensionInterface::class);
632 }
633 }
634
635 $this->typeExtensions = $typeExtensions;
636 }
637
638 public function getBlockPrefix(): string
639 {
640 return $this->innerType->getBlockPrefix();
641 }
642
643 public function getParent(): ?ResolvedFormTypeInterface
644 {
645 return $this->parent;
646 }
647
648 public function getInnerType(): FormTypeInterface
649 {
650 return $this->innerType;
651 }
652
653 public function getTypeExtensions(): array
654 {
655 return $this->typeExtensions;
656 }
657
658 public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface
659 {
660 try {
661 $options = $this->getOptionsResolver()->resolve($options);
662 } catch (ExceptionInterface $e) {
663 throw new $e(\sprintf('An error has occurred resolving the options of the form "%s": ', get_debug_type($this->getInnerType())).$e->getMessage(), $e->getCode(), $e);
664 }
665
666 // Should be decoupled from the specific option at some point
667 $dataClass = $options['data_class'] ?? null;
668
669 $builder = $this->newBuilder($name, $dataClass, $factory, $options);
670 $builder->setType($this);
671
672 return $builder;
673 }
674
675 public function createView(FormInterface $form, ?FormView $parent = null): FormView
676 {
677 return $this->newView($parent);
678 }
679
680 public function buildForm(FormBuilderInterface $builder, array $options): void
681 {
682 $this->parent?->buildForm($builder, $options);
683
684 $this->innerType->buildForm($builder, $options);
685
686 foreach ($this->typeExtensions as $extension) {
687 $extension->buildForm($builder, $options);
688 }
689 }
690
691 public function buildView(FormView $view, FormInterface $form, array $options): void
692 {
693 $this->parent?->buildView($view, $form, $options);
694
695 $this->innerType->buildView($view, $form, $options);
696
697 foreach ($this->typeExtensions as $extension) {
698 $extension->buildView($view, $form, $options);
699 }
700 }
701
702 public function finishView(FormView $view, FormInterface $form, array $options): void
703 {
704 $this->parent?->finishView($view, $form, $options);
705
706 $this->innerType->finishView($view, $form, $options);
707
708 foreach ($this->typeExtensions as $extension) {
709 /* @var FormTypeExtensionInterface $extension */
710 $extension->finishView($view, $form, $options);
711 }
712 }
713
714 public function getOptionsResolver(): OptionsResolver
715 {
716 if (!isset($this->optionsResolver)) {
717 if (null !== $this->parent) {
718 $this->optionsResolver = clone $this->parent->getOptionsResolver();
719 } else {
720 $this->optionsResolver = new OptionsResolver();
721 }
722
723 $this->innerType->configureOptions($this->optionsResolver);
724
725 foreach ($this->typeExtensions as $extension) {
726 $extension->configureOptions($this->optionsResolver);
727 }
728 }
729
730 return $this->optionsResolver;
731 }
732
733 /**
734 * Creates a new builder instance.
735 *
736 * Override this method if you want to customize the builder class.
737 */
738 protected function newBuilder(string $name, ?string $dataClass, FormFactoryInterface $factory, array $options): FormBuilderInterface
739 {
740 if ($this->innerType instanceof ButtonTypeInterface) {
741 return new ButtonBuilder($name, $options);
742 }
743
744 if ($this->innerType instanceof SubmitButtonTypeInterface) {
745 return new SubmitButtonBuilder($name, $options);
746 }
747
748 return new FormBuilder($name, $dataClass, new EventDispatcher(), $factory, $options);
749 }
750
751 /**
752 * Creates a new view instance.
753 *
754 * Override this method if you want to customize the view class.
755 */
756 protected function newView(?FormView $parent = null): FormView
757 {
758 return new FormView($parent);
759 }
760}
761
762------------------------------------------------------------------------------------------------------------------------
763 ./Forms.php
764------------------------------------------------------------------------------------------------------------------------
765<?php
766
767/*
768 * This file is part of the Symfony package.
769 *
770 * (c) Fabien Potencier <fabien@symfony.com>
771 *
772 * For the full copyright and license information, please view the LICENSE
773 * file that was distributed with this source code.
774 */
775
776namespace Symfony\Component\Form;
777
778/**
779 * Entry point of the Form component.
780 *
781 * Use this class to conveniently create new form factories:
782 *
783 * use Symfony\Component\Form\Forms;
784 *
785 * $formFactory = Forms::createFormFactory();
786 *
787 * $form = $formFactory->createBuilder()
788 * ->add('firstName', 'Symfony\Component\Form\Extension\Core\Type\TextType')
789 * ->add('lastName', 'Symfony\Component\Form\Extension\Core\Type\TextType')
790 * ->add('age', 'Symfony\Component\Form\Extension\Core\Type\IntegerType')
791 * ->add('color', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [
792 * 'choices' => ['Red' => 'r', 'Blue' => 'b'],
793 * ])
794 * ->getForm();
795 *
796 * You can also add custom extensions to the form factory:
797 *
798 * $formFactory = Forms::createFormFactoryBuilder()
799 * ->addExtension(new AcmeExtension())
800 * ->getFormFactory();
801 *
802 * If you create custom form types or type extensions, it is
803 * generally recommended to create your own extensions that lazily
804 * load these types and type extensions. In projects where performance
805 * does not matter that much, you can also pass them directly to the
806 * form factory:
807 *
808 * $formFactory = Forms::createFormFactoryBuilder()
809 * ->addType(new PersonType())
810 * ->addType(new PhoneNumberType())
811 * ->addTypeExtension(new FormTypeHelpTextExtension())
812 * ->getFormFactory();
813 *
814 * Support for the Validator component is provided by ValidatorExtension.
815 * This extension needs a validator object to function properly:
816 *
817 * use Symfony\Component\Validator\Validation;
818 * use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
819 *
820 * $validator = Validation::createValidator();
821 * $formFactory = Forms::createFormFactoryBuilder()
822 * ->addExtension(new ValidatorExtension($validator))
823 * ->getFormFactory();
824 *
825 * @author Bernhard Schussek <bschussek@gmail.com>
826 */
827final class Forms
828{
829 /**
830 * Creates a form factory with the default configuration.
831 */
832 public static function createFormFactory(): FormFactoryInterface
833 {
834 return self::createFormFactoryBuilder()->getFormFactory();
835 }
836
837 /**
838 * Creates a form factory builder with the default configuration.
839 */
840 public static function createFormFactoryBuilder(): FormFactoryBuilderInterface
841 {
842 return new FormFactoryBuilder(true);
843 }
844
845 /**
846 * This class cannot be instantiated.
847 */
848 private function __construct()
849 {
850 }
851}
852
853------------------------------------------------------------------------------------------------------------------------
854 ./FormConfigBuilderInterface.php
855------------------------------------------------------------------------------------------------------------------------
856<?php
857
858/*
859 * This file is part of the Symfony package.
860 *
861 * (c) Fabien Potencier <fabien@symfony.com>
862 *
863 * For the full copyright and license information, please view the LICENSE
864 * file that was distributed with this source code.
865 */
866
867namespace Symfony\Component\Form;
868
869use Symfony\Component\EventDispatcher\EventSubscriberInterface;
870use Symfony\Component\PropertyAccess\PropertyPathInterface;
871
872/**
873 * @author Bernhard Schussek <bschussek@gmail.com>
874 */
875interface FormConfigBuilderInterface extends FormConfigInterface
876{
877 /**
878 * Adds an event listener to an event on this form.
879 *
880 * @param int $priority The priority of the listener. Listeners
881 * with a higher priority are called before
882 * listeners with a lower priority.
883 *
884 * @return $this
885 */
886 public function addEventListener(string $eventName, callable $listener, int $priority = 0): static;
887
888 /**
889 * Adds an event subscriber for events on this form.
890 *
891 * @return $this
892 */
893 public function addEventSubscriber(EventSubscriberInterface $subscriber): static;
894
895 /**
896 * Appends / prepends a transformer to the view transformer chain.
897 *
898 * The transform method of the transformer is used to convert data from the
899 * normalized to the view format.
900 * The reverseTransform method of the transformer is used to convert from the
901 * view to the normalized format.
902 *
903 * @param bool $forcePrepend If set to true, prepend instead of appending
904 *
905 * @return $this
906 */
907 public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): static;
908
909 /**
910 * Clears the view transformers.
911 *
912 * @return $this
913 */
914 public function resetViewTransformers(): static;
915
916 /**
917 * Prepends / appends a transformer to the normalization transformer chain.
918 *
919 * The transform method of the transformer is used to convert data from the
920 * model to the normalized format.
921 * The reverseTransform method of the transformer is used to convert from the
922 * normalized to the model format.
923 *
924 * @param bool $forceAppend If set to true, append instead of prepending
925 *
926 * @return $this
927 */
928 public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): static;
929
930 /**
931 * Clears the normalization transformers.
932 *
933 * @return $this
934 */
935 public function resetModelTransformers(): static;
936
937 /**
938 * Sets the value for an attribute.
939 *
940 * @param mixed $value The value of the attribute
941 *
942 * @return $this
943 */
944 public function setAttribute(string $name, mixed $value): static;
945
946 /**
947 * Sets the attributes.
948 *
949 * @return $this
950 */
951 public function setAttributes(array $attributes): static;
952
953 /**
954 * Sets the data mapper used by the form.
955 *
956 * @return $this
957 */
958 public function setDataMapper(?DataMapperInterface $dataMapper): static;
959
960 /**
961 * Sets whether the form is disabled.
962 *
963 * @return $this
964 */
965 public function setDisabled(bool $disabled): static;
966
967 /**
968 * Sets the data used for the client data when no value is submitted.
969 *
970 * @param mixed $emptyData The empty data
971 *
972 * @return $this
973 */
974 public function setEmptyData(mixed $emptyData): static;
975
976 /**
977 * Sets whether errors bubble up to the parent.
978 *
979 * @return $this
980 */
981 public function setErrorBubbling(bool $errorBubbling): static;
982
983 /**
984 * Sets whether this field is required to be filled out when submitted.
985 *
986 * @return $this
987 */
988 public function setRequired(bool $required): static;
989
990 /**
991 * Sets the property path that the form should be mapped to.
992 *
993 * @param string|PropertyPathInterface|null $propertyPath The property path or null if the path should be set
994 * automatically based on the form's name
995 *
996 * @return $this
997 */
998 public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): static;
999
1000 /**
1001 * Sets whether the form should be mapped to an element of its
1002 * parent's data.
1003 *
1004 * @return $this
1005 */
1006 public function setMapped(bool $mapped): static;
1007
1008 /**
1009 * Sets whether the form's data should be modified by reference.
1010 *
1011 * @return $this
1012 */
1013 public function setByReference(bool $byReference): static;
1014
1015 /**
1016 * Sets whether the form should read and write the data of its parent.
1017 *
1018 * @return $this
1019 */
1020 public function setInheritData(bool $inheritData): static;
1021
1022 /**
1023 * Sets whether the form should be compound.
1024 *
1025 * @return $this
1026 *
1027 * @see FormConfigInterface::getCompound()
1028 */
1029 public function setCompound(bool $compound): static;
1030
1031 /**
1032 * Sets the resolved type.
1033 *
1034 * @return $this
1035 */
1036 public function setType(ResolvedFormTypeInterface $type): static;
1037
1038 /**
1039 * Sets the initial data of the form.
1040 *
1041 * @param mixed $data The data of the form in model format
1042 *
1043 * @return $this
1044 */
1045 public function setData(mixed $data): static;
1046
1047 /**
1048 * Locks the form's data to the data passed in the configuration.
1049 *
1050 * A form with locked data is restricted to the data passed in
1051 * this configuration. The data can only be modified then by
1052 * submitting the form or using PRE_SET_DATA event.
1053 *
1054 * It means data passed to a factory method or mapped from the
1055 * parent will be ignored.
1056 *
1057 * @return $this
1058 */
1059 public function setDataLocked(bool $locked): static;
1060
1061 /**
1062 * Sets the form factory used for creating new forms.
1063 *
1064 * @return $this
1065 */
1066 public function setFormFactory(FormFactoryInterface $formFactory): static;
1067
1068 /**
1069 * Sets the target URL of the form.
1070 *
1071 * @return $this
1072 */
1073 public function setAction(string $action): static;
1074
1075 /**
1076 * Sets the HTTP method used by the form.
1077 *
1078 * @return $this
1079 */
1080 public function setMethod(string $method): static;
1081
1082 /**
1083 * Sets the request handler used by the form.
1084 *
1085 * @return $this
1086 */
1087 public function setRequestHandler(RequestHandlerInterface $requestHandler): static;
1088
1089 /**
1090 * Sets whether the form should be initialized automatically.
1091 *
1092 * Should be set to true only for root forms.
1093 *
1094 * @param bool $initialize True to initialize the form automatically,
1095 * false to suppress automatic initialization.
1096 * In the second case, you need to call
1097 * {@link FormInterface::initialize()} manually.
1098 *
1099 * @return $this
1100 */
1101 public function setAutoInitialize(bool $initialize): static;
1102
1103 /**
1104 * Builds and returns the form configuration.
1105 */
1106 public function getFormConfig(): FormConfigInterface;
1107
1108 /**
1109 * Sets the callback that will be called to determine if the model
1110 * data of the form is empty or not.
1111 *
1112 * @return $this
1113 */
1114 public function setIsEmptyCallback(?callable $isEmptyCallback): static;
1115}
1116
1117------------------------------------------------------------------------------------------------------------------------
1118 ./FormBuilderInterface.php
1119------------------------------------------------------------------------------------------------------------------------
1120<?php
1121
1122/*
1123 * This file is part of the Symfony package.
1124 *
1125 * (c) Fabien Potencier <fabien@symfony.com>
1126 *
1127 * For the full copyright and license information, please view the LICENSE
1128 * file that was distributed with this source code.
1129 */
1130
1131namespace Symfony\Component\Form;
1132
1133/**
1134 * @author Bernhard Schussek <bschussek@gmail.com>
1135 *
1136 * @extends \Traversable<string, FormBuilderInterface>
1137 */
1138interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuilderInterface
1139{
1140 /**
1141 * Adds a new field to this group. A field must have a unique name within
1142 * the group. Otherwise the existing field is overwritten.
1143 *
1144 * If you add a nested group, this group should also be represented in the
1145 * object hierarchy.
1146 *
1147 * @param array<string, mixed> $options
1148 */
1149 public function add(string|self $child, ?string $type = null, array $options = []): static;
1150
1151 /**
1152 * Creates a form builder.
1153 *
1154 * @param string $name The name of the form or the name of the property
1155 * @param string|null $type The type of the form or null if name is a property
1156 * @param array<string, mixed> $options
1157 */
1158 public function create(string $name, ?string $type = null, array $options = []): self;
1159
1160 /**
1161 * Returns a child by name.
1162 *
1163 * @throws Exception\InvalidArgumentException if the given child does not exist
1164 */
1165 public function get(string $name): self;
1166
1167 /**
1168 * Removes the field with the given name.
1169 */
1170 public function remove(string $name): static;
1171
1172 /**
1173 * Returns whether a field with the given name exists.
1174 */
1175 public function has(string $name): bool;
1176
1177 /**
1178 * Returns the children.
1179 *
1180 * @return array<string, self>
1181 */
1182 public function all(): array;
1183
1184 /**
1185 * Creates the form.
1186 */
1187 public function getForm(): FormInterface;
1188}
1189
1190------------------------------------------------------------------------------------------------------------------------
1191 ./Command/DebugCommand.php
1192------------------------------------------------------------------------------------------------------------------------
1193<?php
1194
1195/*
1196 * This file is part of the Symfony package.
1197 *
1198 * (c) Fabien Potencier <fabien@symfony.com>
1199 *
1200 * For the full copyright and license information, please view the LICENSE
1201 * file that was distributed with this source code.
1202 */
1203
1204namespace Symfony\Component\Form\Command;
1205
1206use Symfony\Component\Console\Attribute\AsCommand;
1207use Symfony\Component\Console\Command\Command;
1208use Symfony\Component\Console\Completion\CompletionInput;
1209use Symfony\Component\Console\Completion\CompletionSuggestions;
1210use Symfony\Component\Console\Exception\InvalidArgumentException;
1211use Symfony\Component\Console\Input\InputArgument;
1212use Symfony\Component\Console\Input\InputInterface;
1213use Symfony\Component\Console\Input\InputOption;
1214use Symfony\Component\Console\Output\OutputInterface;
1215use Symfony\Component\Console\Style\SymfonyStyle;
1216use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
1217use Symfony\Component\Form\Console\Helper\DescriptorHelper;
1218use Symfony\Component\Form\Extension\Core\CoreExtension;
1219use Symfony\Component\Form\FormRegistryInterface;
1220use Symfony\Component\Form\FormTypeInterface;
1221
1222/**
1223 * A console command for retrieving information about form types.
1224 *
1225 * @author Yonel Ceruto <yonelceruto@gmail.com>
1226 */
1227#[AsCommand(name: 'debug:form', description: 'Display form type information')]
1228class DebugCommand extends Command
1229{
1230 public function __construct(
1231 private FormRegistryInterface $formRegistry,
1232 private array $namespaces = ['Symfony\Component\Form\Extension\Core\Type'],
1233 private array $types = [],
1234 private array $extensions = [],
1235 private array $guessers = [],
1236 private ?FileLinkFormatter $fileLinkFormatter = null,
1237 ) {
1238 parent::__construct();
1239 }
1240
1241 protected function configure(): void
1242 {
1243 $this
1244 ->setDefinition([
1245 new InputArgument('class', InputArgument::OPTIONAL, 'The form type class'),
1246 new InputArgument('option', InputArgument::OPTIONAL, 'The form type option'),
1247 new InputOption('show-deprecated', null, InputOption::VALUE_NONE, 'Display deprecated options in form types'),
1248 new InputOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'txt'),
1249 ])
1250 ->setHelp(<<<'EOF'
1251The <info>%command.name%</info> command displays information about form types.
1252
1253 <info>php %command.full_name%</info>
1254
1255The command lists all built-in types, services types, type extensions and
1256guessers currently available.
1257
1258 <info>php %command.full_name% Symfony\Component\Form\Extension\Core\Type\ChoiceType</info>
1259 <info>php %command.full_name% ChoiceType</info>
1260
1261The command lists all defined options that contains the given form type,
1262as well as their parents and type extensions.
1263
1264 <info>php %command.full_name% ChoiceType choice_value</info>
1265
1266Use the <info>--show-deprecated</info> option to display form types with
1267deprecated options or the deprecated options of the given form type:
1268
1269 <info>php %command.full_name% --show-deprecated</info>
1270 <info>php %command.full_name% ChoiceType --show-deprecated</info>
1271
1272The command displays the definition of the given option name.
1273
1274 <info>php %command.full_name% --format=json</info>
1275
1276The command lists everything in a machine readable json format.
1277EOF
1278 )
1279 ;
1280 }
1281
1282 protected function execute(InputInterface $input, OutputInterface $output): int
1283 {
1284 $io = new SymfonyStyle($input, $output);
1285
1286 if (null === $class = $input->getArgument('class')) {
1287 $object = null;
1288 $options['core_types'] = $this->getCoreTypes();
1289 $options['service_types'] = array_values(array_diff($this->types, $options['core_types']));
1290 if ($input->getOption('show-deprecated')) {
1291 $options['core_types'] = $this->filterTypesByDeprecated($options['core_types']);
1292 $options['service_types'] = $this->filterTypesByDeprecated($options['service_types']);
1293 }
1294 $options['extensions'] = $this->extensions;
1295 $options['guessers'] = $this->guessers;
1296 foreach ($options as $k => $list) {
1297 sort($options[$k]);
1298 }
1299 } else {
1300 if (!class_exists($class) || !is_subclass_of($class, FormTypeInterface::class)) {
1301 $class = $this->getFqcnTypeClass($input, $io, $class);
1302 }
1303 $resolvedType = $this->formRegistry->getType($class);
1304
1305 if ($option = $input->getArgument('option')) {
1306 $object = $resolvedType->getOptionsResolver();
1307
1308 if (!$object->isDefined($option)) {
1309 $message = \sprintf('Option "%s" is not defined in "%s".', $option, $resolvedType->getInnerType()::class);
1310
1311 if ($alternatives = $this->findAlternatives($option, $object->getDefinedOptions())) {
1312 if (1 === \count($alternatives)) {
1313 $message .= "\n\nDid you mean this?\n ";
1314 } else {
1315 $message .= "\n\nDid you mean one of these?\n ";
1316 }
1317 $message .= implode("\n ", $alternatives);
1318 }
1319
1320 throw new InvalidArgumentException($message);
1321 }
1322
1323 $options['type'] = $resolvedType->getInnerType();
1324 $options['option'] = $option;
1325 } else {
1326 $object = $resolvedType;
1327 }
1328 }
1329
1330 $helper = new DescriptorHelper($this->fileLinkFormatter);
1331 $options['format'] = $input->getOption('format');
1332 $options['show_deprecated'] = $input->getOption('show-deprecated');
1333 $helper->describe($io, $object, $options);
1334
1335 return 0;
1336 }
1337
1338 private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, string $shortClassName): string
1339 {
1340 $classes = $this->getFqcnTypeClasses($shortClassName);
1341
1342 if (0 === $count = \count($classes)) {
1343 $message = \sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces));
1344
1345 $allTypes = array_merge($this->getCoreTypes(), $this->types);
1346 if ($alternatives = $this->findAlternatives($shortClassName, $allTypes)) {
1347 if (1 === \count($alternatives)) {
1348 $message .= "\n\nDid you mean this?\n ";
1349 } else {
1350 $message .= "\n\nDid you mean one of these?\n ";
1351 }
1352 $message .= implode("\n ", $alternatives);
1353 }
1354
1355 throw new InvalidArgumentException($message);
1356 }
1357 if (1 === $count) {
1358 return $classes[0];
1359 }
1360 if (!$input->isInteractive()) {
1361 throw new InvalidArgumentException(\sprintf("The type \"%s\" is ambiguous.\n\nDid you mean one of these?\n %s.", $shortClassName, implode("\n ", $classes)));
1362 }
1363
1364 return $io->choice(\sprintf("The type \"%s\" is ambiguous.\n\nSelect one of the following form types to display its information:", $shortClassName), $classes, $classes[0]);
1365 }
1366
1367 private function getFqcnTypeClasses(string $shortClassName): array
1368 {
1369 $classes = [];
1370 sort($this->namespaces);
1371 foreach ($this->namespaces as $namespace) {
1372 if (class_exists($fqcn = $namespace.'\\'.$shortClassName)) {
1373 $classes[] = $fqcn;
1374 } elseif (class_exists($fqcn = $namespace.'\\'.ucfirst($shortClassName))) {
1375 $classes[] = $fqcn;
1376 } elseif (class_exists($fqcn = $namespace.'\\'.ucfirst($shortClassName).'Type')) {
1377 $classes[] = $fqcn;
1378 } elseif (str_ends_with($shortClassName, 'type') && class_exists($fqcn = $namespace.'\\'.ucfirst(substr($shortClassName, 0, -4).'Type'))) {
1379 $classes[] = $fqcn;
1380 }
1381 }
1382
1383 return $classes;
1384 }
1385
1386 private function getCoreTypes(): array
1387 {
1388 $coreExtension = new CoreExtension();
1389 $loadTypesRefMethod = (new \ReflectionObject($coreExtension))->getMethod('loadTypes');
1390 $coreTypes = $loadTypesRefMethod->invoke($coreExtension);
1391 $coreTypes = array_map(static fn (FormTypeInterface $type) => $type::class, $coreTypes);
1392 sort($coreTypes);
1393
1394 return $coreTypes;
1395 }
1396
1397 private function filterTypesByDeprecated(array $types): array
1398 {
1399 $typesWithDeprecatedOptions = [];
1400 foreach ($types as $class) {
1401 $optionsResolver = $this->formRegistry->getType($class)->getOptionsResolver();
1402 foreach ($optionsResolver->getDefinedOptions() as $option) {
1403 if ($optionsResolver->isDeprecated($option)) {
1404 $typesWithDeprecatedOptions[] = $class;
1405 break;
1406 }
1407 }
1408 }
1409
1410 return $typesWithDeprecatedOptions;
1411 }
1412
1413 private function findAlternatives(string $name, array $collection): array
1414 {
1415 $alternatives = [];
1416 foreach ($collection as $item) {
1417 $lev = levenshtein($name, $item);
1418 if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) {
1419 $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
1420 }
1421 }
1422
1423 $threshold = 1e3;
1424 $alternatives = array_filter($alternatives, static fn ($lev) => $lev < 2 * $threshold);
1425 ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);
1426
1427 return array_keys($alternatives);
1428 }
1429
1430 public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
1431 {
1432 if ($input->mustSuggestArgumentValuesFor('class')) {
1433 $suggestions->suggestValues(array_merge($this->getCoreTypes(), $this->types));
1434
1435 return;
1436 }
1437
1438 if ($input->mustSuggestArgumentValuesFor('option') && null !== $class = $input->getArgument('class')) {
1439 $this->completeOptions($class, $suggestions);
1440
1441 return;
1442 }
1443
1444 if ($input->mustSuggestOptionValuesFor('format')) {
1445 $suggestions->suggestValues($this->getAvailableFormatOptions());
1446 }
1447 }
1448
1449 private function completeOptions(string $class, CompletionSuggestions $suggestions): void
1450 {
1451 if (!class_exists($class) || !is_subclass_of($class, FormTypeInterface::class)) {
1452 $classes = $this->getFqcnTypeClasses($class);
1453
1454 if (1 === \count($classes)) {
1455 $class = $classes[0];
1456 }
1457 }
1458
1459 if (!$this->formRegistry->hasType($class)) {
1460 return;
1461 }
1462
1463 $resolvedType = $this->formRegistry->getType($class);
1464 $suggestions->suggestValues($resolvedType->getOptionsResolver()->getDefinedOptions());
1465 }
1466
1467 /** @return string[] */
1468 private function getAvailableFormatOptions(): array
1469 {
1470 return (new DescriptorHelper())->getFormats();
1471 }
1472}
1473
1474------------------------------------------------------------------------------------------------------------------------
1475 ./FormTypeExtensionInterface.php
1476------------------------------------------------------------------------------------------------------------------------
1477<?php
1478
1479/*
1480 * This file is part of the Symfony package.
1481 *
1482 * (c) Fabien Potencier <fabien@symfony.com>
1483 *
1484 * For the full copyright and license information, please view the LICENSE
1485 * file that was distributed with this source code.
1486 */
1487
1488namespace Symfony\Component\Form;
1489
1490use Symfony\Component\OptionsResolver\OptionsResolver;
1491
1492/**
1493 * @author Bernhard Schussek <bschussek@gmail.com>
1494 */
1495interface FormTypeExtensionInterface
1496{
1497 /**
1498 * Gets the extended types.
1499 *
1500 * @return string[]
1501 */
1502 public static function getExtendedTypes(): iterable;
1503
1504 public function configureOptions(OptionsResolver $resolver): void;
1505
1506 /**
1507 * Builds the form.
1508 *
1509 * This method is called after the extended type has built the form to
1510 * further modify it.
1511 *
1512 * @param array<string, mixed> $options
1513 *
1514 * @see FormTypeInterface::buildForm()
1515 */
1516 public function buildForm(FormBuilderInterface $builder, array $options): void;
1517
1518 /**
1519 * Builds the view.
1520 *
1521 * This method is called after the extended type has built the view to
1522 * further modify it.
1523 *
1524 * @param array<string, mixed> $options
1525 *
1526 * @see FormTypeInterface::buildView()
1527 */
1528 public function buildView(FormView $view, FormInterface $form, array $options): void;
1529
1530 /**
1531 * Finishes the view.
1532 *
1533 * This method is called after the extended type has finished the view to
1534 * further modify it.
1535 *
1536 * @param array<string, mixed> $options
1537 *
1538 * @see FormTypeInterface::finishView()
1539 */
1540 public function finishView(FormView $view, FormInterface $form, array $options): void;
1541}
1542
1543------------------------------------------------------------------------------------------------------------------------
1544 ./Form.php
1545------------------------------------------------------------------------------------------------------------------------
1546<?php
1547
1548/*
1549 * This file is part of the Symfony package.
1550 *
1551 * (c) Fabien Potencier <fabien@symfony.com>
1552 *
1553 * For the full copyright and license information, please view the LICENSE
1554 * file that was distributed with this source code.
1555 */
1556
1557namespace Symfony\Component\Form;
1558
1559use Symfony\Component\Form\Event\PostSetDataEvent;
1560use Symfony\Component\Form\Event\PostSubmitEvent;
1561use Symfony\Component\Form\Event\PreSetDataEvent;
1562use Symfony\Component\Form\Event\PreSubmitEvent;
1563use Symfony\Component\Form\Event\SubmitEvent;
1564use Symfony\Component\Form\Exception\AlreadySubmittedException;
1565use Symfony\Component\Form\Exception\LogicException;
1566use Symfony\Component\Form\Exception\OutOfBoundsException;
1567use Symfony\Component\Form\Exception\RuntimeException;
1568use Symfony\Component\Form\Exception\TransformationFailedException;
1569use Symfony\Component\Form\Extension\Core\Type\TextType;
1570use Symfony\Component\Form\Util\FormUtil;
1571use Symfony\Component\Form\Util\InheritDataAwareIterator;
1572use Symfony\Component\Form\Util\OrderedHashMap;
1573use Symfony\Component\PropertyAccess\PropertyPath;
1574use Symfony\Component\PropertyAccess\PropertyPathInterface;
1575
1576/**
1577 * Form represents a form.
1578 *
1579 * To implement your own form fields, you need to have a thorough understanding
1580 * of the data flow within a form. A form stores its data in three different
1581 * representations:
1582 *
1583 * (1) the "model" format required by the form's object
1584 * (2) the "normalized" format for internal processing
1585 * (3) the "view" format used for display simple fields
1586 * or map children model data for compound fields
1587 *
1588 * A date field, for example, may store a date as "Y-m-d" string (1) in the
1589 * object. To facilitate processing in the field, this value is normalized
1590 * to a DateTime object (2). In the HTML representation of your form, a
1591 * localized string (3) may be presented to and modified by the user, or it could be an array of values
1592 * to be mapped to choices fields.
1593 *
1594 * In most cases, format (1) and format (2) will be the same. For example,
1595 * a checkbox field uses a Boolean value for both internal processing and
1596 * storage in the object. In these cases you need to set a view transformer
1597 * to convert between formats (2) and (3). You can do this by calling
1598 * addViewTransformer().
1599 *
1600 * In some cases though it makes sense to make format (1) configurable. To
1601 * demonstrate this, let's extend our above date field to store the value
1602 * either as "Y-m-d" string or as timestamp. Internally we still want to
1603 * use a DateTime object for processing. To convert the data from string/integer
1604 * to DateTime you can set a model transformer by calling
1605 * addModelTransformer(). The normalized data is then converted to the displayed
1606 * data as described before.
1607 *
1608 * The conversions (1) -> (2) -> (3) use the transform methods of the transformers.
1609 * The conversions (3) -> (2) -> (1) use the reverseTransform methods of the transformers.
1610 *
1611 * @author Fabien Potencier <fabien@symfony.com>
1612 * @author Bernhard Schussek <bschussek@gmail.com>
1613 *
1614 * @implements \IteratorAggregate<string, FormInterface>
1615 */
1616class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterface
1617{
1618 private ?FormInterface $parent = null;
1619
1620 /**
1621 * A map of FormInterface instances.
1622 *
1623 * @var OrderedHashMap<FormInterface>
1624 */
1625 private OrderedHashMap $children;
1626
1627 /**
1628 * @var FormError[]
1629 */
1630 private array $errors = [];
1631
1632 private bool $submitted = false;
1633
1634 /**
1635 * The button that was used to submit the form.
1636 */
1637 private FormInterface|ClickableInterface|null $clickedButton = null;
1638
1639 private mixed $modelData = null;
1640 private mixed $normData = null;
1641 private mixed $viewData = null;
1642
1643 /**
1644 * The submitted values that don't belong to any children.
1645 */
1646 private array $extraData = [];
1647
1648 /**
1649 * The transformation failure generated during submission, if any.
1650 */
1651 private ?TransformationFailedException $transformationFailure = null;
1652
1653 /**
1654 * Whether the form's data has been initialized.
1655 *
1656 * When the data is initialized with its default value, that default value
1657 * is passed through the transformer chain in order to synchronize the
1658 * model, normalized and view format for the first time. This is done
1659 * lazily in order to save performance when {@link setData()} is called
1660 * manually, making the initialization with the configured default value
1661 * superfluous.
1662 */
1663 private bool $defaultDataSet = false;
1664
1665 /**
1666 * Whether setData() is currently being called.
1667 */
1668 private bool $lockSetData = false;
1669
1670 private string $name = '';
1671
1672 /**
1673 * Whether the form inherits its underlying data from its parent.
1674 */
1675 private bool $inheritData;
1676
1677 private ?PropertyPathInterface $propertyPath = null;
1678
1679 /**
1680 * @throws LogicException if a data mapper is not provided for a compound form
1681 */
1682 public function __construct(
1683 private FormConfigInterface $config,
1684 ) {
1685 // Compound forms always need a data mapper, otherwise calls to
1686 // `setData` and `add` will not lead to the correct population of
1687 // the child forms.
1688 if ($config->getCompound() && !$config->getDataMapper()) {
1689 throw new LogicException('Compound forms need a data mapper.');
1690 }
1691
1692 // If the form inherits the data from its parent, it is not necessary
1693 // to call setData() with the default data.
1694 if ($this->inheritData = $config->getInheritData()) {
1695 $this->defaultDataSet = true;
1696 }
1697
1698 $this->children = new OrderedHashMap();
1699 $this->name = $config->getName();
1700 }
1701
1702 public function __clone()
1703 {
1704 $this->children = clone $this->children;
1705
1706 foreach ($this->children as $key => $child) {
1707 $this->children[$key] = clone $child;
1708 }
1709 }
1710
1711 public function getConfig(): FormConfigInterface
1712 {
1713 return $this->config;
1714 }
1715
1716 public function getName(): string
1717 {
1718 return $this->name;
1719 }
1720
1721 public function getPropertyPath(): ?PropertyPathInterface
1722 {
1723 if ($this->propertyPath || $this->propertyPath = $this->config->getPropertyPath()) {
1724 return $this->propertyPath;
1725 }
1726
1727 if ('' === $this->name) {
1728 return null;
1729 }
1730
1731 $parent = $this->parent;
1732
1733 while ($parent?->getConfig()->getInheritData()) {
1734 $parent = $parent->getParent();
1735 }
1736
1737 if ($parent && null === $parent->getConfig()->getDataClass()) {
1738 $this->propertyPath = new PropertyPath('['.$this->name.']');
1739 } else {
1740 $this->propertyPath = new PropertyPath($this->name);
1741 }
1742
1743 return $this->propertyPath;
1744 }
1745
1746 public function isRequired(): bool
1747 {
1748 if (null === $this->parent || $this->parent->isRequired()) {
1749 return $this->config->getRequired();
1750 }
1751
1752 return false;
1753 }
1754
1755 public function isDisabled(): bool
1756 {
1757 if (null === $this->parent || !$this->parent->isDisabled()) {
1758 return $this->config->getDisabled();
1759 }
1760
1761 return true;
1762 }
1763
1764 public function setParent(?FormInterface $parent): static
1765 {
1766 if ($this->submitted) {
1767 throw new AlreadySubmittedException('You cannot set the parent of a submitted form.');
1768 }
1769
1770 if (null !== $parent && '' === $this->name) {
1771 throw new LogicException('A form with an empty name cannot have a parent form.');
1772 }
1773
1774 $this->parent = $parent;
1775
1776 return $this;
1777 }
1778
1779 public function getParent(): ?FormInterface
1780 {
1781 return $this->parent;
1782 }
1783
1784 public function getRoot(): FormInterface
1785 {
1786 return $this->parent ? $this->parent->getRoot() : $this;
1787 }
1788
1789 public function isRoot(): bool
1790 {
1791 return null === $this->parent;
1792 }
1793
1794 public function setData(mixed $modelData): static
1795 {
1796 // If the form is submitted while disabled, it is set to submitted, but the data is not
1797 // changed. In such cases (i.e. when the form is not initialized yet) don't
1798 // abort this method.
1799 if ($this->submitted && $this->defaultDataSet) {
1800 throw new AlreadySubmittedException('You cannot change the data of a submitted form.');
1801 }
1802
1803 // If the form inherits its parent's data, disallow data setting to
1804 // prevent merge conflicts
1805 if ($this->inheritData) {
1806 throw new RuntimeException('You cannot change the data of a form inheriting its parent data.');
1807 }
1808
1809 // Don't allow modifications of the configured data if the data is locked
1810 if ($this->config->getDataLocked() && $modelData !== $this->config->getData()) {
1811 return $this;
1812 }
1813
1814 if (\is_object($modelData) && !$this->config->getByReference()) {
1815 $modelData = clone $modelData;
1816 }
1817
1818 if ($this->lockSetData) {
1819 throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call setData(). You should call setData() on the FormEvent object instead.');
1820 }
1821
1822 $this->lockSetData = true;
1823 $dispatcher = $this->config->getEventDispatcher();
1824
1825 // Hook to change content of the model data before transformation and mapping children
1826 if ($dispatcher->hasListeners(FormEvents::PRE_SET_DATA)) {
1827 $event = new PreSetDataEvent($this, $modelData);
1828 $dispatcher->dispatch($event, FormEvents::PRE_SET_DATA);
1829 $modelData = $event->getData();
1830 }
1831
1832 // Treat data as strings unless a transformer exists
1833 if (\is_scalar($modelData) && !$this->config->getViewTransformers() && !$this->config->getModelTransformers()) {
1834 $modelData = (string) $modelData;
1835 }
1836
1837 // Synchronize representations - must not change the content!
1838 // Transformation exceptions are not caught on initialization
1839 $normData = $this->modelToNorm($modelData);
1840 $viewData = $this->normToView($normData);
1841
1842 // Validate if view data matches data class (unless empty)
1843 if (!FormUtil::isEmpty($viewData)) {
1844 $dataClass = $this->config->getDataClass();
1845
1846 if (null !== $dataClass && !$viewData instanceof $dataClass) {
1847 $actualType = get_debug_type($viewData);
1848
1849 throw new LogicException('The form\'s view data is expected to be a "'.$dataClass.'", but it is a "'.$actualType.'". You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms "'.$actualType.'" to an instance of "'.$dataClass.'".');
1850 }
1851 }
1852
1853 $this->modelData = $modelData;
1854 $this->normData = $normData;
1855 $this->viewData = $viewData;
1856 $this->defaultDataSet = true;
1857 $this->lockSetData = false;
1858
1859 // Compound forms don't need to invoke this method if they don't have children
1860 if (\count($this->children) > 0) {
1861 // Update child forms from the data (unless their config data is locked)
1862 $this->config->getDataMapper()->mapDataToForms($viewData, new \RecursiveIteratorIterator(new InheritDataAwareIterator($this->children)));
1863 }
1864
1865 if ($dispatcher->hasListeners(FormEvents::POST_SET_DATA)) {
1866 $event = new PostSetDataEvent($this, $modelData);
1867 $dispatcher->dispatch($event, FormEvents::POST_SET_DATA);
1868 }
1869
1870 return $this;
1871 }
1872
1873 public function getData(): mixed
1874 {
1875 if ($this->inheritData) {
1876 if (!$this->parent) {
1877 throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
1878 }
1879
1880 return $this->parent->getData();
1881 }
1882
1883 if (!$this->defaultDataSet) {
1884 if ($this->lockSetData) {
1885 throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getData() if the form data has not already been set. You should call getData() on the FormEvent object instead.');
1886 }
1887
1888 $this->setData($this->config->getData());
1889 }
1890
1891 return $this->modelData;
1892 }
1893
1894 public function getNormData(): mixed
1895 {
1896 if ($this->inheritData) {
1897 if (!$this->parent) {
1898 throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
1899 }
1900
1901 return $this->parent->getNormData();
1902 }
1903
1904 if (!$this->defaultDataSet) {
1905 if ($this->lockSetData) {
1906 throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getNormData() if the form data has not already been set.');
1907 }
1908
1909 $this->setData($this->config->getData());
1910 }
1911
1912 return $this->normData;
1913 }
1914
1915 public function getViewData(): mixed
1916 {
1917 if ($this->inheritData) {
1918 if (!$this->parent) {
1919 throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
1920 }
1921
1922 return $this->parent->getViewData();
1923 }
1924
1925 if (!$this->defaultDataSet) {
1926 if ($this->lockSetData) {
1927 throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getViewData() if the form data has not already been set.');
1928 }
1929
1930 $this->setData($this->config->getData());
1931 }
1932
1933 return $this->viewData;
1934 }
1935
1936 public function getExtraData(): array
1937 {
1938 return $this->extraData;
1939 }
1940
1941 public function initialize(): static
1942 {
1943 if (null !== $this->parent) {
1944 throw new RuntimeException('Only root forms should be initialized.');
1945 }
1946
1947 // Guarantee that the *_SET_DATA events have been triggered once the
1948 // form is initialized. This makes sure that dynamically added or
1949 // removed fields are already visible after initialization.
1950 if (!$this->defaultDataSet) {
1951 $this->setData($this->config->getData());
1952 }
1953
1954 return $this;
1955 }
1956
1957 public function handleRequest(mixed $request = null): static
1958 {
1959 $this->config->getRequestHandler()->handleRequest($this, $request);
1960
1961 return $this;
1962 }
1963
1964 public function submit(mixed $submittedData, bool $clearMissing = true): static
1965 {
1966 if ($this->submitted) {
1967 throw new AlreadySubmittedException('A form can only be submitted once.');
1968 }
1969
1970 // Initialize errors in the very beginning so we're sure
1971 // they are collectable during submission only
1972 $this->errors = [];
1973
1974 // Obviously, a disabled form should not change its data upon submission.
1975 if ($this->isDisabled()) {
1976 $this->submitted = true;
1977
1978 return $this;
1979 }
1980
1981 // The data must be initialized if it was not initialized yet.
1982 // This is necessary to guarantee that the *_SET_DATA listeners
1983 // are always invoked before submit() takes place.
1984 if (!$this->defaultDataSet) {
1985 $this->setData($this->config->getData());
1986 }
1987
1988 // Treat false as NULL to support binding false to checkboxes.
1989 // Don't convert NULL to a string here in order to determine later
1990 // whether an empty value has been submitted or whether no value has
1991 // been submitted at all. This is important for processing checkboxes
1992 // and radio buttons with empty values.
1993 if (false === $submittedData) {
1994 $submittedData = null;
1995 } elseif (\is_scalar($submittedData)) {
1996 $submittedData = (string) $submittedData;
1997 } elseif ($this->config->getRequestHandler()->isFileUpload($submittedData)) {
1998 if (!$this->config->getOption('allow_file_upload')) {
1999 $submittedData = null;
2000 $this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, file upload given.');
2001 }
2002 } elseif (\is_array($submittedData) && !$this->config->getCompound() && !$this->config->getOption('multiple', false)) {
2003 $submittedData = null;
2004 $this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, array given.');
2005 }
2006
2007 $dispatcher = $this->config->getEventDispatcher();
2008
2009 $modelData = null;
2010 $normData = null;
2011 $viewData = null;
2012
2013 try {
2014 if (null !== $this->transformationFailure) {
2015 throw $this->transformationFailure;
2016 }
2017
2018 // Hook to change content of the data submitted by the browser
2019 if ($dispatcher->hasListeners(FormEvents::PRE_SUBMIT)) {
2020 $event = new PreSubmitEvent($this, $submittedData);
2021 $dispatcher->dispatch($event, FormEvents::PRE_SUBMIT);
2022 $submittedData = $event->getData();
2023 }
2024
2025 // Check whether the form is compound.
2026 // This check is preferable over checking the number of children,
2027 // since forms without children may also be compound.
2028 // (think of empty collection forms)
2029 if ($this->config->getCompound()) {
2030 if (!\is_array($submittedData ??= [])) {
2031 throw new TransformationFailedException('Compound forms expect an array or NULL on submission.');
2032 }
2033
2034 foreach ($this->children as $name => $child) {
2035 $isSubmitted = \array_key_exists($name, $submittedData);
2036
2037 if ($isSubmitted || $clearMissing) {
2038 $child->submit($isSubmitted ? $submittedData[$name] : null, $clearMissing);
2039 unset($submittedData[$name]);
2040
2041 if (null !== $this->clickedButton) {
2042 continue;
2043 }
2044
2045 if ($child instanceof ClickableInterface && $child->isClicked()) {
2046 $this->clickedButton = $child;
2047
2048 continue;
2049 }
2050
2051 if (method_exists($child, 'getClickedButton') && null !== $child->getClickedButton()) {
2052 $this->clickedButton = $child->getClickedButton();
2053 }
2054 }
2055 }
2056
2057 $this->extraData = $submittedData;
2058 }
2059
2060 // Forms that inherit their parents' data also are not processed,
2061 // because then it would be too difficult to merge the changes in
2062 // the child and the parent form. Instead, the parent form also takes
2063 // changes in the grandchildren (i.e. children of the form that inherits
2064 // its parent's data) into account.
2065 // (see InheritDataAwareIterator below)
2066 if (!$this->inheritData) {
2067 // If the form is compound, the view data is merged with the data
2068 // of the children using the data mapper.
2069 // If the form is not compound, the view data is assigned to the submitted data.
2070 $viewData = $this->config->getCompound() ? $this->viewData : $submittedData;
2071
2072 if (FormUtil::isEmpty($viewData)) {
2073 $emptyData = $this->config->getEmptyData();
2074
2075 if ($emptyData instanceof \Closure) {
2076 $emptyData = $emptyData($this, $viewData);
2077 }
2078
2079 $viewData = $emptyData;
2080 }
2081
2082 // Merge form data from children into existing view data
2083 // It is not necessary to invoke this method if the form has no children,
2084 // even if it is compound.
2085 if (\count($this->children) > 0) {
2086 // Use InheritDataAwareIterator to process children of
2087 // descendants that inherit this form's data.
2088 // These descendants will not be submitted normally (see the check
2089 // for $this->config->getInheritData() above)
2090 $this->config->getDataMapper()->mapFormsToData(
2091 new \RecursiveIteratorIterator(new InheritDataAwareIterator($this->children)),
2092 $viewData
2093 );
2094 }
2095
2096 // Normalize data to unified representation
2097 $normData = $this->viewToNorm($viewData);
2098
2099 // Hook to change content of the data in the normalized
2100 // representation
2101 if ($dispatcher->hasListeners(FormEvents::SUBMIT)) {
2102 $event = new SubmitEvent($this, $normData);
2103 $dispatcher->dispatch($event, FormEvents::SUBMIT);
2104 $normData = $event->getData();
2105 }
2106
2107 // Synchronize representations - must not change the content!
2108 $modelData = $this->normToModel($normData);
2109 $viewData = $this->normToView($normData);
2110 }
2111 } catch (TransformationFailedException $e) {
2112 $this->transformationFailure = $e;
2113
2114 // If $viewData was not yet set, set it to $submittedData so that
2115 // the erroneous data is accessible on the form.
2116 // Forms that inherit data never set any data, because the getters
2117 // forward to the parent form's getters anyway.
2118 if (null === $viewData && !$this->inheritData) {
2119 $viewData = $submittedData;
2120 }
2121 }
2122
2123 $this->submitted = true;
2124 $this->modelData = $modelData;
2125 $this->normData = $normData;
2126 $this->viewData = $viewData;
2127
2128 if ($dispatcher->hasListeners(FormEvents::POST_SUBMIT)) {
2129 $event = new PostSubmitEvent($this, $viewData);
2130 $dispatcher->dispatch($event, FormEvents::POST_SUBMIT);
2131 }
2132
2133 return $this;
2134 }
2135
2136 public function addError(FormError $error): static
2137 {
2138 if (null === $error->getOrigin()) {
2139 $error->setOrigin($this);
2140 }
2141
2142 if ($this->parent && $this->config->getErrorBubbling()) {
2143 $this->parent->addError($error);
2144 } else {
2145 $this->errors[] = $error;
2146 }
2147
2148 return $this;
2149 }
2150
2151 public function isSubmitted(): bool
2152 {
2153 return $this->submitted;
2154 }
2155
2156 public function isSynchronized(): bool
2157 {
2158 return null === $this->transformationFailure;
2159 }
2160
2161 public function getTransformationFailure(): ?TransformationFailedException
2162 {
2163 return $this->transformationFailure;
2164 }
2165
2166 public function isEmpty(): bool
2167 {
2168 foreach ($this->children as $child) {
2169 if (!$child->isEmpty()) {
2170 return false;
2171 }
2172 }
2173
2174 if (null !== $isEmptyCallback = $this->config->getIsEmptyCallback()) {
2175 return $isEmptyCallback($this->modelData);
2176 }
2177
2178 return FormUtil::isEmpty($this->modelData)
2179 // arrays, countables
2180 || (is_countable($this->modelData) && 0 === \count($this->modelData))
2181 // traversables that are not countable
2182 || ($this->modelData instanceof \Traversable && 0 === iterator_count($this->modelData));
2183 }
2184
2185 public function isValid(): bool
2186 {
2187 if (!$this->submitted) {
2188 throw new LogicException('Cannot check if an unsubmitted form is valid. Call Form::isSubmitted() and ensure that it\'s true before calling Form::isValid().');
2189 }
2190
2191 if ($this->isDisabled()) {
2192 return true;
2193 }
2194
2195 return 0 === \count($this->getErrors(true));
2196 }
2197
2198 /**
2199 * Returns the button that was used to submit the form.
2200 */
2201 public function getClickedButton(): FormInterface|ClickableInterface|null
2202 {
2203 if ($this->clickedButton) {
2204 return $this->clickedButton;
2205 }
2206
2207 return $this->parent && method_exists($this->parent, 'getClickedButton') ? $this->parent->getClickedButton() : null;
2208 }
2209
2210 public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator
2211 {
2212 $errors = $this->errors;
2213
2214 // Copy the errors of nested forms to the $errors array
2215 if ($deep) {
2216 foreach ($this as $child) {
2217 /** @var FormInterface $child */
2218 if ($child->isSubmitted() && $child->isValid()) {
2219 continue;
2220 }
2221
2222 $iterator = $child->getErrors(true, $flatten);
2223
2224 if (0 === \count($iterator)) {
2225 continue;
2226 }
2227
2228 if ($flatten) {
2229 foreach ($iterator as $error) {
2230 $errors[] = $error;
2231 }
2232 } else {
2233 $errors[] = $iterator;
2234 }
2235 }
2236 }
2237
2238 return new FormErrorIterator($this, $errors);
2239 }
2240
2241 public function clearErrors(bool $deep = false): static
2242 {
2243 $this->errors = [];
2244
2245 if ($deep) {
2246 // Clear errors from children
2247 foreach ($this as $child) {
2248 if ($child instanceof ClearableErrorsInterface) {
2249 $child->clearErrors(true);
2250 }
2251 }
2252 }
2253
2254 return $this;
2255 }
2256
2257 public function all(): array
2258 {
2259 return iterator_to_array($this->children);
2260 }
2261
2262 public function add(FormInterface|string $child, ?string $type = null, array $options = []): static
2263 {
2264 if ($this->submitted) {
2265 throw new AlreadySubmittedException('You cannot add children to a submitted form.');
2266 }
2267
2268 if (!$this->config->getCompound()) {
2269 throw new LogicException('You cannot add children to a simple form. Maybe you should set the option "compound" to true?');
2270 }
2271
2272 if (!$child instanceof FormInterface) {
2273 // Never initialize child forms automatically
2274 $options['auto_initialize'] = false;
2275
2276 if (null === $type && null === $this->config->getDataClass()) {
2277 $type = TextType::class;
2278 }
2279
2280 if (null === $type) {
2281 $child = $this->config->getFormFactory()->createForProperty($this->config->getDataClass(), $child, null, $options);
2282 } else {
2283 $child = $this->config->getFormFactory()->createNamed($child, $type, null, $options);
2284 }
2285 } elseif ($child->getConfig()->getAutoInitialize()) {
2286 throw new RuntimeException(\sprintf('Automatic initialization is only supported on root forms. You should set the "auto_initialize" option to false on the field "%s".', $child->getName()));
2287 }
2288
2289 $this->children[$child->getName()] = $child;
2290
2291 $child->setParent($this);
2292
2293 // If setData() is currently being called, there is no need to call
2294 // mapDataToForms() here, as mapDataToForms() is called at the end
2295 // of setData() anyway. Not doing this check leads to an endless
2296 // recursion when initializing the form lazily and an event listener
2297 // (such as ResizeFormListener) adds fields depending on the data:
2298 //
2299 // * setData() is called, the form is not initialized yet
2300 // * add() is called by the listener (setData() is not complete, so
2301 // the form is still not initialized)
2302 // * getViewData() is called
2303 // * setData() is called since the form is not initialized yet
2304 // * ... endless recursion ...
2305 //
2306 // Also skip data mapping if setData() has not been called yet.
2307 // setData() will be called upon form initialization and data mapping
2308 // will take place by then.
2309 if (!$this->lockSetData && $this->defaultDataSet && !$this->inheritData) {
2310 $viewData = $this->getViewData();
2311 $this->config->getDataMapper()->mapDataToForms(
2312 $viewData,
2313 new \RecursiveIteratorIterator(new InheritDataAwareIterator(new \ArrayIterator([$child->getName() => $child])))
2314 );
2315 }
2316
2317 return $this;
2318 }
2319
2320 public function remove(string $name): static
2321 {
2322 if ($this->submitted) {
2323 throw new AlreadySubmittedException('You cannot remove children from a submitted form.');
2324 }
2325
2326 if (isset($this->children[$name])) {
2327 if (!$this->children[$name]->isSubmitted()) {
2328 $this->children[$name]->setParent(null);
2329 }
2330
2331 unset($this->children[$name]);
2332 }
2333
2334 return $this;
2335 }
2336
2337 public function has(string $name): bool
2338 {
2339 return isset($this->children[$name]);
2340 }
2341
2342 public function get(string $name): FormInterface
2343 {
2344 if (isset($this->children[$name])) {
2345 return $this->children[$name];
2346 }
2347
2348 throw new OutOfBoundsException(\sprintf('Child "%s" does not exist.', $name));
2349 }
2350
2351 /**
2352 * Returns whether a child with the given name exists (implements the \ArrayAccess interface).
2353 *
2354 * @param string $name The name of the child
2355 */
2356 public function offsetExists(mixed $name): bool
2357 {
2358 return $this->has($name);
2359 }
2360
2361 /**
2362 * Returns the child with the given name (implements the \ArrayAccess interface).
2363 *
2364 * @param string $name The name of the child
2365 *
2366 * @throws OutOfBoundsException if the named child does not exist
2367 */
2368 public function offsetGet(mixed $name): FormInterface
2369 {
2370 return $this->get($name);
2371 }
2372
2373 /**
2374 * Adds a child to the form (implements the \ArrayAccess interface).
2375 *
2376 * @param string $name Ignored. The name of the child is used
2377 * @param FormInterface $child The child to be added
2378 *
2379 * @throws AlreadySubmittedException if the form has already been submitted
2380 * @throws LogicException when trying to add a child to a non-compound form
2381 *
2382 * @see self::add()
2383 */
2384 public function offsetSet(mixed $name, mixed $child): void
2385 {
2386 $this->add($child);
2387 }
2388
2389 /**
2390 * Removes the child with the given name from the form (implements the \ArrayAccess interface).
2391 *
2392 * @param string $name The name of the child to remove
2393 *
2394 * @throws AlreadySubmittedException if the form has already been submitted
2395 */
2396 public function offsetUnset(mixed $name): void
2397 {
2398 $this->remove($name);
2399 }
2400
2401 /**
2402 * Returns the iterator for this group.
2403 *
2404 * @return \Traversable<string, FormInterface>
2405 */
2406 public function getIterator(): \Traversable
2407 {
2408 return $this->children;
2409 }
2410
2411 /**
2412 * Returns the number of form children (implements the \Countable interface).
2413 */
2414 public function count(): int
2415 {
2416 return \count($this->children);
2417 }
2418
2419 public function createView(?FormView $parent = null): FormView
2420 {
2421 if (null === $parent && $this->parent) {
2422 $parent = $this->parent->createView();
2423 }
2424
2425 $type = $this->config->getType();
2426 $options = $this->config->getOptions();
2427
2428 // The methods createView(), buildView() and finishView() are called
2429 // explicitly here in order to be able to override either of them
2430 // in a custom resolved form type.
2431 $view = $type->createView($this, $parent);
2432
2433 $type->buildView($view, $this, $options);
2434
2435 foreach ($this->children as $name => $child) {
2436 $view->children[$name] = $child->createView($view);
2437 }
2438
2439 $this->sort($view->children);
2440
2441 $type->finishView($view, $this, $options);
2442
2443 return $view;
2444 }
2445
2446 /**
2447 * Sorts view fields based on their priority value.
2448 */
2449 private function sort(array &$children): void
2450 {
2451 $c = [];
2452 $i = 0;
2453 $needsSorting = false;
2454 foreach ($children as $name => $child) {
2455 $c[$name] = ['p' => $child->vars['priority'] ?? 0, 'i' => $i++];
2456
2457 if (0 !== $c[$name]['p']) {
2458 $needsSorting = true;
2459 }
2460 }
2461
2462 if (!$needsSorting) {
2463 return;
2464 }
2465
2466 uksort($children, static fn ($a, $b): int => [$c[$b]['p'], $c[$a]['i']] <=> [$c[$a]['p'], $c[$b]['i']]);
2467 }
2468
2469 /**
2470 * Normalizes the underlying data if a model transformer is set.
2471 *
2472 * @throws TransformationFailedException If the underlying data cannot be transformed to "normalized" format
2473 */
2474 private function modelToNorm(mixed $value): mixed
2475 {
2476 try {
2477 foreach ($this->config->getModelTransformers() as $transformer) {
2478 $value = $transformer->transform($value);
2479 }
2480 } catch (TransformationFailedException $exception) {
2481 throw new TransformationFailedException(\sprintf('Unable to transform data for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
2482 }
2483
2484 return $value;
2485 }
2486
2487 /**
2488 * Reverse transforms a value if a model transformer is set.
2489 *
2490 * @throws TransformationFailedException If the value cannot be transformed to "model" format
2491 */
2492 private function normToModel(mixed $value): mixed
2493 {
2494 try {
2495 $transformers = $this->config->getModelTransformers();
2496
2497 for ($i = \count($transformers) - 1; $i >= 0; --$i) {
2498 $value = $transformers[$i]->reverseTransform($value);
2499 }
2500 } catch (TransformationFailedException $exception) {
2501 throw new TransformationFailedException(\sprintf('Unable to reverse value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
2502 }
2503
2504 return $value;
2505 }
2506
2507 /**
2508 * Transforms the value if a view transformer is set.
2509 *
2510 * @throws TransformationFailedException If the normalized value cannot be transformed to "view" format
2511 */
2512 private function normToView(mixed $value): mixed
2513 {
2514 // Scalar values should be converted to strings to
2515 // facilitate differentiation between empty ("") and zero (0).
2516 // Only do this for simple forms, as the resulting value in
2517 // compound forms is passed to the data mapper and thus should
2518 // not be converted to a string before.
2519 if (!($transformers = $this->config->getViewTransformers()) && !$this->config->getCompound()) {
2520 return null === $value || \is_scalar($value) ? (string) $value : $value;
2521 }
2522
2523 try {
2524 foreach ($transformers as $transformer) {
2525 $value = $transformer->transform($value);
2526 }
2527 } catch (TransformationFailedException $exception) {
2528 throw new TransformationFailedException(\sprintf('Unable to transform value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
2529 }
2530
2531 return $value;
2532 }
2533
2534 /**
2535 * Reverse transforms a value if a view transformer is set.
2536 *
2537 * @throws TransformationFailedException If the submitted value cannot be transformed to "normalized" format
2538 */
2539 private function viewToNorm(mixed $value): mixed
2540 {
2541 if (!$transformers = $this->config->getViewTransformers()) {
2542 return '' === $value ? null : $value;
2543 }
2544
2545 try {
2546 for ($i = \count($transformers) - 1; $i >= 0; --$i) {
2547 $value = $transformers[$i]->reverseTransform($value);
2548 }
2549 } catch (TransformationFailedException $exception) {
2550 throw new TransformationFailedException(\sprintf('Unable to reverse value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
2551 }
2552
2553 return $value;
2554 }
2555}
2556
2557------------------------------------------------------------------------------------------------------------------------
2558 ./FormInterface.php
2559------------------------------------------------------------------------------------------------------------------------
2560<?php
2561
2562/*
2563 * This file is part of the Symfony package.
2564 *
2565 * (c) Fabien Potencier <fabien@symfony.com>
2566 *
2567 * For the full copyright and license information, please view the LICENSE
2568 * file that was distributed with this source code.
2569 */
2570
2571namespace Symfony\Component\Form;
2572
2573use Symfony\Component\PropertyAccess\PropertyPathInterface;
2574
2575/**
2576 * A form group bundling multiple forms in a hierarchical structure.
2577 *
2578 * @author Bernhard Schussek <bschussek@gmail.com>
2579 *
2580 * @extends \ArrayAccess<string, FormInterface>
2581 * @extends \Traversable<string, FormInterface>
2582 */
2583interface FormInterface extends \ArrayAccess, \Traversable, \Countable
2584{
2585 /**
2586 * Sets the parent form.
2587 *
2588 * @param FormInterface|null $parent The parent form or null if it's the root
2589 *
2590 * @return $this
2591 *
2592 * @throws Exception\AlreadySubmittedException if the form has already been submitted
2593 * @throws Exception\LogicException when trying to set a parent for a form with
2594 * an empty name
2595 */
2596 public function setParent(?self $parent): static;
2597
2598 /**
2599 * Returns the parent form.
2600 */
2601 public function getParent(): ?self;
2602
2603 /**
2604 * Adds or replaces a child to the form.
2605 *
2606 * @param FormInterface|string $child The FormInterface instance or the name of the child
2607 * @param string|null $type The child's type, if a name was passed
2608 * @param array $options The child's options, if a name was passed
2609 *
2610 * @return $this
2611 *
2612 * @throws Exception\AlreadySubmittedException if the form has already been submitted
2613 * @throws Exception\LogicException when trying to add a child to a non-compound form
2614 * @throws Exception\UnexpectedTypeException if $child or $type has an unexpected type
2615 */
2616 public function add(self|string $child, ?string $type = null, array $options = []): static;
2617
2618 /**
2619 * Returns the child with the given name.
2620 *
2621 * @throws Exception\OutOfBoundsException if the named child does not exist
2622 */
2623 public function get(string $name): self;
2624
2625 /**
2626 * Returns whether a child with the given name exists.
2627 */
2628 public function has(string $name): bool;
2629
2630 /**
2631 * Removes a child from the form.
2632 *
2633 * @return $this
2634 *
2635 * @throws Exception\AlreadySubmittedException if the form has already been submitted
2636 */
2637 public function remove(string $name): static;
2638
2639 /**
2640 * Returns all children in this group.
2641 *
2642 * @return self[]
2643 */
2644 public function all(): array;
2645
2646 /**
2647 * Returns the errors of this form.
2648 *
2649 * @param bool $deep Whether to include errors of child forms as well
2650 * @param bool $flatten Whether to flatten the list of errors in case
2651 * $deep is set to true
2652 */
2653 public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator;
2654
2655 /**
2656 * Updates the form with default model data.
2657 *
2658 * @param mixed $modelData The data formatted as expected for the underlying object
2659 *
2660 * @return $this
2661 *
2662 * @throws Exception\AlreadySubmittedException If the form has already been submitted
2663 * @throws Exception\LogicException if the view data does not match the expected type
2664 * according to {@link FormConfigInterface::getDataClass}
2665 * @throws Exception\RuntimeException If listeners try to call setData in a cycle or if
2666 * the form inherits data from its parent
2667 * @throws Exception\TransformationFailedException if the synchronization failed
2668 */
2669 public function setData(mixed $modelData): static;
2670
2671 /**
2672 * Returns the model data in the format needed for the underlying object.
2673 *
2674 * @return mixed When the field is not submitted, the default data is returned.
2675 * When the field is submitted, the default data has been bound
2676 * to the submitted view data.
2677 *
2678 * @throws Exception\RuntimeException If the form inherits data but has no parent
2679 */
2680 public function getData(): mixed;
2681
2682 /**
2683 * Returns the normalized data of the field, used as internal bridge
2684 * between model data and view data.
2685 *
2686 * @return mixed When the field is not submitted, the default data is returned.
2687 * When the field is submitted, the normalized submitted data
2688 * is returned if the field is synchronized with the view data,
2689 * null otherwise.
2690 *
2691 * @throws Exception\RuntimeException If the form inherits data but has no parent
2692 */
2693 public function getNormData(): mixed;
2694
2695 /**
2696 * Returns the view data of the field.
2697 *
2698 * It may be defined by {@link FormConfigInterface::getDataClass}.
2699 *
2700 * There are two cases:
2701 *
2702 * - When the form is compound the view data is mapped to the children.
2703 * Each child will use its mapped data as model data.
2704 * It can be an array, an object or null.
2705 *
2706 * - When the form is simple its view data is used to be bound
2707 * to the submitted data.
2708 * It can be a string or an array.
2709 *
2710 * In both cases the view data is the actual altered data on submission.
2711 *
2712 * @throws Exception\RuntimeException If the form inherits data but has no parent
2713 */
2714 public function getViewData(): mixed;
2715
2716 /**
2717 * Returns the extra submitted data.
2718 *
2719 * @return array The submitted data which do not belong to a child
2720 */
2721 public function getExtraData(): array;
2722
2723 /**
2724 * Returns the form's configuration.
2725 */
2726 public function getConfig(): FormConfigInterface;
2727
2728 /**
2729 * Returns whether the form is submitted.
2730 */
2731 public function isSubmitted(): bool;
2732
2733 /**
2734 * Returns the name by which the form is identified in forms.
2735 *
2736 * Only root forms are allowed to have an empty name.
2737 */
2738 public function getName(): string;
2739
2740 /**
2741 * Returns the property path that the form is mapped to.
2742 */
2743 public function getPropertyPath(): ?PropertyPathInterface;
2744
2745 /**
2746 * Adds an error to this form.
2747 *
2748 * @return $this
2749 */
2750 public function addError(FormError $error): static;
2751
2752 /**
2753 * Returns whether the form and all children are valid.
2754 *
2755 * @throws Exception\LogicException if the form is not submitted
2756 */
2757 public function isValid(): bool;
2758
2759 /**
2760 * Returns whether the form is required to be filled out.
2761 *
2762 * If the form has a parent and the parent is not required, this method
2763 * will always return false. Otherwise the value set with setRequired()
2764 * is returned.
2765 */
2766 public function isRequired(): bool;
2767
2768 /**
2769 * Returns whether this form is disabled.
2770 *
2771 * The content of a disabled form is displayed, but not allowed to be
2772 * modified. The validation of modified disabled forms should fail.
2773 *
2774 * Forms whose parents are disabled are considered disabled regardless of
2775 * their own state.
2776 */
2777 public function isDisabled(): bool;
2778
2779 /**
2780 * Returns whether the form is empty.
2781 */
2782 public function isEmpty(): bool;
2783
2784 /**
2785 * Returns whether the data in the different formats is synchronized.
2786 *
2787 * If the data is not synchronized, you can get the transformation failure
2788 * by calling {@link getTransformationFailure()}.
2789 *
2790 * If the form is not submitted, this method always returns true.
2791 */
2792 public function isSynchronized(): bool;
2793
2794 /**
2795 * Returns the data transformation failure, if any, during submission.
2796 */
2797 public function getTransformationFailure(): ?Exception\TransformationFailedException;
2798
2799 /**
2800 * Initializes the form tree.
2801 *
2802 * Should be called on the root form after constructing the tree.
2803 *
2804 * @return $this
2805 *
2806 * @throws Exception\RuntimeException If the form is not the root
2807 */
2808 public function initialize(): static;
2809
2810 /**
2811 * Inspects the given request and calls {@link submit()} if the form was
2812 * submitted.
2813 *
2814 * Internally, the request is forwarded to the configured
2815 * {@link RequestHandlerInterface} instance, which determines whether to
2816 * submit the form or not.
2817 *
2818 * @return $this
2819 */
2820 public function handleRequest(mixed $request = null): static;
2821
2822 /**
2823 * Submits data to the form.
2824 *
2825 * @param string|array|null $submittedData The submitted data
2826 * @param bool $clearMissing Whether to set fields to NULL
2827 * when they are missing in the
2828 * submitted data. This argument
2829 * is only used in compound form
2830 *
2831 * @return $this
2832 *
2833 * @throws Exception\AlreadySubmittedException if the form has already been submitted
2834 */
2835 public function submit(string|array|null $submittedData, bool $clearMissing = true): static;
2836
2837 /**
2838 * Returns the root of the form tree.
2839 */
2840 public function getRoot(): self;
2841
2842 /**
2843 * Returns whether the field is the root of the form tree.
2844 */
2845 public function isRoot(): bool;
2846
2847 public function createView(?FormView $parent = null): FormView;
2848}
2849
2850------------------------------------------------------------------------------------------------------------------------
2851 ./FormError.php
2852------------------------------------------------------------------------------------------------------------------------
2853<?php
2854
2855/*
2856 * This file is part of the Symfony package.
2857 *
2858 * (c) Fabien Potencier <fabien@symfony.com>
2859 *
2860 * For the full copyright and license information, please view the LICENSE
2861 * file that was distributed with this source code.
2862 */
2863
2864namespace Symfony\Component\Form;
2865
2866use Symfony\Component\Form\Exception\BadMethodCallException;
2867
2868/**
2869 * Wraps errors in forms.
2870 *
2871 * @author Bernhard Schussek <bschussek@gmail.com>
2872 */
2873class FormError
2874{
2875 protected string $messageTemplate;
2876
2877 /**
2878 * The form that spawned this error.
2879 */
2880 private ?FormInterface $origin = null;
2881
2882 /**
2883 * Any array key in $messageParameters will be used as a placeholder in
2884 * $messageTemplate.
2885 *
2886 * @param string $message The translated error message
2887 * @param string|null $messageTemplate The template for the error message
2888 * @param array $messageParameters The parameters that should be
2889 * substituted in the message template
2890 * @param int|null $messagePluralization The value for error message pluralization
2891 * @param mixed $cause The cause of the error
2892 *
2893 * @see \Symfony\Component\Translation\Translator
2894 */
2895 public function __construct(
2896 private string $message,
2897 ?string $messageTemplate = null,
2898 protected array $messageParameters = [],
2899 protected ?int $messagePluralization = null,
2900 private mixed $cause = null,
2901 ) {
2902 $this->messageTemplate = $messageTemplate ?: $message;
2903 }
2904
2905 /**
2906 * Returns the error message.
2907 */
2908 public function getMessage(): string
2909 {
2910 return $this->message;
2911 }
2912
2913 /**
2914 * Returns the error message template.
2915 */
2916 public function getMessageTemplate(): string
2917 {
2918 return $this->messageTemplate;
2919 }
2920
2921 /**
2922 * Returns the parameters to be inserted in the message template.
2923 */
2924 public function getMessageParameters(): array
2925 {
2926 return $this->messageParameters;
2927 }
2928
2929 /**
2930 * Returns the value for error message pluralization.
2931 */
2932 public function getMessagePluralization(): ?int
2933 {
2934 return $this->messagePluralization;
2935 }
2936
2937 /**
2938 * Returns the cause of this error.
2939 */
2940 public function getCause(): mixed
2941 {
2942 return $this->cause;
2943 }
2944
2945 /**
2946 * Sets the form that caused this error.
2947 *
2948 * This method must only be called once.
2949 *
2950 * @throws BadMethodCallException If the method is called more than once
2951 */
2952 public function setOrigin(FormInterface $origin): void
2953 {
2954 if (null !== $this->origin) {
2955 throw new BadMethodCallException('setOrigin() must only be called once.');
2956 }
2957
2958 $this->origin = $origin;
2959 }
2960
2961 /**
2962 * Returns the form that caused this error.
2963 */
2964 public function getOrigin(): ?FormInterface
2965 {
2966 return $this->origin;
2967 }
2968}
2969
2970------------------------------------------------------------------------------------------------------------------------
2971 ./FormEvent.php
2972------------------------------------------------------------------------------------------------------------------------
2973<?php
2974
2975/*
2976 * This file is part of the Symfony package.
2977 *
2978 * (c) Fabien Potencier <fabien@symfony.com>
2979 *
2980 * For the full copyright and license information, please view the LICENSE
2981 * file that was distributed with this source code.
2982 */
2983
2984namespace Symfony\Component\Form;
2985
2986use Symfony\Contracts\EventDispatcher\Event;
2987
2988/**
2989 * @author Bernhard Schussek <bschussek@gmail.com>
2990 */
2991class FormEvent extends Event
2992{
2993 public function __construct(
2994 private FormInterface $form,
2995 protected mixed $data,
2996 ) {
2997 }
2998
2999 /**
3000 * Returns the form at the source of the event.
3001 */
3002 public function getForm(): FormInterface
3003 {
3004 return $this->form;
3005 }
3006
3007 /**
3008 * Returns the data associated with this event.
3009 */
3010 public function getData(): mixed
3011 {
3012 return $this->data;
3013 }
3014
3015 /**
3016 * Allows updating with some filtered data.
3017 */
3018 public function setData(mixed $data): void
3019 {
3020 $this->data = $data;
3021 }
3022}
3023
3024------------------------------------------------------------------------------------------------------------------------
3025 ./DependencyInjection/FormPass.php
3026------------------------------------------------------------------------------------------------------------------------
3027<?php
3028
3029/*
3030 * This file is part of the Symfony package.
3031 *
3032 * (c) Fabien Potencier <fabien@symfony.com>
3033 *
3034 * For the full copyright and license information, please view the LICENSE
3035 * file that was distributed with this source code.
3036 */
3037
3038namespace Symfony\Component\Form\DependencyInjection;
3039
3040use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
3041use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
3042use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
3043use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
3044use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
3045use Symfony\Component\DependencyInjection\ContainerBuilder;
3046use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
3047use Symfony\Component\DependencyInjection\Reference;
3048
3049/**
3050 * Adds all services with the tags "form.type", "form.type_extension" and
3051 * "form.type_guesser" as arguments of the "form.extension" service.
3052 *
3053 * @author Bernhard Schussek <bschussek@gmail.com>
3054 */
3055class FormPass implements CompilerPassInterface
3056{
3057 use PriorityTaggedServiceTrait;
3058
3059 public function process(ContainerBuilder $container): void
3060 {
3061 if (!$container->hasDefinition('form.extension')) {
3062 return;
3063 }
3064
3065 $definition = $container->getDefinition('form.extension');
3066 $definition->replaceArgument(0, $this->processFormTypes($container));
3067 $definition->replaceArgument(1, $this->processFormTypeExtensions($container));
3068 $definition->replaceArgument(2, $this->processFormTypeGuessers($container));
3069 }
3070
3071 private function processFormTypes(ContainerBuilder $container): Reference
3072 {
3073 // Get service locator argument
3074 $servicesMap = [];
3075 $namespaces = ['Symfony\Component\Form\Extension\Core\Type' => true];
3076 $csrfTokenIds = [];
3077
3078 // Builds an array with fully-qualified type class names as keys and service IDs as values
3079 foreach ($container->findTaggedServiceIds('form.type', true) as $serviceId => $tag) {
3080 // Add form type service to the service locator
3081 $serviceDefinition = $container->getDefinition($serviceId);
3082 $servicesMap[$formType = $serviceDefinition->getClass()] = new Reference($serviceId);
3083 $namespaces[substr($formType, 0, strrpos($formType, '\\'))] = true;
3084
3085 if (isset($tag[0]['csrf_token_id'])) {
3086 $csrfTokenIds[$formType] = $tag[0]['csrf_token_id'];
3087 }
3088 }
3089
3090 if ($container->hasDefinition('console.command.form_debug')) {
3091 $commandDefinition = $container->getDefinition('console.command.form_debug');
3092 $commandDefinition->setArgument(1, array_keys($namespaces));
3093 $commandDefinition->setArgument(2, array_keys($servicesMap));
3094 }
3095
3096 if ($csrfTokenIds && $container->hasDefinition('form.type_extension.csrf')) {
3097 $csrfExtension = $container->getDefinition('form.type_extension.csrf');
3098
3099 if (8 <= \count($csrfExtension->getArguments())) {
3100 $csrfExtension->replaceArgument(7, $csrfTokenIds);
3101 }
3102 }
3103
3104 return ServiceLocatorTagPass::register($container, $servicesMap);
3105 }
3106
3107 private function processFormTypeExtensions(ContainerBuilder $container): array
3108 {
3109 $typeExtensions = [];
3110 $typeExtensionsClasses = [];
3111 foreach ($this->findAndSortTaggedServices('form.type_extension', $container) as $reference) {
3112 $serviceId = (string) $reference;
3113 $serviceDefinition = $container->getDefinition($serviceId);
3114
3115 $tag = $serviceDefinition->getTag('form.type_extension');
3116 $typeExtensionClass = $container->getParameterBag()->resolveValue($serviceDefinition->getClass());
3117
3118 if (isset($tag[0]['extended_type'])) {
3119 $typeExtensions[$tag[0]['extended_type']][] = new Reference($serviceId);
3120 $typeExtensionsClasses[] = $typeExtensionClass;
3121 } else {
3122 $extendsTypes = false;
3123
3124 $typeExtensionsClasses[] = $typeExtensionClass;
3125 foreach ($typeExtensionClass::getExtendedTypes() as $extendedType) {
3126 $typeExtensions[$extendedType][] = new Reference($serviceId);
3127 $extendsTypes = true;
3128 }
3129
3130 if (!$extendsTypes) {
3131 throw new InvalidArgumentException(\sprintf('The getExtendedTypes() method for service "%s" does not return any extended types.', $serviceId));
3132 }
3133 }
3134 }
3135
3136 foreach ($typeExtensions as $extendedType => $extensions) {
3137 $typeExtensions[$extendedType] = new IteratorArgument($extensions);
3138 }
3139
3140 if ($container->hasDefinition('console.command.form_debug')) {
3141 $commandDefinition = $container->getDefinition('console.command.form_debug');
3142 $commandDefinition->setArgument(3, $typeExtensionsClasses);
3143 }
3144
3145 return $typeExtensions;
3146 }
3147
3148 private function processFormTypeGuessers(ContainerBuilder $container): ArgumentInterface
3149 {
3150 $guessers = [];
3151 $guessersClasses = [];
3152 foreach ($container->findTaggedServiceIds('form.type_guesser', true) as $serviceId => $tags) {
3153 $guessers[] = new Reference($serviceId);
3154
3155 $serviceDefinition = $container->getDefinition($serviceId);
3156 $guessersClasses[] = $serviceDefinition->getClass();
3157 }
3158
3159 if ($container->hasDefinition('console.command.form_debug')) {
3160 $commandDefinition = $container->getDefinition('console.command.form_debug');
3161 $commandDefinition->setArgument(4, $guessersClasses);
3162 }
3163
3164 return new IteratorArgument($guessers);
3165 }
3166}
3167
3168------------------------------------------------------------------------------------------------------------------------
3169 ./FormTypeGuesserChain.php
3170------------------------------------------------------------------------------------------------------------------------
3171<?php
3172
3173/*
3174 * This file is part of the Symfony package.
3175 *
3176 * (c) Fabien Potencier <fabien@symfony.com>
3177 *
3178 * For the full copyright and license information, please view the LICENSE
3179 * file that was distributed with this source code.
3180 */
3181
3182namespace Symfony\Component\Form;
3183
3184use Symfony\Component\Form\Exception\UnexpectedTypeException;
3185use Symfony\Component\Form\Guess\Guess;
3186use Symfony\Component\Form\Guess\TypeGuess;
3187use Symfony\Component\Form\Guess\ValueGuess;
3188
3189class FormTypeGuesserChain implements FormTypeGuesserInterface
3190{
3191 private array $guessers = [];
3192
3193 /**
3194 * @param FormTypeGuesserInterface[] $guessers
3195 *
3196 * @throws UnexpectedTypeException if any guesser does not implement FormTypeGuesserInterface
3197 */
3198 public function __construct(iterable $guessers)
3199 {
3200 $tmpGuessers = [];
3201 foreach ($guessers as $guesser) {
3202 if (!$guesser instanceof FormTypeGuesserInterface) {
3203 throw new UnexpectedTypeException($guesser, FormTypeGuesserInterface::class);
3204 }
3205
3206 if ($guesser instanceof self) {
3207 $tmpGuessers[] = $guesser->guessers;
3208 } else {
3209 $tmpGuessers[] = [$guesser];
3210 }
3211 }
3212
3213 $this->guessers = array_merge([], ...$tmpGuessers);
3214 }
3215
3216 public function guessType(string $class, string $property): ?TypeGuess
3217 {
3218 return $this->guess(static fn ($guesser) => $guesser->guessType($class, $property));
3219 }
3220
3221 public function guessRequired(string $class, string $property): ?ValueGuess
3222 {
3223 return $this->guess(static fn ($guesser) => $guesser->guessRequired($class, $property));
3224 }
3225
3226 public function guessMaxLength(string $class, string $property): ?ValueGuess
3227 {
3228 return $this->guess(static fn ($guesser) => $guesser->guessMaxLength($class, $property));
3229 }
3230
3231 public function guessPattern(string $class, string $property): ?ValueGuess
3232 {
3233 return $this->guess(static fn ($guesser) => $guesser->guessPattern($class, $property));
3234 }
3235
3236 /**
3237 * Executes a closure for each guesser and returns the best guess from the
3238 * return values.
3239 *
3240 * @param \Closure $closure The closure to execute. Accepts a guesser
3241 * as argument and should return a Guess instance
3242 */
3243 private function guess(\Closure $closure): ?Guess
3244 {
3245 $guesses = [];
3246
3247 foreach ($this->guessers as $guesser) {
3248 if ($guess = $closure($guesser)) {
3249 $guesses[] = $guess;
3250 }
3251 }
3252
3253 return Guess::getBestGuess($guesses);
3254 }
3255}
3256
3257------------------------------------------------------------------------------------------------------------------------
3258 ./FormEvents.php
3259------------------------------------------------------------------------------------------------------------------------
3260<?php
3261
3262/*
3263 * This file is part of the Symfony package.
3264 *
3265 * (c) Fabien Potencier <fabien@symfony.com>
3266 *
3267 * For the full copyright and license information, please view the LICENSE
3268 * file that was distributed with this source code.
3269 */
3270
3271namespace Symfony\Component\Form;
3272
3273use Symfony\Component\Form\Event\PostSetDataEvent;
3274use Symfony\Component\Form\Event\PostSubmitEvent;
3275use Symfony\Component\Form\Event\PreSetDataEvent;
3276use Symfony\Component\Form\Event\PreSubmitEvent;
3277use Symfony\Component\Form\Event\SubmitEvent;
3278
3279/**
3280 * To learn more about how form events work check the documentation
3281 * entry at {@link https://symfony.com/doc/any/components/form/form_events.html}.
3282 *
3283 * To learn how to dynamically modify forms using events check the cookbook
3284 * entry at {@link https://symfony.com/doc/any/cookbook/form/dynamic_form_modification.html}.
3285 *
3286 * @author Bernhard Schussek <bschussek@gmail.com>
3287 */
3288final class FormEvents
3289{
3290 /**
3291 * The PRE_SUBMIT event is dispatched at the beginning of the Form::submit() method.
3292 *
3293 * It can be used to:
3294 * - Change data from the request, before submitting the data to the form.
3295 * - Add or remove form fields, before submitting the data to the form.
3296 *
3297 * @Event("Symfony\Component\Form\Event\PreSubmitEvent")
3298 */
3299 public const PRE_SUBMIT = 'form.pre_submit';
3300
3301 /**
3302 * The SUBMIT event is dispatched after the Form::submit() method
3303 * has changed the view data by the request data, or submitted and mapped
3304 * the children if the form is compound, and after reverse transformation
3305 * to normalized representation.
3306 *
3307 * It's also dispatched just before the Form::submit() method transforms back
3308 * the normalized data to the model and view data.
3309 *
3310 * So at this stage children of compound forms are submitted and synchronized, unless
3311 * their transformation failed, but a parent would still be at the PRE_SUBMIT level.
3312 *
3313 * Since the current form is not synchronized yet, it is still possible to add and
3314 * remove fields.
3315 *
3316 * @Event("Symfony\Component\Form\Event\SubmitEvent")
3317 */
3318 public const SUBMIT = 'form.submit';
3319
3320 /**
3321 * The FormEvents::POST_SUBMIT event is dispatched at the very end of the Form::submit().
3322 *
3323 * It this stage the model and view data may have been denormalized. Otherwise the form
3324 * is desynchronized because transformation failed during submission.
3325 *
3326 * It can be used to fetch data after denormalization.
3327 *
3328 * The event attaches the current view data. To know whether this is the renormalized data
3329 * or the invalid request data, call Form::isSynchronized() first.
3330 *
3331 * @Event("Symfony\Component\Form\Event\PostSubmitEvent")
3332 */
3333 public const POST_SUBMIT = 'form.post_submit';
3334
3335 /**
3336 * The FormEvents::PRE_SET_DATA event is dispatched at the beginning of the Form::setData() method.
3337 *
3338 * It can be used to:
3339 * - Modify the data given during pre-population;
3340 * - Keep synchronized the form depending on the data (adding or removing fields dynamically).
3341 *
3342 * @Event("Symfony\Component\Form\Event\PreSetDataEvent")
3343 */
3344 public const PRE_SET_DATA = 'form.pre_set_data';
3345
3346 /**
3347 * The FormEvents::POST_SET_DATA event is dispatched at the end of the Form::setData() method.
3348 *
3349 * This event can be used to modify the form depending on the final state of the underlying data
3350 * accessible in every representation: model, normalized and view.
3351 *
3352 * @Event("Symfony\Component\Form\Event\PostSetDataEvent")
3353 */
3354 public const POST_SET_DATA = 'form.post_set_data';
3355
3356 /**
3357 * Event aliases.
3358 *
3359 * These aliases can be consumed by RegisterListenersPass.
3360 */
3361 public const ALIASES = [
3362 PreSubmitEvent::class => self::PRE_SUBMIT,
3363 SubmitEvent::class => self::SUBMIT,
3364 PostSubmitEvent::class => self::POST_SUBMIT,
3365 PreSetDataEvent::class => self::PRE_SET_DATA,
3366 PostSetDataEvent::class => self::POST_SET_DATA,
3367 ];
3368
3369 private function __construct()
3370 {
3371 }
3372}
3373
3374------------------------------------------------------------------------------------------------------------------------
3375 ./FormRegistryInterface.php
3376------------------------------------------------------------------------------------------------------------------------
3377<?php
3378
3379/*
3380 * This file is part of the Symfony package.
3381 *
3382 * (c) Fabien Potencier <fabien@symfony.com>
3383 *
3384 * For the full copyright and license information, please view the LICENSE
3385 * file that was distributed with this source code.
3386 */
3387
3388namespace Symfony\Component\Form;
3389
3390/**
3391 * The central registry of the Form component.
3392 *
3393 * @author Bernhard Schussek <bschussek@gmail.com>
3394 */
3395interface FormRegistryInterface
3396{
3397 /**
3398 * Returns a form type by name.
3399 *
3400 * This method registers the type extensions from the form extensions.
3401 *
3402 * @throws Exception\InvalidArgumentException if the type cannot be retrieved from any extension
3403 */
3404 public function getType(string $name): ResolvedFormTypeInterface;
3405
3406 /**
3407 * Returns whether the given form type is supported.
3408 */
3409 public function hasType(string $name): bool;
3410
3411 /**
3412 * Returns the guesser responsible for guessing types.
3413 */
3414 public function getTypeGuesser(): ?FormTypeGuesserInterface;
3415
3416 /**
3417 * Returns the extensions loaded by the framework.
3418 *
3419 * @return FormExtensionInterface[]
3420 */
3421 public function getExtensions(): array;
3422}
3423
3424------------------------------------------------------------------------------------------------------------------------
3425 ./FormTypeInterface.php
3426------------------------------------------------------------------------------------------------------------------------
3427<?php
3428
3429/*
3430 * This file is part of the Symfony package.
3431 *
3432 * (c) Fabien Potencier <fabien@symfony.com>
3433 *
3434 * For the full copyright and license information, please view the LICENSE
3435 * file that was distributed with this source code.
3436 */
3437
3438namespace Symfony\Component\Form;
3439
3440use Symfony\Component\OptionsResolver\OptionsResolver;
3441
3442/**
3443 * @author Bernhard Schussek <bschussek@gmail.com>
3444 */
3445interface FormTypeInterface
3446{
3447 /**
3448 * Returns the name of the parent type.
3449 *
3450 * The parent type and its extensions will configure the form with the
3451 * following methods before the current implementation.
3452 *
3453 * @return string|null
3454 */
3455 public function getParent();
3456
3457 /**
3458 * Configures the options for this type.
3459 *
3460 * @return void
3461 */
3462 public function configureOptions(OptionsResolver $resolver);
3463
3464 /**
3465 * Builds the form.
3466 *
3467 * This method is called for each type in the hierarchy starting from the
3468 * top most type. Type extensions can further modify the form.
3469 *
3470 * @param array<string, mixed> $options
3471 *
3472 * @return void
3473 *
3474 * @see FormTypeExtensionInterface::buildForm()
3475 */
3476 public function buildForm(FormBuilderInterface $builder, array $options);
3477
3478 /**
3479 * Builds the form view.
3480 *
3481 * This method is called for each type in the hierarchy starting from the
3482 * top most type. Type extensions can further modify the view.
3483 *
3484 * A view of a form is built before the views of the child forms are built.
3485 * This means that you cannot access child views in this method. If you need
3486 * to do so, move your logic to {@link finishView()} instead.
3487 *
3488 * @param array<string, mixed> $options
3489 *
3490 * @return void
3491 *
3492 * @see FormTypeExtensionInterface::buildView()
3493 */
3494 public function buildView(FormView $view, FormInterface $form, array $options);
3495
3496 /**
3497 * Finishes the form view.
3498 *
3499 * This method gets called for each type in the hierarchy starting from the
3500 * top most type. Type extensions can further modify the view.
3501 *
3502 * When this method is called, views of the form's children have already
3503 * been built and finished and can be accessed. You should only implement
3504 * such logic in this method that actually accesses child views. For everything
3505 * else you are recommended to implement {@link buildView()} instead.
3506 *
3507 * @param array<string, mixed> $options
3508 *
3509 * @return void
3510 *
3511 * @see FormTypeExtensionInterface::finishView()
3512 */
3513 public function finishView(FormView $view, FormInterface $form, array $options);
3514
3515 /**
3516 * Returns the prefix of the template block name for this type.
3517 *
3518 * The block prefix defaults to the underscored short class name with
3519 * the "Type" suffix removed (e.g. "UserProfileType" => "user_profile").
3520 *
3521 * @return string
3522 */
3523 public function getBlockPrefix();
3524}
3525
3526------------------------------------------------------------------------------------------------------------------------
3527 ./Exception/OutOfBoundsException.php
3528------------------------------------------------------------------------------------------------------------------------
3529<?php
3530
3531/*
3532 * This file is part of the Symfony package.
3533 *
3534 * (c) Fabien Potencier <fabien@symfony.com>
3535 *
3536 * For the full copyright and license information, please view the LICENSE
3537 * file that was distributed with this source code.
3538 */
3539
3540namespace Symfony\Component\Form\Exception;
3541
3542/**
3543 * Base OutOfBoundsException for Form component.
3544 *
3545 * @author Alexander Kotynia <aleksander.kot@gmail.com>
3546 */
3547class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
3548{
3549}
3550
3551------------------------------------------------------------------------------------------------------------------------
3552 ./Exception/AccessException.php
3553------------------------------------------------------------------------------------------------------------------------
3554<?php
3555
3556/*
3557 * This file is part of the Symfony package.
3558 *
3559 * (c) Fabien Potencier <fabien@symfony.com>
3560 *
3561 * For the full copyright and license information, please view the LICENSE
3562 * file that was distributed with this source code.
3563 */
3564
3565namespace Symfony\Component\Form\Exception;
3566
3567class AccessException extends RuntimeException
3568{
3569}
3570
3571------------------------------------------------------------------------------------------------------------------------
3572 ./Exception/TransformationFailedException.php
3573------------------------------------------------------------------------------------------------------------------------
3574<?php
3575
3576/*
3577 * This file is part of the Symfony package.
3578 *
3579 * (c) Fabien Potencier <fabien@symfony.com>
3580 *
3581 * For the full copyright and license information, please view the LICENSE
3582 * file that was distributed with this source code.
3583 */
3584
3585namespace Symfony\Component\Form\Exception;
3586
3587/**
3588 * Indicates a value transformation error.
3589 *
3590 * @author Bernhard Schussek <bschussek@gmail.com>
3591 */
3592class TransformationFailedException extends RuntimeException
3593{
3594 private ?string $invalidMessage;
3595 private array $invalidMessageParameters;
3596
3597 public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, ?string $invalidMessage = null, array $invalidMessageParameters = [])
3598 {
3599 parent::__construct($message, $code, $previous);
3600
3601 $this->setInvalidMessage($invalidMessage, $invalidMessageParameters);
3602 }
3603
3604 /**
3605 * Sets the message that will be shown to the user.
3606 *
3607 * @param string|null $invalidMessage The message or message key
3608 * @param array $invalidMessageParameters Data to be passed into the translator
3609 */
3610 public function setInvalidMessage(?string $invalidMessage, array $invalidMessageParameters = []): void
3611 {
3612 $this->invalidMessage = $invalidMessage;
3613 $this->invalidMessageParameters = $invalidMessageParameters;
3614 }
3615
3616 public function getInvalidMessage(): ?string
3617 {
3618 return $this->invalidMessage;
3619 }
3620
3621 public function getInvalidMessageParameters(): array
3622 {
3623 return $this->invalidMessageParameters;
3624 }
3625}
3626
3627------------------------------------------------------------------------------------------------------------------------
3628 ./Exception/RuntimeException.php
3629------------------------------------------------------------------------------------------------------------------------
3630<?php
3631
3632/*
3633 * This file is part of the Symfony package.
3634 *
3635 * (c) Fabien Potencier <fabien@symfony.com>
3636 *
3637 * For the full copyright and license information, please view the LICENSE
3638 * file that was distributed with this source code.
3639 */
3640
3641namespace Symfony\Component\Form\Exception;
3642
3643/**
3644 * Base RuntimeException for the Form component.
3645 *
3646 * @author Bernhard Schussek <bschussek@gmail.com>
3647 */
3648class RuntimeException extends \RuntimeException implements ExceptionInterface
3649{
3650}
3651
3652------------------------------------------------------------------------------------------------------------------------
3653 ./Exception/ExceptionInterface.php
3654------------------------------------------------------------------------------------------------------------------------
3655<?php
3656
3657/*
3658 * This file is part of the Symfony package.
3659 *
3660 * (c) Fabien Potencier <fabien@symfony.com>
3661 *
3662 * For the full copyright and license information, please view the LICENSE
3663 * file that was distributed with this source code.
3664 */
3665
3666namespace Symfony\Component\Form\Exception;
3667
3668/**
3669 * Base ExceptionInterface for the Form component.
3670 *
3671 * @author Bernhard Schussek <bschussek@gmail.com>
3672 */
3673interface ExceptionInterface extends \Throwable
3674{
3675}
3676
3677------------------------------------------------------------------------------------------------------------------------
3678 ./Exception/BadMethodCallException.php
3679------------------------------------------------------------------------------------------------------------------------
3680<?php
3681
3682/*
3683 * This file is part of the Symfony package.
3684 *
3685 * (c) Fabien Potencier <fabien@symfony.com>
3686 *
3687 * For the full copyright and license information, please view the LICENSE
3688 * file that was distributed with this source code.
3689 */
3690
3691namespace Symfony\Component\Form\Exception;
3692
3693/**
3694 * Base BadMethodCallException for the Form component.
3695 *
3696 * @author Bernhard Schussek <bschussek@gmail.com>
3697 */
3698class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
3699{
3700}
3701
3702------------------------------------------------------------------------------------------------------------------------
3703 ./Exception/InvalidArgumentException.php
3704------------------------------------------------------------------------------------------------------------------------
3705<?php
3706
3707/*
3708 * This file is part of the Symfony package.
3709 *
3710 * (c) Fabien Potencier <fabien@symfony.com>
3711 *
3712 * For the full copyright and license information, please view the LICENSE
3713 * file that was distributed with this source code.
3714 */
3715
3716namespace Symfony\Component\Form\Exception;
3717
3718/**
3719 * Base InvalidArgumentException for the Form component.
3720 *
3721 * @author Bernhard Schussek <bschussek@gmail.com>
3722 */
3723class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
3724{
3725}
3726
3727------------------------------------------------------------------------------------------------------------------------
3728 ./Exception/LogicException.php
3729------------------------------------------------------------------------------------------------------------------------
3730<?php
3731
3732/*
3733 * This file is part of the Symfony package.
3734 *
3735 * (c) Fabien Potencier <fabien@symfony.com>
3736 *
3737 * For the full copyright and license information, please view the LICENSE
3738 * file that was distributed with this source code.
3739 */
3740
3741namespace Symfony\Component\Form\Exception;
3742
3743/**
3744 * Base LogicException for Form component.
3745 *
3746 * @author Alexander Kotynia <aleksander.kot@gmail.com>
3747 */
3748class LogicException extends \LogicException implements ExceptionInterface
3749{
3750}
3751
3752------------------------------------------------------------------------------------------------------------------------
3753 ./Exception/InvalidConfigurationException.php
3754------------------------------------------------------------------------------------------------------------------------
3755<?php
3756
3757/*
3758 * This file is part of the Symfony package.
3759 *
3760 * (c) Fabien Potencier <fabien@symfony.com>
3761 *
3762 * For the full copyright and license information, please view the LICENSE
3763 * file that was distributed with this source code.
3764 */
3765
3766namespace Symfony\Component\Form\Exception;
3767
3768class InvalidConfigurationException extends InvalidArgumentException
3769{
3770}
3771
3772------------------------------------------------------------------------------------------------------------------------
3773 ./Exception/ErrorMappingException.php
3774------------------------------------------------------------------------------------------------------------------------
3775<?php
3776
3777/*
3778 * This file is part of the Symfony package.
3779 *
3780 * (c) Fabien Potencier <fabien@symfony.com>
3781 *
3782 * For the full copyright and license information, please view the LICENSE
3783 * file that was distributed with this source code.
3784 */
3785
3786namespace Symfony\Component\Form\Exception;
3787
3788class ErrorMappingException extends RuntimeException
3789{
3790}
3791
3792------------------------------------------------------------------------------------------------------------------------
3793 ./Exception/StringCastException.php
3794------------------------------------------------------------------------------------------------------------------------
3795<?php
3796
3797/*
3798 * This file is part of the Symfony package.
3799 *
3800 * (c) Fabien Potencier <fabien@symfony.com>
3801 *
3802 * For the full copyright and license information, please view the LICENSE
3803 * file that was distributed with this source code.
3804 */
3805
3806namespace Symfony\Component\Form\Exception;
3807
3808class StringCastException extends RuntimeException
3809{
3810}
3811
3812------------------------------------------------------------------------------------------------------------------------
3813 ./Exception/AlreadySubmittedException.php
3814------------------------------------------------------------------------------------------------------------------------
3815<?php
3816
3817/*
3818 * This file is part of the Symfony package.
3819 *
3820 * (c) Fabien Potencier <fabien@symfony.com>
3821 *
3822 * For the full copyright and license information, please view the LICENSE
3823 * file that was distributed with this source code.
3824 */
3825
3826namespace Symfony\Component\Form\Exception;
3827
3828/**
3829 * Thrown when an operation is called that is not acceptable after submitting
3830 * a form.
3831 *
3832 * @author Bernhard Schussek <bschussek@gmail.com>
3833 */
3834class AlreadySubmittedException extends LogicException
3835{
3836}
3837
3838------------------------------------------------------------------------------------------------------------------------
3839 ./Exception/UnexpectedTypeException.php
3840------------------------------------------------------------------------------------------------------------------------
3841<?php
3842
3843/*
3844 * This file is part of the Symfony package.
3845 *
3846 * (c) Fabien Potencier <fabien@symfony.com>
3847 *
3848 * For the full copyright and license information, please view the LICENSE
3849 * file that was distributed with this source code.
3850 */
3851
3852namespace Symfony\Component\Form\Exception;
3853
3854class UnexpectedTypeException extends InvalidArgumentException
3855{
3856 public function __construct(mixed $value, string $expectedType)
3857 {
3858 parent::__construct(\sprintf('Expected argument of type "%s", "%s" given', $expectedType, get_debug_type($value)));
3859 }
3860}
3861
3862------------------------------------------------------------------------------------------------------------------------
3863 ./Event/PreSetDataEvent.php
3864------------------------------------------------------------------------------------------------------------------------
3865<?php
3866
3867/*
3868 * This file is part of the Symfony package.
3869 *
3870 * (c) Fabien Potencier <fabien@symfony.com>
3871 *
3872 * For the full copyright and license information, please view the LICENSE
3873 * file that was distributed with this source code.
3874 */
3875
3876namespace Symfony\Component\Form\Event;
3877
3878use Symfony\Component\Form\FormEvent;
3879
3880/**
3881 * This event is dispatched at the beginning of the Form::setData() method.
3882 *
3883 * It can be used to modify the data given during pre-population.
3884 */
3885final class PreSetDataEvent extends FormEvent
3886{
3887}
3888
3889------------------------------------------------------------------------------------------------------------------------
3890 ./Event/PreSubmitEvent.php
3891------------------------------------------------------------------------------------------------------------------------
3892<?php
3893
3894/*
3895 * This file is part of the Symfony package.
3896 *
3897 * (c) Fabien Potencier <fabien@symfony.com>
3898 *
3899 * For the full copyright and license information, please view the LICENSE
3900 * file that was distributed with this source code.
3901 */
3902
3903namespace Symfony\Component\Form\Event;
3904
3905use Symfony\Component\Form\FormEvent;
3906
3907/**
3908 * This event is dispatched at the beginning of the Form::submit() method.
3909 *
3910 * It can be used to:
3911 * - Change data from the request, before submitting the data to the form.
3912 * - Add or remove form fields, before submitting the data to the form.
3913 */
3914final class PreSubmitEvent extends FormEvent
3915{
3916}
3917
3918------------------------------------------------------------------------------------------------------------------------
3919 ./Event/PostSetDataEvent.php
3920------------------------------------------------------------------------------------------------------------------------
3921<?php
3922
3923/*
3924 * This file is part of the Symfony package.
3925 *
3926 * (c) Fabien Potencier <fabien@symfony.com>
3927 *
3928 * For the full copyright and license information, please view the LICENSE
3929 * file that was distributed with this source code.
3930 */
3931
3932namespace Symfony\Component\Form\Event;
3933
3934use Symfony\Component\Form\Exception\BadMethodCallException;
3935use Symfony\Component\Form\FormEvent;
3936
3937/**
3938 * This event is dispatched at the end of the Form::setData() method.
3939 *
3940 * It can be used to modify a form depending on the populated data (adding or
3941 * removing fields dynamically).
3942 */
3943final class PostSetDataEvent extends FormEvent
3944{
3945 public function setData(mixed $data): never
3946 {
3947 throw new BadMethodCallException('Form data cannot be changed during "form.post_set_data", you should use "form.pre_set_data" instead.');
3948 }
3949}
3950
3951------------------------------------------------------------------------------------------------------------------------
3952 ./Event/PostSubmitEvent.php
3953------------------------------------------------------------------------------------------------------------------------
3954<?php
3955
3956/*
3957 * This file is part of the Symfony package.
3958 *
3959 * (c) Fabien Potencier <fabien@symfony.com>
3960 *
3961 * For the full copyright and license information, please view the LICENSE
3962 * file that was distributed with this source code.
3963 */
3964
3965namespace Symfony\Component\Form\Event;
3966
3967use Symfony\Component\Form\Exception\BadMethodCallException;
3968use Symfony\Component\Form\FormEvent;
3969
3970/**
3971 * This event is dispatched after the Form::submit()
3972 * once the model and view data have been denormalized.
3973 *
3974 * It can be used to fetch data after denormalization.
3975 */
3976final class PostSubmitEvent extends FormEvent
3977{
3978 public function setData(mixed $data): never
3979 {
3980 throw new BadMethodCallException('Form data cannot be changed during "form.post_submit", you should use "form.pre_submit" or "form.submit" instead.');
3981 }
3982}
3983
3984------------------------------------------------------------------------------------------------------------------------
3985 ./Event/SubmitEvent.php
3986------------------------------------------------------------------------------------------------------------------------
3987<?php
3988
3989/*
3990 * This file is part of the Symfony package.
3991 *
3992 * (c) Fabien Potencier <fabien@symfony.com>
3993 *
3994 * For the full copyright and license information, please view the LICENSE
3995 * file that was distributed with this source code.
3996 */
3997
3998namespace Symfony\Component\Form\Event;
3999
4000use Symfony\Component\Form\FormEvent;
4001
4002/**
4003 * This event is dispatched just before the Form::submit() method
4004 * transforms back the normalized data to the model and view data.
4005 *
4006 * It can be used to change data from the normalized representation of the data.
4007 */
4008final class SubmitEvent extends FormEvent
4009{
4010}
4011
4012------------------------------------------------------------------------------------------------------------------------
4013 ./Util/OrderedHashMapIterator.php
4014------------------------------------------------------------------------------------------------------------------------
4015<?php
4016
4017/*
4018 * This file is part of the Symfony package.
4019 *
4020 * (c) Fabien Potencier <fabien@symfony.com>
4021 *
4022 * For the full copyright and license information, please view the LICENSE
4023 * file that was distributed with this source code.
4024 */
4025
4026namespace Symfony\Component\Form\Util;
4027
4028/**
4029 * Iterator for {@link OrderedHashMap} objects.
4030 *
4031 * @author Bernhard Schussek <bschussek@gmail.com>
4032 *
4033 * @internal
4034 *
4035 * @template-covariant TValue
4036 *
4037 * @implements \Iterator<string, TValue>
4038 */
4039class OrderedHashMapIterator implements \Iterator
4040{
4041 private int $cursor = 0;
4042 private int $cursorId;
4043 private ?string $key = null;
4044 /** @var TValue|null */
4045 private mixed $current = null;
4046
4047 /**
4048 * @param TValue[] $elements The elements of the map, indexed by their
4049 * keys
4050 * @param list<string> $orderedKeys The keys of the map in the order in which
4051 * they should be iterated
4052 * @param array<int, int> $managedCursors An array from which to reference the
4053 * iterator's cursor as long as it is alive.
4054 * This array is managed by the corresponding
4055 * {@link OrderedHashMap} instance to support
4056 * recognizing the deletion of elements.
4057 */
4058 public function __construct(
4059 private array &$elements,
4060 private array &$orderedKeys,
4061 private array &$managedCursors,
4062 ) {
4063 $this->cursorId = \count($managedCursors);
4064
4065 $this->managedCursors[$this->cursorId] = &$this->cursor;
4066 }
4067
4068 public function __sleep(): array
4069 {
4070 throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
4071 }
4072
4073 public function __wakeup(): void
4074 {
4075 throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
4076 }
4077
4078 /**
4079 * Removes the iterator's cursors from the managed cursors of the
4080 * corresponding {@link OrderedHashMap} instance.
4081 */
4082 public function __destruct()
4083 {
4084 // Use array_splice() instead of unset() to prevent holes in the
4085 // array indices, which would break the initialization of $cursorId
4086 array_splice($this->managedCursors, $this->cursorId, 1);
4087 }
4088
4089 public function current(): mixed
4090 {
4091 return $this->current;
4092 }
4093
4094 public function next(): void
4095 {
4096 ++$this->cursor;
4097
4098 if (isset($this->orderedKeys[$this->cursor])) {
4099 $this->key = $this->orderedKeys[$this->cursor];
4100 $this->current = $this->elements[$this->key];
4101 } else {
4102 $this->key = null;
4103 $this->current = null;
4104 }
4105 }
4106
4107 public function key(): mixed
4108 {
4109 return $this->key;
4110 }
4111
4112 public function valid(): bool
4113 {
4114 return null !== $this->key;
4115 }
4116
4117 public function rewind(): void
4118 {
4119 $this->cursor = 0;
4120
4121 if (isset($this->orderedKeys[0])) {
4122 $this->key = $this->orderedKeys[0];
4123 $this->current = $this->elements[$this->key];
4124 } else {
4125 $this->key = null;
4126 $this->current = null;
4127 }
4128 }
4129}
4130
4131------------------------------------------------------------------------------------------------------------------------
4132 ./Util/ServerParams.php
4133------------------------------------------------------------------------------------------------------------------------
4134<?php
4135
4136/*
4137 * This file is part of the Symfony package.
4138 *
4139 * (c) Fabien Potencier <fabien@symfony.com>
4140 *
4141 * For the full copyright and license information, please view the LICENSE
4142 * file that was distributed with this source code.
4143 */
4144
4145namespace Symfony\Component\Form\Util;
4146
4147use Symfony\Component\HttpFoundation\RequestStack;
4148
4149/**
4150 * @author Bernhard Schussek <bschussek@gmail.com>
4151 */
4152class ServerParams
4153{
4154 public function __construct(
4155 private ?RequestStack $requestStack = null,
4156 ) {
4157 }
4158
4159 /**
4160 * Returns true if the POST max size has been exceeded in the request.
4161 */
4162 public function hasPostMaxSizeBeenExceeded(): bool
4163 {
4164 $contentLength = $this->getContentLength();
4165 $maxContentLength = $this->getPostMaxSize();
4166
4167 return $maxContentLength && $contentLength > $maxContentLength;
4168 }
4169
4170 /**
4171 * Returns maximum post size in bytes.
4172 */
4173 public function getPostMaxSize(): int|float|null
4174 {
4175 $iniMax = strtolower($this->getNormalizedIniPostMaxSize());
4176
4177 if ('' === $iniMax) {
4178 return null;
4179 }
4180
4181 $max = ltrim($iniMax, '+');
4182 if (str_starts_with($max, '0x')) {
4183 $max = \intval($max, 16);
4184 } elseif (str_starts_with($max, '0')) {
4185 $max = \intval($max, 8);
4186 } else {
4187 $max = (int) $max;
4188 }
4189
4190 switch (substr($iniMax, -1)) {
4191 case 't': $max *= 1024;
4192 // no break
4193 case 'g': $max *= 1024;
4194 // no break
4195 case 'm': $max *= 1024;
4196 // no break
4197 case 'k': $max *= 1024;
4198 }
4199
4200 return $max;
4201 }
4202
4203 /**
4204 * Returns the normalized "post_max_size" ini setting.
4205 */
4206 public function getNormalizedIniPostMaxSize(): string
4207 {
4208 return strtoupper(trim(\ini_get('post_max_size')));
4209 }
4210
4211 /**
4212 * Returns the content length of the request.
4213 */
4214 public function getContentLength(): mixed
4215 {
4216 if (null !== $this->requestStack && null !== $request = $this->requestStack->getCurrentRequest()) {
4217 return $request->server->get('CONTENT_LENGTH');
4218 }
4219
4220 return isset($_SERVER['CONTENT_LENGTH'])
4221 ? (int) $_SERVER['CONTENT_LENGTH']
4222 : null;
4223 }
4224}
4225
4226------------------------------------------------------------------------------------------------------------------------
4227 ./Util/InheritDataAwareIterator.php
4228------------------------------------------------------------------------------------------------------------------------
4229<?php
4230
4231/*
4232 * This file is part of the Symfony package.
4233 *
4234 * (c) Fabien Potencier <fabien@symfony.com>
4235 *
4236 * For the full copyright and license information, please view the LICENSE
4237 * file that was distributed with this source code.
4238 */
4239
4240namespace Symfony\Component\Form\Util;
4241
4242/**
4243 * Iterator that traverses an array of forms.
4244 *
4245 * Contrary to \ArrayIterator, this iterator recognizes changes in the original
4246 * array during iteration.
4247 *
4248 * You can wrap the iterator into a {@link \RecursiveIteratorIterator} in order to
4249 * enter any child form that inherits its parent's data and iterate the children
4250 * of that form as well.
4251 *
4252 * @author Bernhard Schussek <bschussek@gmail.com>
4253 */
4254class InheritDataAwareIterator extends \IteratorIterator implements \RecursiveIterator
4255{
4256 public function getChildren(): static
4257 {
4258 return new static($this->current());
4259 }
4260
4261 public function hasChildren(): bool
4262 {
4263 return (bool) $this->current()->getConfig()->getInheritData();
4264 }
4265}
4266
4267------------------------------------------------------------------------------------------------------------------------
4268 ./Util/OptionsResolverWrapper.php
4269------------------------------------------------------------------------------------------------------------------------
4270<?php
4271
4272/*
4273 * This file is part of the Symfony package.
4274 *
4275 * (c) Fabien Potencier <fabien@symfony.com>
4276 *
4277 * For the full copyright and license information, please view the LICENSE
4278 * file that was distributed with this source code.
4279 */
4280
4281namespace Symfony\Component\Form\Util;
4282
4283use Symfony\Component\OptionsResolver\Exception\AccessException;
4284use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
4285use Symfony\Component\OptionsResolver\OptionsResolver;
4286
4287/**
4288 * @author Yonel Ceruto <yonelceruto@gmail.com>
4289 *
4290 * @internal
4291 */
4292class OptionsResolverWrapper extends OptionsResolver
4293{
4294 private array $undefined = [];
4295
4296 /**
4297 * @return $this
4298 */
4299 public function setNormalizer(string $option, \Closure $normalizer): static
4300 {
4301 try {
4302 parent::setNormalizer($option, $normalizer);
4303 } catch (UndefinedOptionsException) {
4304 $this->undefined[$option] = true;
4305 }
4306
4307 return $this;
4308 }
4309
4310 /**
4311 * @return $this
4312 */
4313 public function setAllowedValues(string $option, mixed $allowedValues): static
4314 {
4315 try {
4316 parent::setAllowedValues($option, $allowedValues);
4317 } catch (UndefinedOptionsException) {
4318 $this->undefined[$option] = true;
4319 }
4320
4321 return $this;
4322 }
4323
4324 /**
4325 * @return $this
4326 */
4327 public function addAllowedValues(string $option, mixed $allowedValues): static
4328 {
4329 try {
4330 parent::addAllowedValues($option, $allowedValues);
4331 } catch (UndefinedOptionsException) {
4332 $this->undefined[$option] = true;
4333 }
4334
4335 return $this;
4336 }
4337
4338 /**
4339 * @param string|array $allowedTypes
4340 *
4341 * @return $this
4342 */
4343 public function setAllowedTypes(string $option, $allowedTypes): static
4344 {
4345 try {
4346 parent::setAllowedTypes($option, $allowedTypes);
4347 } catch (UndefinedOptionsException) {
4348 $this->undefined[$option] = true;
4349 }
4350
4351 return $this;
4352 }
4353
4354 /**
4355 * @param string|array $allowedTypes
4356 *
4357 * @return $this
4358 */
4359 public function addAllowedTypes(string $option, $allowedTypes): static
4360 {
4361 try {
4362 parent::addAllowedTypes($option, $allowedTypes);
4363 } catch (UndefinedOptionsException) {
4364 $this->undefined[$option] = true;
4365 }
4366
4367 return $this;
4368 }
4369
4370 public function resolve(array $options = []): array
4371 {
4372 throw new AccessException('Resolve options is not supported.');
4373 }
4374
4375 public function getUndefinedOptions(): array
4376 {
4377 return array_keys($this->undefined);
4378 }
4379}
4380
4381------------------------------------------------------------------------------------------------------------------------
4382 ./Util/OrderedHashMap.php
4383------------------------------------------------------------------------------------------------------------------------
4384<?php
4385
4386/*
4387 * This file is part of the Symfony package.
4388 *
4389 * (c) Fabien Potencier <fabien@symfony.com>
4390 *
4391 * For the full copyright and license information, please view the LICENSE
4392 * file that was distributed with this source code.
4393 */
4394
4395namespace Symfony\Component\Form\Util;
4396
4397/**
4398 * A hash map which keeps track of deletions and additions.
4399 *
4400 * Like in associative arrays, elements can be mapped to integer or string keys.
4401 * Unlike associative arrays, the map keeps track of the order in which keys
4402 * were added and removed. This order is reflected during iteration.
4403 *
4404 * The map supports concurrent modification during iteration. That means that
4405 * you can insert and remove elements from within a foreach loop and the
4406 * iterator will reflect those changes accordingly.
4407 *
4408 * While elements that are added during the loop are recognized by the iterator,
4409 * changed elements are not. Otherwise the loop could be infinite if each loop
4410 * changes the current element:
4411 *
4412 * $map = new OrderedHashMap();
4413 * $map[1] = 1;
4414 * $map[2] = 2;
4415 * $map[3] = 3;
4416 *
4417 * foreach ($map as $index => $value) {
4418 * echo "$index: $value\n"
4419 * if (1 === $index) {
4420 * $map[1] = 4;
4421 * $map[] = 5;
4422 * }
4423 * }
4424 *
4425 * print_r(iterator_to_array($map));
4426 *
4427 * // => 1: 1
4428 * // 2: 2
4429 * // 3: 3
4430 * // 4: 5
4431 * // Array
4432 * // (
4433 * // [1] => 4
4434 * // [2] => 2
4435 * // [3] => 3
4436 * // [4] => 5
4437 * // )
4438 *
4439 * The map also supports multiple parallel iterators. That means that you can
4440 * nest foreach loops without affecting each other's iteration:
4441 *
4442 * foreach ($map as $index => $value) {
4443 * foreach ($map as $index2 => $value2) {
4444 * // ...
4445 * }
4446 * }
4447 *
4448 * @author Bernhard Schussek <bschussek@gmail.com>
4449 *
4450 * @template TValue
4451 *
4452 * @implements \ArrayAccess<string, TValue>
4453 * @implements \IteratorAggregate<string, TValue>
4454 */
4455class OrderedHashMap implements \ArrayAccess, \IteratorAggregate, \Countable
4456{
4457 /**
4458 * The keys of the map in the order in which they were inserted or changed.
4459 *
4460 * @var list<string>
4461 */
4462 private array $orderedKeys = [];
4463
4464 /**
4465 * References to the cursors of all open iterators.
4466 *
4467 * @var array<int, int>
4468 */
4469 private array $managedCursors = [];
4470
4471 /**
4472 * Creates a new map.
4473 *
4474 * @param TValue[] $elements The initial elements of the map, indexed by their keys
4475 */
4476 public function __construct(
4477 private array $elements = [],
4478 ) {
4479 // the explicit string type-cast is necessary as digit-only keys would be returned as integers otherwise
4480 $this->orderedKeys = array_map(strval(...), array_keys($elements));
4481 }
4482
4483 public function offsetExists(mixed $key): bool
4484 {
4485 return isset($this->elements[$key]);
4486 }
4487
4488 public function offsetGet(mixed $key): mixed
4489 {
4490 if (!isset($this->elements[$key])) {
4491 throw new \OutOfBoundsException(\sprintf('The offset "%s" does not exist.', $key));
4492 }
4493
4494 return $this->elements[$key];
4495 }
4496
4497 public function offsetSet(mixed $key, mixed $value): void
4498 {
4499 if (null === $key || !isset($this->elements[$key])) {
4500 if (null === $key) {
4501 $key = [] === $this->orderedKeys
4502 // If the array is empty, use 0 as key
4503 ? 0
4504 // Imitate PHP behavior of generating a key that equals
4505 // the highest existing integer key + 1
4506 : 1 + (int) max($this->orderedKeys);
4507 }
4508
4509 $this->orderedKeys[] = (string) $key;
4510 }
4511
4512 $this->elements[$key] = $value;
4513 }
4514
4515 public function offsetUnset(mixed $key): void
4516 {
4517 if (false !== ($position = array_search((string) $key, $this->orderedKeys))) {
4518 array_splice($this->orderedKeys, $position, 1);
4519 unset($this->elements[$key]);
4520
4521 foreach ($this->managedCursors as $i => $cursor) {
4522 if ($cursor >= $position) {
4523 --$this->managedCursors[$i];
4524 }
4525 }
4526 }
4527 }
4528
4529 public function getIterator(): \Traversable
4530 {
4531 return new OrderedHashMapIterator($this->elements, $this->orderedKeys, $this->managedCursors);
4532 }
4533
4534 public function count(): int
4535 {
4536 return \count($this->elements);
4537 }
4538}
4539
4540------------------------------------------------------------------------------------------------------------------------
4541 ./Util/StringUtil.php
4542------------------------------------------------------------------------------------------------------------------------
4543<?php
4544
4545/*
4546 * This file is part of the Symfony package.
4547 *
4548 * (c) Fabien Potencier <fabien@symfony.com>
4549 *
4550 * For the full copyright and license information, please view the LICENSE
4551 * file that was distributed with this source code.
4552 */
4553
4554namespace Symfony\Component\Form\Util;
4555
4556/**
4557 * @author Issei Murasawa <issei.m7@gmail.com>
4558 * @author Bernhard Schussek <bschussek@gmail.com>
4559 */
4560class StringUtil
4561{
4562 /**
4563 * This class should not be instantiated.
4564 */
4565 private function __construct()
4566 {
4567 }
4568
4569 /**
4570 * Returns the trimmed data.
4571 */
4572 public static function trim(string $string): string
4573 {
4574 if (null !== $result = @preg_replace('/^[\pZ\p{Cc}\p{Cf}]+|[\pZ\p{Cc}\p{Cf}]+$/u', '', $string)) {
4575 return $result;
4576 }
4577
4578 return trim($string);
4579 }
4580
4581 /**
4582 * Converts a fully-qualified class name to a block prefix.
4583 *
4584 * @param string $fqcn The fully-qualified class name
4585 */
4586 public static function fqcnToBlockPrefix(string $fqcn): ?string
4587 {
4588 // Non-greedy ("+?") to match "type" suffix, if present
4589 if (preg_match('~([^\\\\]+?)(type)?$~i', $fqcn, $matches)) {
4590 return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $matches[1]));
4591 }
4592
4593 return null;
4594 }
4595}
4596
4597------------------------------------------------------------------------------------------------------------------------
4598 ./Util/FormUtil.php
4599------------------------------------------------------------------------------------------------------------------------
4600<?php
4601
4602/*
4603 * This file is part of the Symfony package.
4604 *
4605 * (c) Fabien Potencier <fabien@symfony.com>
4606 *
4607 * For the full copyright and license information, please view the LICENSE
4608 * file that was distributed with this source code.
4609 */
4610
4611namespace Symfony\Component\Form\Util;
4612
4613/**
4614 * @author Bernhard Schussek <bschussek@gmail.com>
4615 */
4616class FormUtil
4617{
4618 /**
4619 * This class should not be instantiated.
4620 */
4621 private function __construct()
4622 {
4623 }
4624
4625 /**
4626 * Returns whether the given data is empty.
4627 *
4628 * This logic is reused multiple times throughout the processing of
4629 * a form and needs to be consistent. PHP keyword `empty` cannot
4630 * be used as it also considers 0 and "0" to be empty.
4631 */
4632 public static function isEmpty(mixed $data): bool
4633 {
4634 // Should not do a check for [] === $data!!!
4635 // This method is used in occurrences where arrays are
4636 // not considered to be empty, ever.
4637 return null === $data || '' === $data;
4638 }
4639
4640 /**
4641 * Recursively replaces or appends elements of the first array with elements
4642 * of second array. If the key is an integer, the values will be appended to
4643 * the new array; otherwise, the value from the second array will replace
4644 * the one from the first array.
4645 */
4646 public static function mergeParamsAndFiles(array $params, array $files): array
4647 {
4648 $isFilesList = array_is_list($files);
4649
4650 foreach ($params as $key => $value) {
4651 if (\is_array($value) && \is_array($files[$key] ?? null)) {
4652 $params[$key] = self::mergeParamsAndFiles($value, $files[$key]);
4653 unset($files[$key]);
4654 }
4655 }
4656
4657 if (!$isFilesList) {
4658 return array_replace($params, $files);
4659 }
4660
4661 foreach ($files as $value) {
4662 $params[] = $value;
4663 }
4664
4665 return $params;
4666 }
4667}
4668
4669------------------------------------------------------------------------------------------------------------------------
4670 ./FormTypeGuesserInterface.php
4671------------------------------------------------------------------------------------------------------------------------
4672<?php
4673
4674/*
4675 * This file is part of the Symfony package.
4676 *
4677 * (c) Fabien Potencier <fabien@symfony.com>
4678 *
4679 * For the full copyright and license information, please view the LICENSE
4680 * file that was distributed with this source code.
4681 */
4682
4683namespace Symfony\Component\Form;
4684
4685/**
4686 * @author Bernhard Schussek <bschussek@gmail.com>
4687 */
4688interface FormTypeGuesserInterface
4689{
4690 /**
4691 * Returns a field guess for a property name of a class.
4692 */
4693 public function guessType(string $class, string $property): ?Guess\TypeGuess;
4694
4695 /**
4696 * Returns a guess whether a property of a class is required.
4697 */
4698 public function guessRequired(string $class, string $property): ?Guess\ValueGuess;
4699
4700 /**
4701 * Returns a guess about the field's maximum length.
4702 */
4703 public function guessMaxLength(string $class, string $property): ?Guess\ValueGuess;
4704
4705 /**
4706 * Returns a guess about the field's pattern.
4707 */
4708 public function guessPattern(string $class, string $property): ?Guess\ValueGuess;
4709}
4710
4711------------------------------------------------------------------------------------------------------------------------
4712 ./ChoiceList/Loader/IntlCallbackChoiceLoader.php
4713------------------------------------------------------------------------------------------------------------------------
4714<?php
4715
4716/*
4717 * This file is part of the Symfony package.
4718 *
4719 * (c) Fabien Potencier <fabien@symfony.com>
4720 *
4721 * For the full copyright and license information, please view the LICENSE
4722 * file that was distributed with this source code.
4723 */
4724
4725namespace Symfony\Component\Form\ChoiceList\Loader;
4726
4727/**
4728 * Callback choice loader optimized for Intl choice types.
4729 *
4730 * @author Jules Pietri <jules@heahprod.com>
4731 * @author Yonel Ceruto <yonelceruto@gmail.com>
4732 */
4733class IntlCallbackChoiceLoader extends CallbackChoiceLoader
4734{
4735 public function loadChoicesForValues(array $values, ?callable $value = null): array
4736 {
4737 return parent::loadChoicesForValues(array_filter($values), $value);
4738 }
4739
4740 public function loadValuesForChoices(array $choices, ?callable $value = null): array
4741 {
4742 $choices = array_filter($choices);
4743
4744 // If no callable is set, choices are the same as values
4745 if (null === $value) {
4746 return $choices;
4747 }
4748
4749 return parent::loadValuesForChoices($choices, $value);
4750 }
4751}
4752
4753------------------------------------------------------------------------------------------------------------------------
4754 ./ChoiceList/Loader/AbstractChoiceLoader.php
4755------------------------------------------------------------------------------------------------------------------------
4756<?php
4757
4758/*
4759 * This file is part of the Symfony package.
4760 *
4761 * (c) Fabien Potencier <fabien@symfony.com>
4762 *
4763 * For the full copyright and license information, please view the LICENSE
4764 * file that was distributed with this source code.
4765 */
4766
4767namespace Symfony\Component\Form\ChoiceList\Loader;
4768
4769use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
4770use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
4771
4772/**
4773 * @author Jules Pietri <jules@heahprod.com>
4774 */
4775abstract class AbstractChoiceLoader implements ChoiceLoaderInterface
4776{
4777 private ?iterable $choices;
4778
4779 /**
4780 * @final
4781 */
4782 public function loadChoiceList(?callable $value = null): ChoiceListInterface
4783 {
4784 return new ArrayChoiceList($this->choices ??= $this->loadChoices(), $value);
4785 }
4786
4787 public function loadChoicesForValues(array $values, ?callable $value = null): array
4788 {
4789 if (!$values) {
4790 return [];
4791 }
4792
4793 return $this->doLoadChoicesForValues($values, $value);
4794 }
4795
4796 public function loadValuesForChoices(array $choices, ?callable $value = null): array
4797 {
4798 if (!$choices) {
4799 return [];
4800 }
4801
4802 if ($value) {
4803 // if a value callback exists, use it
4804 return array_map(fn ($item) => (string) $value($item), $choices);
4805 }
4806
4807 return $this->doLoadValuesForChoices($choices);
4808 }
4809
4810 abstract protected function loadChoices(): iterable;
4811
4812 protected function doLoadChoicesForValues(array $values, ?callable $value): array
4813 {
4814 return $this->loadChoiceList($value)->getChoicesForValues($values);
4815 }
4816
4817 protected function doLoadValuesForChoices(array $choices): array
4818 {
4819 return $this->loadChoiceList()->getValuesForChoices($choices);
4820 }
4821}
4822
4823------------------------------------------------------------------------------------------------------------------------
4824 ./ChoiceList/Loader/FilterChoiceLoaderDecorator.php
4825------------------------------------------------------------------------------------------------------------------------
4826<?php
4827
4828/*
4829 * This file is part of the Symfony package.
4830 *
4831 * (c) Fabien Potencier <fabien@symfony.com>
4832 *
4833 * For the full copyright and license information, please view the LICENSE
4834 * file that was distributed with this source code.
4835 */
4836
4837namespace Symfony\Component\Form\ChoiceList\Loader;
4838
4839/**
4840 * A decorator to filter choices only when they are loaded or partially loaded.
4841 *
4842 * @author Jules Pietri <jules@heahprod.com>
4843 */
4844class FilterChoiceLoaderDecorator extends AbstractChoiceLoader
4845{
4846 private ChoiceLoaderInterface $decoratedLoader;
4847 private \Closure $filter;
4848
4849 public function __construct(ChoiceLoaderInterface $loader, callable $filter)
4850 {
4851 $this->decoratedLoader = $loader;
4852 $this->filter = $filter(...);
4853 }
4854
4855 protected function loadChoices(): iterable
4856 {
4857 $list = $this->decoratedLoader->loadChoiceList();
4858
4859 if (array_values($list->getValues()) === array_values($structuredValues = $list->getStructuredValues())) {
4860 return array_filter(array_combine($list->getOriginalKeys(), $list->getChoices()), $this->filter);
4861 }
4862
4863 foreach ($structuredValues as $group => $values) {
4864 if (\is_array($values)) {
4865 if ($values && $filtered = array_filter($list->getChoicesForValues($values), $this->filter)) {
4866 $choices[$group] = $filtered;
4867 }
4868 continue;
4869 // filter empty groups
4870 }
4871
4872 if ($filtered = array_filter($list->getChoicesForValues([$values]), $this->filter)) {
4873 $choices[$group] = $filtered[0];
4874 }
4875 }
4876
4877 return $choices ?? [];
4878 }
4879
4880 public function loadChoicesForValues(array $values, ?callable $value = null): array
4881 {
4882 return array_filter($this->decoratedLoader->loadChoicesForValues($values, $value), $this->filter);
4883 }
4884
4885 public function loadValuesForChoices(array $choices, ?callable $value = null): array
4886 {
4887 return $this->decoratedLoader->loadValuesForChoices(array_filter($choices, $this->filter), $value);
4888 }
4889}
4890
4891------------------------------------------------------------------------------------------------------------------------
4892 ./ChoiceList/Loader/LazyChoiceLoader.php
4893------------------------------------------------------------------------------------------------------------------------
4894<?php
4895
4896/*
4897 * This file is part of the Symfony package.
4898 *
4899 * (c) Fabien Potencier <fabien@symfony.com>
4900 *
4901 * For the full copyright and license information, please view the LICENSE
4902 * file that was distributed with this source code.
4903 */
4904
4905namespace Symfony\Component\Form\ChoiceList\Loader;
4906
4907use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
4908use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
4909
4910/**
4911 * A choice loader that loads its choices and values lazily, only when necessary.
4912 *
4913 * @author Yonel Ceruto <yonelceruto@gmail.com>
4914 */
4915class LazyChoiceLoader implements ChoiceLoaderInterface
4916{
4917 private ?ChoiceListInterface $choiceList = null;
4918
4919 public function __construct(
4920 private readonly ChoiceLoaderInterface $loader,
4921 ) {
4922 }
4923
4924 public function loadChoiceList(?callable $value = null): ChoiceListInterface
4925 {
4926 return $this->choiceList ??= new ArrayChoiceList([], $value);
4927 }
4928
4929 public function loadChoicesForValues(array $values, ?callable $value = null): array
4930 {
4931 $choices = $this->loader->loadChoicesForValues($values, $value);
4932 $this->choiceList = new ArrayChoiceList($choices, $value);
4933
4934 return $choices;
4935 }
4936
4937 public function loadValuesForChoices(array $choices, ?callable $value = null): array
4938 {
4939 $values = $this->loader->loadValuesForChoices($choices, $value);
4940
4941 if ($this->choiceList?->getValuesForChoices($choices) !== $values) {
4942 $this->loadChoicesForValues($values, $value);
4943 }
4944
4945 return $values;
4946 }
4947}
4948
4949------------------------------------------------------------------------------------------------------------------------
4950 ./ChoiceList/Loader/CallbackChoiceLoader.php
4951------------------------------------------------------------------------------------------------------------------------
4952<?php
4953
4954/*
4955 * This file is part of the Symfony package.
4956 *
4957 * (c) Fabien Potencier <fabien@symfony.com>
4958 *
4959 * For the full copyright and license information, please view the LICENSE
4960 * file that was distributed with this source code.
4961 */
4962
4963namespace Symfony\Component\Form\ChoiceList\Loader;
4964
4965/**
4966 * Loads an {@link ArrayChoiceList} instance from a callable returning iterable choices.
4967 *
4968 * @author Jules Pietri <jules@heahprod.com>
4969 */
4970class CallbackChoiceLoader extends AbstractChoiceLoader
4971{
4972 private \Closure $callback;
4973
4974 /**
4975 * @param callable $callback The callable returning iterable choices
4976 */
4977 public function __construct(callable $callback)
4978 {
4979 $this->callback = $callback(...);
4980 }
4981
4982 protected function loadChoices(): iterable
4983 {
4984 return ($this->callback)();
4985 }
4986}
4987
4988------------------------------------------------------------------------------------------------------------------------
4989 ./ChoiceList/Loader/ChoiceLoaderInterface.php
4990------------------------------------------------------------------------------------------------------------------------
4991<?php
4992
4993/*
4994 * This file is part of the Symfony package.
4995 *
4996 * (c) Fabien Potencier <fabien@symfony.com>
4997 *
4998 * For the full copyright and license information, please view the LICENSE
4999 * file that was distributed with this source code.
5000 */
5001
5002namespace Symfony\Component\Form\ChoiceList\Loader;
5003
5004use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
5005
5006/**
5007 * Loads a choice list.
5008 *
5009 * The methods {@link loadChoicesForValues()} and {@link loadValuesForChoices()}
5010 * can be used to load the list only partially in cases where a fully-loaded
5011 * list is not necessary.
5012 *
5013 * @author Bernhard Schussek <bschussek@gmail.com>
5014 */
5015interface ChoiceLoaderInterface
5016{
5017 /**
5018 * Loads a list of choices.
5019 *
5020 * Optionally, a callable can be passed for generating the choice values.
5021 * The callable receives the choice as only argument.
5022 * Null may be passed when the choice list contains the empty value.
5023 *
5024 * @param callable|null $value The callable which generates the values
5025 * from choices
5026 */
5027 public function loadChoiceList(?callable $value = null): ChoiceListInterface;
5028
5029 /**
5030 * Loads the choices corresponding to the given values.
5031 *
5032 * The choices are returned with the same keys and in the same order as the
5033 * corresponding values in the given array.
5034 *
5035 * Optionally, a callable can be passed for generating the choice values.
5036 * The callable receives the choice as only argument.
5037 * Null may be passed when the choice list contains the empty value.
5038 *
5039 * @param string[] $values An array of choice values. Non-existing
5040 * values in this array are ignored
5041 * @param callable|null $value The callable generating the choice values
5042 */
5043 public function loadChoicesForValues(array $values, ?callable $value = null): array;
5044
5045 /**
5046 * Loads the values corresponding to the given choices.
5047 *
5048 * The values are returned with the same keys and in the same order as the
5049 * corresponding choices in the given array.
5050 *
5051 * Optionally, a callable can be passed for generating the choice values.
5052 * The callable receives the choice as only argument.
5053 * Null may be passed when the choice list contains the empty value.
5054 *
5055 * @param array $choices An array of choices. Non-existing choices in
5056 * this array are ignored
5057 * @param callable|null $value The callable generating the choice values
5058 *
5059 * @return string[]
5060 */
5061 public function loadValuesForChoices(array $choices, ?callable $value = null): array;
5062}
5063
5064------------------------------------------------------------------------------------------------------------------------
5065 ./ChoiceList/ChoiceList.php
5066------------------------------------------------------------------------------------------------------------------------
5067<?php
5068
5069/*
5070 * This file is part of the Symfony package.
5071 *
5072 * (c) Fabien Potencier <fabien@symfony.com>
5073 *
5074 * For the full copyright and license information, please view the LICENSE
5075 * file that was distributed with this source code.
5076 */
5077
5078namespace Symfony\Component\Form\ChoiceList;
5079
5080use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
5081use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
5082use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFilter;
5083use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
5084use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
5085use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceTranslationParameters;
5086use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
5087use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy;
5088use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice;
5089use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
5090use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
5091use Symfony\Component\Form\FormTypeExtensionInterface;
5092use Symfony\Component\Form\FormTypeInterface;
5093
5094/**
5095 * A set of convenient static methods to create cacheable choice list options.
5096 *
5097 * @author Jules Pietri <jules@heahprod.com>
5098 */
5099final class ChoiceList
5100{
5101 /**
5102 * Creates a cacheable loader from any callable providing iterable choices.
5103 *
5104 * @param callable $choices A callable that must return iterable choices or grouped choices
5105 * @param mixed $vary Dynamic data used to compute a unique hash when caching the loader
5106 */
5107 public static function lazy(FormTypeInterface|FormTypeExtensionInterface $formType, callable $choices, mixed $vary = null): ChoiceLoader
5108 {
5109 return self::loader($formType, new CallbackChoiceLoader($choices), $vary);
5110 }
5111
5112 /**
5113 * Decorates a loader to make it cacheable.
5114 *
5115 * @param ChoiceLoaderInterface $loader A loader responsible for creating loading choices or grouped choices
5116 * @param mixed $vary Dynamic data used to compute a unique hash when caching the loader
5117 */
5118 public static function loader(FormTypeInterface|FormTypeExtensionInterface $formType, ChoiceLoaderInterface $loader, mixed $vary = null): ChoiceLoader
5119 {
5120 return new ChoiceLoader($formType, $loader, $vary);
5121 }
5122
5123 /**
5124 * Decorates a "choice_value" callback to make it cacheable.
5125 *
5126 * @param callable|array $value Any pseudo callable to create a unique string value from a choice
5127 * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback
5128 */
5129 public static function value(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $value, mixed $vary = null): ChoiceValue
5130 {
5131 return new ChoiceValue($formType, $value, $vary);
5132 }
5133
5134 /**
5135 * @param callable|array $filter Any pseudo callable to filter a choice list
5136 * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback
5137 */
5138 public static function filter(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $filter, mixed $vary = null): ChoiceFilter
5139 {
5140 return new ChoiceFilter($formType, $filter, $vary);
5141 }
5142
5143 /**
5144 * Decorates a "choice_label" option to make it cacheable.
5145 *
5146 * @param callable|false $label Any pseudo callable to create a label from a choice or false to discard it
5147 * @param mixed $vary Dynamic data used to compute a unique hash when caching the option
5148 */
5149 public static function label(FormTypeInterface|FormTypeExtensionInterface $formType, callable|false $label, mixed $vary = null): ChoiceLabel
5150 {
5151 return new ChoiceLabel($formType, $label, $vary);
5152 }
5153
5154 /**
5155 * Decorates a "choice_name" callback to make it cacheable.
5156 *
5157 * @param callable|array $fieldName Any pseudo callable to create a field name from a choice
5158 * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback
5159 */
5160 public static function fieldName(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $fieldName, mixed $vary = null): ChoiceFieldName
5161 {
5162 return new ChoiceFieldName($formType, $fieldName, $vary);
5163 }
5164
5165 /**
5166 * Decorates a "choice_attr" option to make it cacheable.
5167 *
5168 * @param callable|array $attr Any pseudo callable or array to create html attributes from a choice
5169 * @param mixed $vary Dynamic data used to compute a unique hash when caching the option
5170 */
5171 public static function attr(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $attr, mixed $vary = null): ChoiceAttr
5172 {
5173 return new ChoiceAttr($formType, $attr, $vary);
5174 }
5175
5176 /**
5177 * Decorates a "choice_translation_parameters" option to make it cacheable.
5178 *
5179 * @param callable|array $translationParameters Any pseudo callable or array to create translation parameters from a choice
5180 * @param mixed $vary Dynamic data used to compute a unique hash when caching the option
5181 */
5182 public static function translationParameters(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $translationParameters, mixed $vary = null): ChoiceTranslationParameters
5183 {
5184 return new ChoiceTranslationParameters($formType, $translationParameters, $vary);
5185 }
5186
5187 /**
5188 * Decorates a "group_by" callback to make it cacheable.
5189 *
5190 * @param callable|array $groupBy Any pseudo callable to return a group name from a choice
5191 * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback
5192 */
5193 public static function groupBy(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $groupBy, mixed $vary = null): GroupBy
5194 {
5195 return new GroupBy($formType, $groupBy, $vary);
5196 }
5197
5198 /**
5199 * Decorates a "preferred_choices" option to make it cacheable.
5200 *
5201 * @param callable|array $preferred Any pseudo callable or array to return a group name from a choice
5202 * @param mixed $vary Dynamic data used to compute a unique hash when caching the option
5203 */
5204 public static function preferred(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $preferred, mixed $vary = null): PreferredChoice
5205 {
5206 return new PreferredChoice($formType, $preferred, $vary);
5207 }
5208
5209 /**
5210 * Should not be instantiated.
5211 */
5212 private function __construct()
5213 {
5214 }
5215}
5216
5217------------------------------------------------------------------------------------------------------------------------
5218 ./ChoiceList/ChoiceListInterface.php
5219------------------------------------------------------------------------------------------------------------------------
5220<?php
5221
5222/*
5223 * This file is part of the Symfony package.
5224 *
5225 * (c) Fabien Potencier <fabien@symfony.com>
5226 *
5227 * For the full copyright and license information, please view the LICENSE
5228 * file that was distributed with this source code.
5229 */
5230
5231namespace Symfony\Component\Form\ChoiceList;
5232
5233/**
5234 * A list of choices that can be selected in a choice field.
5235 *
5236 * A choice list assigns unique string values to each of a list of choices.
5237 * These string values are displayed in the "value" attributes in HTML and
5238 * submitted back to the server.
5239 *
5240 * The acceptable data types for the choices depend on the implementation.
5241 * Values must always be strings and (within the list) free of duplicates.
5242 *
5243 * @author Bernhard Schussek <bschussek@gmail.com>
5244 */
5245interface ChoiceListInterface
5246{
5247 /**
5248 * Returns all selectable choices.
5249 *
5250 * @return array The selectable choices indexed by the corresponding values
5251 */
5252 public function getChoices(): array;
5253
5254 /**
5255 * Returns the values for the choices.
5256 *
5257 * The values are strings that do not contain duplicates:
5258 *
5259 * $form->add('field', 'choice', [
5260 * 'choices' => [
5261 * 'Decided' => ['Yes' => true, 'No' => false],
5262 * 'Undecided' => ['Maybe' => null],
5263 * ],
5264 * ]);
5265 *
5266 * In this example, the result of this method is:
5267 *
5268 * [
5269 * 'Yes' => '0',
5270 * 'No' => '1',
5271 * 'Maybe' => '2',
5272 * ]
5273 *
5274 * Null and false MUST NOT conflict when being casted to string.
5275 * For this some default incremented values SHOULD be computed.
5276 *
5277 * @return string[]
5278 */
5279 public function getValues(): array;
5280
5281 /**
5282 * Returns the values in the structure originally passed to the list.
5283 *
5284 * Contrary to {@link getValues()}, the result is indexed by the original
5285 * keys of the choices. If the original array contained nested arrays, these
5286 * nested arrays are represented here as well:
5287 *
5288 * $form->add('field', 'choice', [
5289 * 'choices' => [
5290 * 'Decided' => ['Yes' => true, 'No' => false],
5291 * 'Undecided' => ['Maybe' => null],
5292 * ],
5293 * ]);
5294 *
5295 * In this example, the result of this method is:
5296 *
5297 * [
5298 * 'Decided' => ['Yes' => '0', 'No' => '1'],
5299 * 'Undecided' => ['Maybe' => '2'],
5300 * ]
5301 *
5302 * Nested arrays do not make sense in a view format unless
5303 * they are used as a convenient way of grouping.
5304 * If the implementation does not intend to support grouped choices,
5305 * this method SHOULD be equivalent to {@link getValues()}.
5306 * The $groupBy callback parameter SHOULD be used instead.
5307 *
5308 * @return string[]
5309 */
5310 public function getStructuredValues(): array;
5311
5312 /**
5313 * Returns the original keys of the choices.
5314 *
5315 * The original keys are the keys of the choice array that was passed in the
5316 * "choice" option of the choice type. Note that this array may contain
5317 * duplicates if the "choice" option contained choice groups:
5318 *
5319 * $form->add('field', 'choice', [
5320 * 'choices' => [
5321 * 'Decided' => [true, false],
5322 * 'Undecided' => [null],
5323 * ],
5324 * ]);
5325 *
5326 * In this example, the original key 0 appears twice, once for `true` and
5327 * once for `null`.
5328 *
5329 * @return int[]|string[] The original choice keys indexed by the
5330 * corresponding choice values
5331 */
5332 public function getOriginalKeys(): array;
5333
5334 /**
5335 * Returns the choices corresponding to the given values.
5336 *
5337 * The choices are returned with the same keys and in the same order as the
5338 * corresponding values in the given array.
5339 *
5340 * @param string[] $values An array of choice values. Non-existing values in
5341 * this array are ignored
5342 */
5343 public function getChoicesForValues(array $values): array;
5344
5345 /**
5346 * Returns the values corresponding to the given choices.
5347 *
5348 * The values are returned with the same keys and in the same order as the
5349 * corresponding choices in the given array.
5350 *
5351 * @param array $choices An array of choices. Non-existing choices in this
5352 * array are ignored
5353 *
5354 * @return string[]
5355 */
5356 public function getValuesForChoices(array $choices): array;
5357}
5358
5359------------------------------------------------------------------------------------------------------------------------
5360 ./ChoiceList/LazyChoiceList.php
5361------------------------------------------------------------------------------------------------------------------------
5362<?php
5363
5364/*
5365 * This file is part of the Symfony package.
5366 *
5367 * (c) Fabien Potencier <fabien@symfony.com>
5368 *
5369 * For the full copyright and license information, please view the LICENSE
5370 * file that was distributed with this source code.
5371 */
5372
5373namespace Symfony\Component\Form\ChoiceList;
5374
5375use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
5376
5377/**
5378 * A choice list that loads its choices lazily.
5379 *
5380 * The choices are fetched using a {@link ChoiceLoaderInterface} instance.
5381 * If only {@link getChoicesForValues()} or {@link getValuesForChoices()} is
5382 * called, the choice list is only loaded partially for improved performance.
5383 *
5384 * Once {@link getChoices()} or {@link getValues()} is called, the list is
5385 * loaded fully.
5386 *
5387 * @author Bernhard Schussek <bschussek@gmail.com>
5388 */
5389class LazyChoiceList implements ChoiceListInterface
5390{
5391 /**
5392 * The callable creating string values for each choice.
5393 *
5394 * If null, choices are cast to strings.
5395 */
5396 private ?\Closure $value;
5397
5398 /**
5399 * Creates a lazily-loaded list using the given loader.
5400 *
5401 * Optionally, a callable can be passed for generating the choice values.
5402 * The callable receives the choice as first and the array key as the second
5403 * argument.
5404 *
5405 * @param callable|null $value The callable creating string values for each choice.
5406 * If null, choices are cast to strings.
5407 */
5408 public function __construct(
5409 private ChoiceLoaderInterface $loader,
5410 ?callable $value = null,
5411 ) {
5412 $this->value = null === $value ? null : $value(...);
5413 }
5414
5415 public function getChoices(): array
5416 {
5417 return $this->loader->loadChoiceList($this->value)->getChoices();
5418 }
5419
5420 public function getValues(): array
5421 {
5422 return $this->loader->loadChoiceList($this->value)->getValues();
5423 }
5424
5425 public function getStructuredValues(): array
5426 {
5427 return $this->loader->loadChoiceList($this->value)->getStructuredValues();
5428 }
5429
5430 public function getOriginalKeys(): array
5431 {
5432 return $this->loader->loadChoiceList($this->value)->getOriginalKeys();
5433 }
5434
5435 public function getChoicesForValues(array $values): array
5436 {
5437 return $this->loader->loadChoicesForValues($values, $this->value);
5438 }
5439
5440 public function getValuesForChoices(array $choices): array
5441 {
5442 return $this->loader->loadValuesForChoices($choices, $this->value);
5443 }
5444}
5445
5446------------------------------------------------------------------------------------------------------------------------
5447 ./ChoiceList/ArrayChoiceList.php
5448------------------------------------------------------------------------------------------------------------------------
5449<?php
5450
5451/*
5452 * This file is part of the Symfony package.
5453 *
5454 * (c) Fabien Potencier <fabien@symfony.com>
5455 *
5456 * For the full copyright and license information, please view the LICENSE
5457 * file that was distributed with this source code.
5458 */
5459
5460namespace Symfony\Component\Form\ChoiceList;
5461
5462/**
5463 * A list of choices with arbitrary data types.
5464 *
5465 * The user of this class is responsible for assigning string values to the
5466 * choices and for their uniqueness.
5467 * Both the choices and their values are passed to the constructor.
5468 * Each choice must have a corresponding value (with the same key) in
5469 * the values array.
5470 *
5471 * @author Bernhard Schussek <bschussek@gmail.com>
5472 */
5473class ArrayChoiceList implements ChoiceListInterface
5474{
5475 protected array $choices;
5476
5477 /**
5478 * The values indexed by the original keys.
5479 */
5480 protected array $structuredValues;
5481
5482 /**
5483 * The original keys of the choices array.
5484 */
5485 protected array $originalKeys;
5486 protected ?\Closure $valueCallback = null;
5487
5488 /**
5489 * Creates a list with the given choices and values.
5490 *
5491 * The given choice array must have the same array keys as the value array.
5492 *
5493 * @param iterable $choices The selectable choices
5494 * @param callable|null $value The callable for creating the value
5495 * for a choice. If `null` is passed,
5496 * incrementing integers are used as
5497 * values
5498 */
5499 public function __construct(iterable $choices, ?callable $value = null)
5500 {
5501 if ($choices instanceof \Traversable) {
5502 $choices = iterator_to_array($choices);
5503 }
5504
5505 if (null === $value && $this->castableToString($choices)) {
5506 $value = static fn ($choice) => false === $choice ? '0' : (string) $choice;
5507 }
5508
5509 if (null !== $value) {
5510 // If a deterministic value generator was passed, use it later
5511 $this->valueCallback = $value(...);
5512 } else {
5513 // Otherwise generate incrementing integers as values
5514 $value = static function () {
5515 static $i = 0;
5516
5517 return $i++;
5518 };
5519 }
5520
5521 // If the choices are given as recursive array (i.e. with explicit
5522 // choice groups), flatten the array. The grouping information is needed
5523 // in the view only.
5524 $this->flatten($choices, $value, $choicesByValues, $keysByValues, $structuredValues);
5525
5526 $this->choices = $choicesByValues;
5527 $this->originalKeys = $keysByValues;
5528 $this->structuredValues = $structuredValues;
5529 }
5530
5531 public function getChoices(): array
5532 {
5533 return $this->choices;
5534 }
5535
5536 public function getValues(): array
5537 {
5538 return array_map('strval', array_keys($this->choices));
5539 }
5540
5541 public function getStructuredValues(): array
5542 {
5543 return $this->structuredValues;
5544 }
5545
5546 public function getOriginalKeys(): array
5547 {
5548 return $this->originalKeys;
5549 }
5550
5551 public function getChoicesForValues(array $values): array
5552 {
5553 $choices = [];
5554
5555 foreach ($values as $i => $givenValue) {
5556 if (\array_key_exists($givenValue, $this->choices)) {
5557 $choices[$i] = $this->choices[$givenValue];
5558 }
5559 }
5560
5561 return $choices;
5562 }
5563
5564 public function getValuesForChoices(array $choices): array
5565 {
5566 $values = [];
5567
5568 // Use the value callback to compare choices by their values, if present
5569 if ($this->valueCallback) {
5570 $givenValues = [];
5571
5572 foreach ($choices as $i => $givenChoice) {
5573 $givenValues[$i] = (string) ($this->valueCallback)($givenChoice);
5574 }
5575
5576 return array_intersect($givenValues, array_keys($this->choices));
5577 }
5578
5579 // Otherwise compare choices by identity
5580 foreach ($choices as $i => $givenChoice) {
5581 foreach ($this->choices as $value => $choice) {
5582 if ($choice === $givenChoice) {
5583 $values[$i] = (string) $value;
5584 break;
5585 }
5586 }
5587 }
5588
5589 return $values;
5590 }
5591
5592 /**
5593 * Flattens an array into the given output variables.
5594 *
5595 * @param array $choices The array to flatten
5596 * @param callable $value The callable for generating choice values
5597 * @param array|null $choicesByValues The flattened choices indexed by the
5598 * corresponding values
5599 * @param array|null $keysByValues The original keys indexed by the
5600 * corresponding values
5601 * @param array|null $structuredValues The values indexed by the original keys
5602 *
5603 * @internal
5604 */
5605 protected function flatten(array $choices, callable $value, ?array &$choicesByValues, ?array &$keysByValues, ?array &$structuredValues): void
5606 {
5607 if (null === $choicesByValues) {
5608 $choicesByValues = [];
5609 $keysByValues = [];
5610 $structuredValues = [];
5611 }
5612
5613 foreach ($choices as $key => $choice) {
5614 if (\is_array($choice)) {
5615 $this->flatten($choice, $value, $choicesByValues, $keysByValues, $structuredValues[$key]);
5616
5617 continue;
5618 }
5619
5620 $choiceValue = (string) $value($choice);
5621 $choicesByValues[$choiceValue] = $choice;
5622 $keysByValues[$choiceValue] = $key;
5623 $structuredValues[$key] = $choiceValue;
5624 }
5625 }
5626
5627 /**
5628 * Checks whether the given choices can be cast to strings without
5629 * generating duplicates.
5630 * This method is responsible for preventing conflict between scalar values
5631 * and the empty value.
5632 */
5633 private function castableToString(array $choices, array &$cache = []): bool
5634 {
5635 foreach ($choices as $choice) {
5636 if (\is_array($choice)) {
5637 if (!$this->castableToString($choice, $cache)) {
5638 return false;
5639 }
5640
5641 continue;
5642 } elseif (!\is_scalar($choice)) {
5643 return false;
5644 }
5645
5646 // prevent having false casted to the empty string by isset()
5647 $choice = false === $choice ? '0' : (string) $choice;
5648
5649 if (isset($cache[$choice])) {
5650 return false;
5651 }
5652
5653 $cache[$choice] = true;
5654 }
5655
5656 return true;
5657 }
5658}
5659
5660------------------------------------------------------------------------------------------------------------------------
5661 ./ChoiceList/Factory/CachingFactoryDecorator.php
5662------------------------------------------------------------------------------------------------------------------------
5663<?php
5664
5665/*
5666 * This file is part of the Symfony package.
5667 *
5668 * (c) Fabien Potencier <fabien@symfony.com>
5669 *
5670 * For the full copyright and license information, please view the LICENSE
5671 * file that was distributed with this source code.
5672 */
5673
5674namespace Symfony\Component\Form\ChoiceList\Factory;
5675
5676use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
5677use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
5678use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
5679use Symfony\Contracts\Service\ResetInterface;
5680
5681/**
5682 * Caches the choice lists created by the decorated factory.
5683 *
5684 * To cache a list based on its options, arguments must be decorated
5685 * by a {@see Cache\AbstractStaticOption} implementation.
5686 *
5687 * @author Bernhard Schussek <bschussek@gmail.com>
5688 * @author Jules Pietri <jules@heahprod.com>
5689 */
5690class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterface
5691{
5692 /**
5693 * @var ChoiceListInterface[]
5694 */
5695 private array $lists = [];
5696
5697 /**
5698 * @var ChoiceListView[]
5699 */
5700 private array $views = [];
5701
5702 /**
5703 * Generates a SHA-256 hash for the given value.
5704 *
5705 * Optionally, a namespace string can be passed. Calling this method will
5706 * the same values, but different namespaces, will return different hashes.
5707 *
5708 * @return string The SHA-256 hash
5709 *
5710 * @internal
5711 */
5712 public static function generateHash(mixed $value, string $namespace = ''): string
5713 {
5714 if (\is_object($value)) {
5715 $value = spl_object_hash($value);
5716 } elseif (\is_array($value)) {
5717 array_walk_recursive($value, static function (&$v) {
5718 if (\is_object($v)) {
5719 $v = spl_object_hash($v);
5720 }
5721 });
5722 }
5723
5724 return hash('sha256', $namespace.':'.serialize($value));
5725 }
5726
5727 public function __construct(
5728 private ChoiceListFactoryInterface $decoratedFactory,
5729 ) {
5730 }
5731
5732 /**
5733 * Returns the decorated factory.
5734 */
5735 public function getDecoratedFactory(): ChoiceListFactoryInterface
5736 {
5737 return $this->decoratedFactory;
5738 }
5739
5740 public function createListFromChoices(iterable $choices, mixed $value = null, mixed $filter = null): ChoiceListInterface
5741 {
5742 if ($choices instanceof \Traversable) {
5743 $choices = iterator_to_array($choices);
5744 }
5745
5746 $cache = true;
5747 // Only cache per value and filter when needed. The value is not validated on purpose.
5748 // The decorated factory may decide which values to accept and which not.
5749 if ($value instanceof Cache\ChoiceValue) {
5750 $value = $value->getOption();
5751 } elseif ($value) {
5752 $cache = false;
5753 }
5754 if ($filter instanceof Cache\ChoiceFilter) {
5755 $filter = $filter->getOption();
5756 } elseif ($filter) {
5757 $cache = false;
5758 }
5759
5760 if (!$cache) {
5761 return $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
5762 }
5763
5764 $hash = self::generateHash([$choices, $value, $filter], 'fromChoices');
5765
5766 if (!isset($this->lists[$hash])) {
5767 $this->lists[$hash] = $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
5768 }
5769
5770 return $this->lists[$hash];
5771 }
5772
5773 public function createListFromLoader(ChoiceLoaderInterface $loader, mixed $value = null, mixed $filter = null): ChoiceListInterface
5774 {
5775 $cache = true;
5776
5777 if ($loader instanceof Cache\ChoiceLoader) {
5778 $loader = $loader->getOption();
5779 } else {
5780 $cache = false;
5781 }
5782
5783 if ($value instanceof Cache\ChoiceValue) {
5784 $value = $value->getOption();
5785 } elseif ($value) {
5786 $cache = false;
5787 }
5788
5789 if ($filter instanceof Cache\ChoiceFilter) {
5790 $filter = $filter->getOption();
5791 } elseif ($filter) {
5792 $cache = false;
5793 }
5794
5795 if (!$cache) {
5796 return $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
5797 }
5798
5799 $hash = self::generateHash([$loader, $value, $filter], 'fromLoader');
5800
5801 if (!isset($this->lists[$hash])) {
5802 $this->lists[$hash] = $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
5803 }
5804
5805 return $this->lists[$hash];
5806 }
5807
5808 public function createView(ChoiceListInterface $list, mixed $preferredChoices = null, mixed $label = null, mixed $index = null, mixed $groupBy = null, mixed $attr = null, mixed $labelTranslationParameters = [], bool $duplicatePreferredChoices = true): ChoiceListView
5809 {
5810 $cache = true;
5811
5812 if ($preferredChoices instanceof Cache\PreferredChoice) {
5813 $preferredChoices = $preferredChoices->getOption();
5814 } elseif ($preferredChoices) {
5815 $cache = false;
5816 }
5817
5818 if ($label instanceof Cache\ChoiceLabel) {
5819 $label = $label->getOption();
5820 } elseif (null !== $label) {
5821 $cache = false;
5822 }
5823
5824 if ($index instanceof Cache\ChoiceFieldName) {
5825 $index = $index->getOption();
5826 } elseif ($index) {
5827 $cache = false;
5828 }
5829
5830 if ($groupBy instanceof Cache\GroupBy) {
5831 $groupBy = $groupBy->getOption();
5832 } elseif ($groupBy) {
5833 $cache = false;
5834 }
5835
5836 if ($attr instanceof Cache\ChoiceAttr) {
5837 $attr = $attr->getOption();
5838 } elseif ($attr) {
5839 $cache = false;
5840 }
5841
5842 if ($labelTranslationParameters instanceof Cache\ChoiceTranslationParameters) {
5843 $labelTranslationParameters = $labelTranslationParameters->getOption();
5844 } elseif ([] !== $labelTranslationParameters) {
5845 $cache = false;
5846 }
5847
5848 if (!$cache) {
5849 return $this->decoratedFactory->createView(
5850 $list,
5851 $preferredChoices,
5852 $label,
5853 $index,
5854 $groupBy,
5855 $attr,
5856 $labelTranslationParameters,
5857 $duplicatePreferredChoices,
5858 );
5859 }
5860
5861 $hash = self::generateHash([$list, $preferredChoices, $label, $index, $groupBy, $attr, $labelTranslationParameters, $duplicatePreferredChoices]);
5862
5863 if (!isset($this->views[$hash])) {
5864 $this->views[$hash] = $this->decoratedFactory->createView(
5865 $list,
5866 $preferredChoices,
5867 $label,
5868 $index,
5869 $groupBy,
5870 $attr,
5871 $labelTranslationParameters,
5872 $duplicatePreferredChoices,
5873 );
5874 }
5875
5876 return $this->views[$hash];
5877 }
5878
5879 public function reset(): void
5880 {
5881 $this->lists = [];
5882 $this->views = [];
5883 Cache\AbstractStaticOption::reset();
5884 }
5885}
5886
5887------------------------------------------------------------------------------------------------------------------------
5888 ./ChoiceList/Factory/Cache/GroupBy.php
5889------------------------------------------------------------------------------------------------------------------------
5890<?php
5891
5892/*
5893 * This file is part of the Symfony package.
5894 *
5895 * (c) Fabien Potencier <fabien@symfony.com>
5896 *
5897 * For the full copyright and license information, please view the LICENSE
5898 * file that was distributed with this source code.
5899 */
5900
5901namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
5902
5903use Symfony\Component\Form\FormTypeExtensionInterface;
5904use Symfony\Component\Form\FormTypeInterface;
5905
5906/**
5907 * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
5908 * which configures a "group_by" callback.
5909 *
5910 * @internal
5911 *
5912 * @author Jules Pietri <jules@heahprod.com>
5913 */
5914final class GroupBy extends AbstractStaticOption
5915{
5916}
5917
5918------------------------------------------------------------------------------------------------------------------------
5919 ./ChoiceList/Factory/Cache/ChoiceLabel.php
5920------------------------------------------------------------------------------------------------------------------------
5921<?php
5922
5923/*
5924 * This file is part of the Symfony package.
5925 *
5926 * (c) Fabien Potencier <fabien@symfony.com>
5927 *
5928 * For the full copyright and license information, please view the LICENSE
5929 * file that was distributed with this source code.
5930 */
5931
5932namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
5933
5934use Symfony\Component\Form\FormTypeExtensionInterface;
5935use Symfony\Component\Form\FormTypeInterface;
5936
5937/**
5938 * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
5939 * which configures a "choice_label" option.
5940 *
5941 * @internal
5942 *
5943 * @author Jules Pietri <jules@heahprod.com>
5944 */
5945final class ChoiceLabel extends AbstractStaticOption
5946{
5947}
5948
5949------------------------------------------------------------------------------------------------------------------------
5950 ./ChoiceList/Factory/Cache/ChoiceAttr.php
5951------------------------------------------------------------------------------------------------------------------------
5952<?php
5953
5954/*
5955 * This file is part of the Symfony package.
5956 *
5957 * (c) Fabien Potencier <fabien@symfony.com>
5958 *
5959 * For the full copyright and license information, please view the LICENSE
5960 * file that was distributed with this source code.
5961 */
5962
5963namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
5964
5965use Symfony\Component\Form\FormTypeExtensionInterface;
5966use Symfony\Component\Form\FormTypeInterface;
5967
5968/**
5969 * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
5970 * which configures a "choice_attr" option.
5971 *
5972 * @internal
5973 *
5974 * @author Jules Pietri <jules@heahprod.com>
5975 */
5976final class ChoiceAttr extends AbstractStaticOption
5977{
5978}
5979
5980------------------------------------------------------------------------------------------------------------------------
5981 ./ChoiceList/Factory/Cache/ChoiceFilter.php
5982------------------------------------------------------------------------------------------------------------------------
5983<?php
5984
5985/*
5986 * This file is part of the Symfony package.
5987 *
5988 * (c) Fabien Potencier <fabien@symfony.com>
5989 *
5990 * For the full copyright and license information, please view the LICENSE
5991 * file that was distributed with this source code.
5992 */
5993
5994namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
5995
5996use Symfony\Component\Form\FormTypeExtensionInterface;
5997use Symfony\Component\Form\FormTypeInterface;
5998
5999/**
6000 * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
6001 * which configures a "choice_filter" option.
6002 *
6003 * @internal
6004 *
6005 * @author Jules Pietri <jules@heahprod.com>
6006 */
6007final class ChoiceFilter extends AbstractStaticOption
6008{
6009}
6010
6011------------------------------------------------------------------------------------------------------------------------
6012 ./ChoiceList/Factory/Cache/PreferredChoice.php
6013------------------------------------------------------------------------------------------------------------------------
6014<?php
6015
6016/*
6017 * This file is part of the Symfony package.
6018 *
6019 * (c) Fabien Potencier <fabien@symfony.com>
6020 *
6021 * For the full copyright and license information, please view the LICENSE
6022 * file that was distributed with this source code.
6023 */
6024
6025namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
6026
6027use Symfony\Component\Form\FormTypeExtensionInterface;
6028use Symfony\Component\Form\FormTypeInterface;
6029
6030/**
6031 * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
6032 * which configures a "preferred_choices" option.
6033 *
6034 * @internal
6035 *
6036 * @author Jules Pietri <jules@heahprod.com>
6037 */
6038final class PreferredChoice extends AbstractStaticOption
6039{
6040}
6041
6042------------------------------------------------------------------------------------------------------------------------
6043 ./ChoiceList/Factory/Cache/ChoiceFieldName.php
6044------------------------------------------------------------------------------------------------------------------------
6045<?php
6046
6047/*
6048 * This file is part of the Symfony package.
6049 *
6050 * (c) Fabien Potencier <fabien@symfony.com>
6051 *
6052 * For the full copyright and license information, please view the LICENSE
6053 * file that was distributed with this source code.
6054 */
6055
6056namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
6057
6058use Symfony\Component\Form\FormTypeExtensionInterface;
6059use Symfony\Component\Form\FormTypeInterface;
6060
6061/**
6062 * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
6063 * which configures a "choice_name" callback.
6064 *
6065 * @internal
6066 *
6067 * @author Jules Pietri <jules@heahprod.com>
6068 */
6069final class ChoiceFieldName extends AbstractStaticOption
6070{
6071}
6072
6073------------------------------------------------------------------------------------------------------------------------
6074 ./ChoiceList/Factory/Cache/ChoiceLoader.php
6075------------------------------------------------------------------------------------------------------------------------
6076<?php
6077
6078/*
6079 * This file is part of the Symfony package.
6080 *
6081 * (c) Fabien Potencier <fabien@symfony.com>
6082 *
6083 * For the full copyright and license information, please view the LICENSE
6084 * file that was distributed with this source code.
6085 */
6086
6087namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
6088
6089use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
6090use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
6091use Symfony\Component\Form\FormTypeExtensionInterface;
6092use Symfony\Component\Form\FormTypeInterface;
6093
6094/**
6095 * A cacheable wrapper for {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
6096 * which configures a "choice_loader" option.
6097 *
6098 * @internal
6099 *
6100 * @author Jules Pietri <jules@heahprod.com>
6101 */
6102final class ChoiceLoader extends AbstractStaticOption implements ChoiceLoaderInterface
6103{
6104 public function loadChoiceList(?callable $value = null): ChoiceListInterface
6105 {
6106 return $this->getOption()->loadChoiceList($value);
6107 }
6108
6109 public function loadChoicesForValues(array $values, ?callable $value = null): array
6110 {
6111 return $this->getOption()->loadChoicesForValues($values, $value);
6112 }
6113
6114 public function loadValuesForChoices(array $choices, ?callable $value = null): array
6115 {
6116 return $this->getOption()->loadValuesForChoices($choices, $value);
6117 }
6118}
6119
6120------------------------------------------------------------------------------------------------------------------------
6121 ./ChoiceList/Factory/Cache/AbstractStaticOption.php
6122------------------------------------------------------------------------------------------------------------------------
6123<?php
6124
6125/*
6126 * This file is part of the Symfony package.
6127 *
6128 * (c) Fabien Potencier <fabien@symfony.com>
6129 *
6130 * For the full copyright and license information, please view the LICENSE
6131 * file that was distributed with this source code.
6132 */
6133
6134namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
6135
6136use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
6137use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
6138use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
6139use Symfony\Component\Form\FormTypeExtensionInterface;
6140use Symfony\Component\Form\FormTypeInterface;
6141
6142/**
6143 * A template decorator for static {@see ChoiceType} options.
6144 *
6145 * Used as fly weight for {@see CachingFactoryDecorator}.
6146 *
6147 * @internal
6148 *
6149 * @author Jules Pietri <jules@heahprod.com>
6150 */
6151abstract class AbstractStaticOption
6152{
6153 private static array $options = [];
6154
6155 private bool|string|array|\Closure|ChoiceLoaderInterface $option;
6156
6157 /**
6158 * @param mixed $option Any pseudo callable, array, string or bool to define a choice list option
6159 * @param mixed $vary Dynamic data used to compute a unique hash when caching the option
6160 */
6161 final public function __construct(FormTypeInterface|FormTypeExtensionInterface $formType, mixed $option, mixed $vary = null)
6162 {
6163 $hash = CachingFactoryDecorator::generateHash([static::class, $formType, $vary]);
6164
6165 $this->option = self::$options[$hash] ??= $option instanceof \Closure || \is_string($option) || \is_bool($option) || $option instanceof ChoiceLoaderInterface || !\is_callable($option) ? $option : $option(...);
6166 }
6167
6168 final public function getOption(): mixed
6169 {
6170 return $this->option;
6171 }
6172
6173 final public static function reset(): void
6174 {
6175 self::$options = [];
6176 }
6177}
6178
6179------------------------------------------------------------------------------------------------------------------------
6180 ./ChoiceList/Factory/Cache/ChoiceValue.php
6181------------------------------------------------------------------------------------------------------------------------
6182<?php
6183
6184/*
6185 * This file is part of the Symfony package.
6186 *
6187 * (c) Fabien Potencier <fabien@symfony.com>
6188 *
6189 * For the full copyright and license information, please view the LICENSE
6190 * file that was distributed with this source code.
6191 */
6192
6193namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
6194
6195use Symfony\Component\Form\FormTypeExtensionInterface;
6196use Symfony\Component\Form\FormTypeInterface;
6197
6198/**
6199 * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
6200 * which configures a "choice_value" callback.
6201 *
6202 * @internal
6203 *
6204 * @author Jules Pietri <jules@heahprod.com>
6205 */
6206final class ChoiceValue extends AbstractStaticOption
6207{
6208}
6209
6210------------------------------------------------------------------------------------------------------------------------
6211 ./ChoiceList/Factory/Cache/ChoiceTranslationParameters.php
6212------------------------------------------------------------------------------------------------------------------------
6213<?php
6214
6215/*
6216 * This file is part of the Symfony package.
6217 *
6218 * (c) Fabien Potencier <fabien@symfony.com>
6219 *
6220 * For the full copyright and license information, please view the LICENSE
6221 * file that was distributed with this source code.
6222 */
6223
6224namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
6225
6226use Symfony\Component\Form\FormTypeExtensionInterface;
6227use Symfony\Component\Form\FormTypeInterface;
6228
6229/**
6230 * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
6231 * which configures a "choice_translation_parameters" option.
6232 *
6233 * @internal
6234 *
6235 * @author Vincent Langlet <vincentlanglet@users.noreply.github.com>
6236 */
6237final class ChoiceTranslationParameters extends AbstractStaticOption
6238{
6239}
6240
6241------------------------------------------------------------------------------------------------------------------------
6242 ./ChoiceList/Factory/DefaultChoiceListFactory.php
6243------------------------------------------------------------------------------------------------------------------------
6244<?php
6245
6246/*
6247 * This file is part of the Symfony package.
6248 *
6249 * (c) Fabien Potencier <fabien@symfony.com>
6250 *
6251 * For the full copyright and license information, please view the LICENSE
6252 * file that was distributed with this source code.
6253 */
6254
6255namespace Symfony\Component\Form\ChoiceList\Factory;
6256
6257use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
6258use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
6259use Symfony\Component\Form\ChoiceList\LazyChoiceList;
6260use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
6261use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
6262use Symfony\Component\Form\ChoiceList\Loader\FilterChoiceLoaderDecorator;
6263use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
6264use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
6265use Symfony\Component\Form\ChoiceList\View\ChoiceView;
6266use Symfony\Contracts\Translation\TranslatableInterface;
6267
6268/**
6269 * Default implementation of {@link ChoiceListFactoryInterface}.
6270 *
6271 * @author Bernhard Schussek <bschussek@gmail.com>
6272 * @author Jules Pietri <jules@heahprod.com>
6273 */
6274class DefaultChoiceListFactory implements ChoiceListFactoryInterface
6275{
6276 public function createListFromChoices(iterable $choices, ?callable $value = null, ?callable $filter = null): ChoiceListInterface
6277 {
6278 if ($filter) {
6279 // filter the choice list lazily
6280 return $this->createListFromLoader(new FilterChoiceLoaderDecorator(
6281 new CallbackChoiceLoader(static fn () => $choices),
6282 $filter
6283 ), $value);
6284 }
6285
6286 return new ArrayChoiceList($choices, $value);
6287 }
6288
6289 public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $value = null, ?callable $filter = null): ChoiceListInterface
6290 {
6291 if ($filter) {
6292 $loader = new FilterChoiceLoaderDecorator($loader, $filter);
6293 }
6294
6295 return new LazyChoiceList($loader, $value);
6296 }
6297
6298 public function createView(ChoiceListInterface $list, array|callable|null $preferredChoices = null, callable|false|null $label = null, ?callable $index = null, ?callable $groupBy = null, array|callable|null $attr = null, array|callable $labelTranslationParameters = [], bool $duplicatePreferredChoices = true): ChoiceListView
6299 {
6300 $preferredViews = [];
6301 $preferredViewsOrder = [];
6302 $otherViews = [];
6303 $choices = $list->getChoices();
6304 $keys = $list->getOriginalKeys();
6305
6306 if (!\is_callable($preferredChoices)) {
6307 if (!$preferredChoices) {
6308 $preferredChoices = null;
6309 } else {
6310 // make sure we have keys that reflect order
6311 $preferredChoices = array_values($preferredChoices);
6312 $preferredChoices = static fn ($choice) => array_search($choice, $preferredChoices, true);
6313 }
6314 }
6315
6316 // The names are generated from an incrementing integer by default
6317 $index ??= 0;
6318
6319 // If $groupBy is a callable returning a string
6320 // choices are added to the group with the name returned by the callable.
6321 // If $groupBy is a callable returning an array
6322 // choices are added to the groups with names returned by the callable
6323 // If the callable returns null, the choice is not added to any group
6324 if (\is_callable($groupBy)) {
6325 foreach ($choices as $value => $choice) {
6326 self::addChoiceViewsGroupedByCallable(
6327 $groupBy,
6328 $choice,
6329 $value,
6330 $label,
6331 $keys,
6332 $index,
6333 $attr,
6334 $labelTranslationParameters,
6335 $preferredChoices,
6336 $preferredViews,
6337 $preferredViewsOrder,
6338 $otherViews,
6339 $duplicatePreferredChoices,
6340 );
6341 }
6342
6343 // Remove empty group views that may have been created by
6344 // addChoiceViewsGroupedByCallable()
6345 foreach ($preferredViews as $key => $view) {
6346 if ($view instanceof ChoiceGroupView && 0 === \count($view->choices)) {
6347 unset($preferredViews[$key]);
6348 }
6349 }
6350
6351 foreach ($otherViews as $key => $view) {
6352 if ($view instanceof ChoiceGroupView && 0 === \count($view->choices)) {
6353 unset($otherViews[$key]);
6354 }
6355 }
6356
6357 foreach ($preferredViewsOrder as $key => $groupViewsOrder) {
6358 if ($groupViewsOrder) {
6359 $preferredViewsOrder[$key] = min($groupViewsOrder);
6360 } else {
6361 unset($preferredViewsOrder[$key]);
6362 }
6363 }
6364 } else {
6365 // Otherwise use the original structure of the choices
6366 self::addChoiceViewsFromStructuredValues(
6367 $list->getStructuredValues(),
6368 $label,
6369 $choices,
6370 $keys,
6371 $index,
6372 $attr,
6373 $labelTranslationParameters,
6374 $preferredChoices,
6375 $preferredViews,
6376 $preferredViewsOrder,
6377 $otherViews,
6378 $duplicatePreferredChoices,
6379 );
6380 }
6381
6382 uksort($preferredViews, static fn ($a, $b) => isset($preferredViewsOrder[$a], $preferredViewsOrder[$b]) ? $preferredViewsOrder[$a] <=> $preferredViewsOrder[$b] : 0);
6383
6384 return new ChoiceListView($otherViews, $preferredViews);
6385 }
6386
6387 private static function addChoiceView($choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void
6388 {
6389 // $value may be an integer or a string, since it's stored in the array
6390 // keys. We want to guarantee it's a string though.
6391 $key = $keys[$value];
6392 $nextIndex = \is_int($index) ? $index++ : $index($choice, $key, $value);
6393
6394 // BC normalize label to accept a false value
6395 if (null === $label) {
6396 // If the labels are null, use the original choice key by default
6397 $label = (string) $key;
6398 } elseif (false !== $label) {
6399 // If "choice_label" is set to false and "expanded" is true, the value false
6400 // should be passed on to the "label" option of the checkboxes/radio buttons
6401 $dynamicLabel = $label($choice, $key, $value);
6402
6403 if (false === $dynamicLabel) {
6404 $label = false;
6405 } elseif ($dynamicLabel instanceof TranslatableInterface) {
6406 $label = $dynamicLabel;
6407 } else {
6408 $label = (string) $dynamicLabel;
6409 }
6410 }
6411
6412 $view = new ChoiceView(
6413 $choice,
6414 $value,
6415 $label,
6416 // The attributes may be a callable or a mapping from choice indices
6417 // to nested arrays
6418 \is_callable($attr) ? $attr($choice, $key, $value) : ($attr[$key] ?? []),
6419 // The label translation parameters may be a callable or a mapping from choice indices
6420 // to nested arrays
6421 \is_callable($labelTranslationParameters) ? $labelTranslationParameters($choice, $key, $value) : ($labelTranslationParameters[$key] ?? [])
6422 );
6423
6424 // $isPreferred may be null if no choices are preferred
6425 if (null !== $isPreferred && false !== $preferredKey = $isPreferred($choice, $key, $value)) {
6426 $preferredViews[$nextIndex] = $view;
6427 $preferredViewsOrder[$nextIndex] = $preferredKey;
6428
6429 if ($duplicatePreferredChoices) {
6430 $otherViews[$nextIndex] = $view;
6431 }
6432 } else {
6433 $otherViews[$nextIndex] = $view;
6434 }
6435 }
6436
6437 private static function addChoiceViewsFromStructuredValues(array $values, $label, array $choices, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void
6438 {
6439 foreach ($values as $key => $value) {
6440 if (null === $value) {
6441 continue;
6442 }
6443
6444 // Add the contents of groups to new ChoiceGroupView instances
6445 if (\is_array($value)) {
6446 $preferredViewsForGroup = [];
6447 $otherViewsForGroup = [];
6448
6449 self::addChoiceViewsFromStructuredValues(
6450 $value,
6451 $label,
6452 $choices,
6453 $keys,
6454 $index,
6455 $attr,
6456 $labelTranslationParameters,
6457 $isPreferred,
6458 $preferredViewsForGroup,
6459 $preferredViewsOrder,
6460 $otherViewsForGroup,
6461 $duplicatePreferredChoices,
6462 );
6463
6464 if (\count($preferredViewsForGroup) > 0) {
6465 $preferredViews[$key] = new ChoiceGroupView($key, $preferredViewsForGroup);
6466 }
6467
6468 if (\count($otherViewsForGroup) > 0) {
6469 $otherViews[$key] = new ChoiceGroupView($key, $otherViewsForGroup);
6470 }
6471
6472 continue;
6473 }
6474
6475 // Add ungrouped items directly
6476 self::addChoiceView(
6477 $choices[$value],
6478 $value,
6479 $label,
6480 $keys,
6481 $index,
6482 $attr,
6483 $labelTranslationParameters,
6484 $isPreferred,
6485 $preferredViews,
6486 $preferredViewsOrder,
6487 $otherViews,
6488 $duplicatePreferredChoices,
6489 );
6490 }
6491 }
6492
6493 private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void
6494 {
6495 $groupLabels = $groupBy($choice, $keys[$value], $value);
6496
6497 if (null === $groupLabels) {
6498 // If the callable returns null, don't group the choice
6499 self::addChoiceView(
6500 $choice,
6501 $value,
6502 $label,
6503 $keys,
6504 $index,
6505 $attr,
6506 $labelTranslationParameters,
6507 $isPreferred,
6508 $preferredViews,
6509 $preferredViewsOrder,
6510 $otherViews,
6511 $duplicatePreferredChoices,
6512 );
6513
6514 return;
6515 }
6516
6517 $groupLabels = \is_array($groupLabels) ? array_map('strval', $groupLabels) : [(string) $groupLabels];
6518
6519 foreach ($groupLabels as $groupLabel) {
6520 // Initialize the group views if necessary. Unnecessarily built group
6521 // views will be cleaned up at the end of createView()
6522 if (!isset($preferredViews[$groupLabel])) {
6523 $preferredViews[$groupLabel] = new ChoiceGroupView($groupLabel);
6524 $otherViews[$groupLabel] = new ChoiceGroupView($groupLabel);
6525 }
6526 if (!isset($preferredViewsOrder[$groupLabel])) {
6527 $preferredViewsOrder[$groupLabel] = [];
6528 }
6529
6530 self::addChoiceView(
6531 $choice,
6532 $value,
6533 $label,
6534 $keys,
6535 $index,
6536 $attr,
6537 $labelTranslationParameters,
6538 $isPreferred,
6539 $preferredViews[$groupLabel]->choices,
6540 $preferredViewsOrder[$groupLabel],
6541 $otherViews[$groupLabel]->choices,
6542 $duplicatePreferredChoices,
6543 );
6544 }
6545 }
6546}
6547
6548------------------------------------------------------------------------------------------------------------------------
6549 ./ChoiceList/Factory/ChoiceListFactoryInterface.php
6550------------------------------------------------------------------------------------------------------------------------
6551<?php
6552
6553/*
6554 * This file is part of the Symfony package.
6555 *
6556 * (c) Fabien Potencier <fabien@symfony.com>
6557 *
6558 * For the full copyright and license information, please view the LICENSE
6559 * file that was distributed with this source code.
6560 */
6561
6562namespace Symfony\Component\Form\ChoiceList\Factory;
6563
6564use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
6565use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
6566use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
6567
6568/**
6569 * Creates {@link ChoiceListInterface} instances.
6570 *
6571 * @author Bernhard Schussek <bschussek@gmail.com>
6572 */
6573interface ChoiceListFactoryInterface
6574{
6575 /**
6576 * Creates a choice list for the given choices.
6577 *
6578 * The choices should be passed in the values of the choices array.
6579 *
6580 * Optionally, a callable can be passed for generating the choice values.
6581 * The callable receives the choice as only argument.
6582 * Null may be passed when the choice list contains the empty value.
6583 *
6584 * @param callable|null $filter The callable filtering the choices
6585 */
6586 public function createListFromChoices(iterable $choices, ?callable $value = null, ?callable $filter = null): ChoiceListInterface;
6587
6588 /**
6589 * Creates a choice list that is loaded with the given loader.
6590 *
6591 * Optionally, a callable can be passed for generating the choice values.
6592 * The callable receives the choice as only argument.
6593 * Null may be passed when the choice list contains the empty value.
6594 *
6595 * @param callable|null $filter The callable filtering the choices
6596 */
6597 public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $value = null, ?callable $filter = null): ChoiceListInterface;
6598
6599 /**
6600 * Creates a view for the given choice list.
6601 *
6602 * Callables may be passed for all optional arguments. The callables receive
6603 * the choice as first and the array key as the second argument.
6604 *
6605 * * The callable for the label and the name should return the generated
6606 * label/choice name.
6607 * * The callable for the preferred choices should return true or false,
6608 * depending on whether the choice should be preferred or not.
6609 * * The callable for the grouping should return the group name or null if
6610 * a choice should not be grouped.
6611 * * The callable for the attributes should return an array of HTML
6612 * attributes that will be inserted in the tag of the choice.
6613 *
6614 * If no callable is passed, the labels will be generated from the choice
6615 * keys. The view indices will be generated using an incrementing integer
6616 * by default.
6617 *
6618 * The preferred choices can also be passed as array. Each choice that is
6619 * contained in that array will be marked as preferred.
6620 *
6621 * The attributes can be passed as multi-dimensional array. The keys should
6622 * match the keys of the choices. The values should be arrays of HTML
6623 * attributes that should be added to the respective choice.
6624 *
6625 * @param array|callable|null $preferredChoices The preferred choices
6626 * @param callable|false|null $label The callable generating the choice labels;
6627 * pass false to discard the label
6628 * @param array|callable|null $attr The callable generating the HTML attributes
6629 * @param array|callable $labelTranslationParameters The parameters used to translate the choice labels
6630 * @param bool $duplicatePreferredChoices Whether the preferred choices should be duplicated
6631 * on top of the list and in their original position
6632 * or only in the top of the list
6633 */
6634 public function createView(ChoiceListInterface $list, array|callable|null $preferredChoices = null, callable|false|null $label = null, ?callable $index = null, ?callable $groupBy = null, array|callable|null $attr = null, array|callable $labelTranslationParameters = [], bool $duplicatePreferredChoices = true): ChoiceListView;
6635}
6636
6637------------------------------------------------------------------------------------------------------------------------
6638 ./ChoiceList/Factory/PropertyAccessDecorator.php
6639------------------------------------------------------------------------------------------------------------------------
6640<?php
6641
6642/*
6643 * This file is part of the Symfony package.
6644 *
6645 * (c) Fabien Potencier <fabien@symfony.com>
6646 *
6647 * For the full copyright and license information, please view the LICENSE
6648 * file that was distributed with this source code.
6649 */
6650
6651namespace Symfony\Component\Form\ChoiceList\Factory;
6652
6653use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
6654use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
6655use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
6656use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
6657use Symfony\Component\PropertyAccess\PropertyAccess;
6658use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
6659use Symfony\Component\PropertyAccess\PropertyPath;
6660use Symfony\Component\PropertyAccess\PropertyPathInterface;
6661
6662/**
6663 * Adds property path support to a choice list factory.
6664 *
6665 * Pass the decorated factory to the constructor:
6666 *
6667 * $decorator = new PropertyAccessDecorator($factory);
6668 *
6669 * You can now pass property paths for generating choice values, labels, view
6670 * indices, HTML attributes and for determining the preferred choices and the
6671 * choice groups:
6672 *
6673 * // extract values from the $value property
6674 * $list = $createListFromChoices($objects, 'value');
6675 *
6676 * @author Bernhard Schussek <bschussek@gmail.com>
6677 */
6678class PropertyAccessDecorator implements ChoiceListFactoryInterface
6679{
6680 private PropertyAccessorInterface $propertyAccessor;
6681
6682 public function __construct(
6683 private ChoiceListFactoryInterface $decoratedFactory,
6684 ?PropertyAccessorInterface $propertyAccessor = null,
6685 ) {
6686 $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
6687 }
6688
6689 /**
6690 * Returns the decorated factory.
6691 */
6692 public function getDecoratedFactory(): ChoiceListFactoryInterface
6693 {
6694 return $this->decoratedFactory;
6695 }
6696
6697 public function createListFromChoices(iterable $choices, mixed $value = null, mixed $filter = null): ChoiceListInterface
6698 {
6699 if (\is_string($value)) {
6700 $value = new PropertyPath($value);
6701 }
6702
6703 if ($value instanceof PropertyPathInterface) {
6704 $accessor = $this->propertyAccessor;
6705 // The callable may be invoked with a non-object/array value
6706 // when such values are passed to
6707 // ChoiceListInterface::getValuesForChoices(). Handle this case
6708 // so that the call to getValue() doesn't break.
6709 $value = static fn ($choice) => \is_object($choice) || \is_array($choice) ? $accessor->getValue($choice, $value) : null;
6710 }
6711
6712 if (\is_string($filter)) {
6713 $filter = new PropertyPath($filter);
6714 }
6715
6716 if ($filter instanceof PropertyPath) {
6717 $accessor = $this->propertyAccessor;
6718 $filter = static fn ($choice) => (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter);
6719 }
6720
6721 return $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
6722 }
6723
6724 public function createListFromLoader(ChoiceLoaderInterface $loader, mixed $value = null, mixed $filter = null): ChoiceListInterface
6725 {
6726 if (\is_string($value)) {
6727 $value = new PropertyPath($value);
6728 }
6729
6730 if ($value instanceof PropertyPathInterface) {
6731 $accessor = $this->propertyAccessor;
6732 // The callable may be invoked with a non-object/array value
6733 // when such values are passed to
6734 // ChoiceListInterface::getValuesForChoices(). Handle this case
6735 // so that the call to getValue() doesn't break.
6736 $value = static fn ($choice) => \is_object($choice) || \is_array($choice) ? $accessor->getValue($choice, $value) : null;
6737 }
6738
6739 if (\is_string($filter)) {
6740 $filter = new PropertyPath($filter);
6741 }
6742
6743 if ($filter instanceof PropertyPath) {
6744 $accessor = $this->propertyAccessor;
6745 $filter = static fn ($choice) => (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter);
6746 }
6747
6748 return $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
6749 }
6750
6751 public function createView(ChoiceListInterface $list, mixed $preferredChoices = null, mixed $label = null, mixed $index = null, mixed $groupBy = null, mixed $attr = null, mixed $labelTranslationParameters = [], bool $duplicatePreferredChoices = true): ChoiceListView
6752 {
6753 $accessor = $this->propertyAccessor;
6754
6755 if (\is_string($label)) {
6756 $label = new PropertyPath($label);
6757 }
6758
6759 if ($label instanceof PropertyPathInterface) {
6760 $label = static fn ($choice) => $accessor->getValue($choice, $label);
6761 }
6762
6763 if (\is_string($preferredChoices)) {
6764 $preferredChoices = new PropertyPath($preferredChoices);
6765 }
6766
6767 if ($preferredChoices instanceof PropertyPathInterface) {
6768 $preferredChoices = static function ($choice) use ($accessor, $preferredChoices) {
6769 try {
6770 return $accessor->getValue($choice, $preferredChoices);
6771 } catch (UnexpectedTypeException) {
6772 // Assume not preferred if not readable
6773 return false;
6774 }
6775 };
6776 }
6777
6778 if (\is_string($index)) {
6779 $index = new PropertyPath($index);
6780 }
6781
6782 if ($index instanceof PropertyPathInterface) {
6783 $index = static fn ($choice) => $accessor->getValue($choice, $index);
6784 }
6785
6786 if (\is_string($groupBy)) {
6787 $groupBy = new PropertyPath($groupBy);
6788 }
6789
6790 if ($groupBy instanceof PropertyPathInterface) {
6791 $groupBy = static function ($choice) use ($accessor, $groupBy) {
6792 try {
6793 return $accessor->getValue($choice, $groupBy);
6794 } catch (UnexpectedTypeException) {
6795 // Don't group if path is not readable
6796 return null;
6797 }
6798 };
6799 }
6800
6801 if (\is_string($attr)) {
6802 $attr = new PropertyPath($attr);
6803 }
6804
6805 if ($attr instanceof PropertyPathInterface) {
6806 $attr = static fn ($choice) => $accessor->getValue($choice, $attr);
6807 }
6808
6809 if (\is_string($labelTranslationParameters)) {
6810 $labelTranslationParameters = new PropertyPath($labelTranslationParameters);
6811 }
6812
6813 if ($labelTranslationParameters instanceof PropertyPath) {
6814 $labelTranslationParameters = static fn ($choice) => $accessor->getValue($choice, $labelTranslationParameters);
6815 }
6816
6817 return $this->decoratedFactory->createView(
6818 $list,
6819 $preferredChoices,
6820 $label,
6821 $index,
6822 $groupBy,
6823 $attr,
6824 $labelTranslationParameters,
6825 $duplicatePreferredChoices,
6826 );
6827 }
6828}
6829
6830------------------------------------------------------------------------------------------------------------------------
6831 ./ChoiceList/View/ChoiceView.php
6832------------------------------------------------------------------------------------------------------------------------
6833<?php
6834
6835/*
6836 * This file is part of the Symfony package.
6837 *
6838 * (c) Fabien Potencier <fabien@symfony.com>
6839 *
6840 * For the full copyright and license information, please view the LICENSE
6841 * file that was distributed with this source code.
6842 */
6843
6844namespace Symfony\Component\Form\ChoiceList\View;
6845
6846use Symfony\Contracts\Translation\TranslatableInterface;
6847
6848/**
6849 * Represents a choice in templates.
6850 *
6851 * @author Bernhard Schussek <bschussek@gmail.com>
6852 */
6853class ChoiceView
6854{
6855 /**
6856 * Creates a new choice view.
6857 *
6858 * @param mixed $data The original choice
6859 * @param string $value The view representation of the choice
6860 * @param string|TranslatableInterface|false $label The label displayed to humans; pass false to discard the label
6861 * @param array $attr Additional attributes for the HTML tag
6862 * @param array $labelTranslationParameters Additional parameters used to translate the label
6863 */
6864 public function __construct(
6865 public mixed $data,
6866 public string $value,
6867 public string|TranslatableInterface|false $label,
6868 public array $attr = [],
6869 public array $labelTranslationParameters = [],
6870 ) {
6871 }
6872}
6873
6874------------------------------------------------------------------------------------------------------------------------
6875 ./ChoiceList/View/ChoiceListView.php
6876------------------------------------------------------------------------------------------------------------------------
6877<?php
6878
6879/*
6880 * This file is part of the Symfony package.
6881 *
6882 * (c) Fabien Potencier <fabien@symfony.com>
6883 *
6884 * For the full copyright and license information, please view the LICENSE
6885 * file that was distributed with this source code.
6886 */
6887
6888namespace Symfony\Component\Form\ChoiceList\View;
6889
6890/**
6891 * Represents a choice list in templates.
6892 *
6893 * A choice list contains choices and optionally preferred choices which are
6894 * displayed in the very beginning of the list. Both choices and preferred
6895 * choices may be grouped in {@link ChoiceGroupView} instances.
6896 *
6897 * @author Bernhard Schussek <bschussek@gmail.com>
6898 */
6899class ChoiceListView
6900{
6901 /**
6902 * Creates a new choice list view.
6903 *
6904 * @param array<ChoiceGroupView|ChoiceView> $choices The choice views
6905 * @param array<ChoiceGroupView|ChoiceView> $preferredChoices the preferred choice views
6906 */
6907 public function __construct(
6908 public array $choices = [],
6909 public array $preferredChoices = [],
6910 ) {
6911 }
6912
6913 /**
6914 * Returns whether a placeholder is in the choices.
6915 *
6916 * A placeholder must be the first child element, not be in a group and have an empty value.
6917 */
6918 public function hasPlaceholder(): bool
6919 {
6920 if ($this->preferredChoices) {
6921 $firstChoice = reset($this->preferredChoices);
6922
6923 return $firstChoice instanceof ChoiceView && '' === $firstChoice->value;
6924 }
6925
6926 $firstChoice = reset($this->choices);
6927
6928 return $firstChoice instanceof ChoiceView && '' === $firstChoice->value;
6929 }
6930}
6931
6932------------------------------------------------------------------------------------------------------------------------
6933 ./ChoiceList/View/ChoiceGroupView.php
6934------------------------------------------------------------------------------------------------------------------------
6935<?php
6936
6937/*
6938 * This file is part of the Symfony package.
6939 *
6940 * (c) Fabien Potencier <fabien@symfony.com>
6941 *
6942 * For the full copyright and license information, please view the LICENSE
6943 * file that was distributed with this source code.
6944 */
6945
6946namespace Symfony\Component\Form\ChoiceList\View;
6947
6948/**
6949 * Represents a group of choices in templates.
6950 *
6951 * @author Bernhard Schussek <bschussek@gmail.com>
6952 *
6953 * @implements \IteratorAggregate<array-key, ChoiceGroupView|ChoiceView>
6954 */
6955class ChoiceGroupView implements \IteratorAggregate
6956{
6957 /**
6958 * Creates a new choice group view.
6959 *
6960 * @param array<ChoiceGroupView|ChoiceView> $choices the choice views in the group
6961 */
6962 public function __construct(
6963 public string $label,
6964 public array $choices = [],
6965 ) {
6966 }
6967
6968 /**
6969 * @return \Traversable<array-key, ChoiceGroupView|ChoiceView>
6970 */
6971 public function getIterator(): \Traversable
6972 {
6973 return new \ArrayIterator($this->choices);
6974 }
6975}
6976
6977------------------------------------------------------------------------------------------------------------------------
6978 ./RequestHandlerInterface.php
6979------------------------------------------------------------------------------------------------------------------------
6980<?php
6981
6982/*
6983 * This file is part of the Symfony package.
6984 *
6985 * (c) Fabien Potencier <fabien@symfony.com>
6986 *
6987 * For the full copyright and license information, please view the LICENSE
6988 * file that was distributed with this source code.
6989 */
6990
6991namespace Symfony\Component\Form;
6992
6993/**
6994 * Submits forms if they were submitted.
6995 *
6996 * @author Bernhard Schussek <bschussek@gmail.com>
6997 */
6998interface RequestHandlerInterface
6999{
7000 /**
7001 * Submits a form if it was submitted.
7002 */
7003 public function handleRequest(FormInterface $form, mixed $request = null): void;
7004
7005 /**
7006 * Returns true if the given data is a file upload.
7007 */
7008 public function isFileUpload(mixed $data): bool;
7009}
7010
7011------------------------------------------------------------------------------------------------------------------------
7012 ./Guess/TypeGuess.php
7013------------------------------------------------------------------------------------------------------------------------
7014<?php
7015
7016/*
7017 * This file is part of the Symfony package.
7018 *
7019 * (c) Fabien Potencier <fabien@symfony.com>
7020 *
7021 * For the full copyright and license information, please view the LICENSE
7022 * file that was distributed with this source code.
7023 */
7024
7025namespace Symfony\Component\Form\Guess;
7026
7027/**
7028 * Contains a guessed class name and a list of options for creating an instance
7029 * of that class.
7030 *
7031 * @author Bernhard Schussek <bschussek@gmail.com>
7032 */
7033class TypeGuess extends Guess
7034{
7035 /**
7036 * @param string $type The guessed field type
7037 * @param array $options The options for creating instances of the
7038 * guessed class
7039 * @param int $confidence The confidence that the guessed class name
7040 * is correct
7041 */
7042 public function __construct(
7043 private string $type,
7044 private array $options,
7045 int $confidence,
7046 ) {
7047 parent::__construct($confidence);
7048 }
7049
7050 /**
7051 * Returns the guessed field type.
7052 */
7053 public function getType(): string
7054 {
7055 return $this->type;
7056 }
7057
7058 /**
7059 * Returns the guessed options for creating instances of the guessed type.
7060 */
7061 public function getOptions(): array
7062 {
7063 return $this->options;
7064 }
7065}
7066
7067------------------------------------------------------------------------------------------------------------------------
7068 ./Guess/Guess.php
7069------------------------------------------------------------------------------------------------------------------------
7070<?php
7071
7072/*
7073 * This file is part of the Symfony package.
7074 *
7075 * (c) Fabien Potencier <fabien@symfony.com>
7076 *
7077 * For the full copyright and license information, please view the LICENSE
7078 * file that was distributed with this source code.
7079 */
7080
7081namespace Symfony\Component\Form\Guess;
7082
7083use Symfony\Component\Form\Exception\InvalidArgumentException;
7084
7085/**
7086 * Base class for guesses made by TypeGuesserInterface implementation.
7087 *
7088 * Each instance contains a confidence value about the correctness of the guess.
7089 * Thus an instance with confidence HIGH_CONFIDENCE is more likely to be
7090 * correct than an instance with confidence LOW_CONFIDENCE.
7091 *
7092 * @author Bernhard Schussek <bschussek@gmail.com>
7093 */
7094abstract class Guess
7095{
7096 /**
7097 * Marks an instance with a value that is extremely likely to be correct.
7098 */
7099 public const VERY_HIGH_CONFIDENCE = 3;
7100
7101 /**
7102 * Marks an instance with a value that is very likely to be correct.
7103 */
7104 public const HIGH_CONFIDENCE = 2;
7105
7106 /**
7107 * Marks an instance with a value that is likely to be correct.
7108 */
7109 public const MEDIUM_CONFIDENCE = 1;
7110
7111 /**
7112 * Marks an instance with a value that may be correct.
7113 */
7114 public const LOW_CONFIDENCE = 0;
7115
7116 /**
7117 * The confidence about the correctness of the value.
7118 *
7119 * One of VERY_HIGH_CONFIDENCE, HIGH_CONFIDENCE, MEDIUM_CONFIDENCE
7120 * and LOW_CONFIDENCE.
7121 */
7122 private int $confidence;
7123
7124 /**
7125 * Returns the guess most likely to be correct from a list of guesses.
7126 *
7127 * If there are multiple guesses with the same, highest confidence, the
7128 * returned guess is any of them.
7129 *
7130 * @param static[] $guesses An array of guesses
7131 */
7132 public static function getBestGuess(array $guesses): ?static
7133 {
7134 $result = null;
7135 $maxConfidence = -1;
7136
7137 foreach ($guesses as $guess) {
7138 if ($maxConfidence < $confidence = $guess->getConfidence()) {
7139 $maxConfidence = $confidence;
7140 $result = $guess;
7141 }
7142 }
7143
7144 return $result;
7145 }
7146
7147 /**
7148 * @throws InvalidArgumentException if the given value of confidence is unknown
7149 */
7150 public function __construct(int $confidence)
7151 {
7152 if (self::VERY_HIGH_CONFIDENCE !== $confidence && self::HIGH_CONFIDENCE !== $confidence
7153 && self::MEDIUM_CONFIDENCE !== $confidence && self::LOW_CONFIDENCE !== $confidence) {
7154 throw new InvalidArgumentException('The confidence should be one of the constants defined in Guess.');
7155 }
7156
7157 $this->confidence = $confidence;
7158 }
7159
7160 /**
7161 * Returns the confidence that the guessed value is correct.
7162 *
7163 * @return int One of the constants VERY_HIGH_CONFIDENCE, HIGH_CONFIDENCE,
7164 * MEDIUM_CONFIDENCE and LOW_CONFIDENCE
7165 */
7166 public function getConfidence(): int
7167 {
7168 return $this->confidence;
7169 }
7170}
7171
7172------------------------------------------------------------------------------------------------------------------------
7173 ./Guess/ValueGuess.php
7174------------------------------------------------------------------------------------------------------------------------
7175<?php
7176
7177/*
7178 * This file is part of the Symfony package.
7179 *
7180 * (c) Fabien Potencier <fabien@symfony.com>
7181 *
7182 * For the full copyright and license information, please view the LICENSE
7183 * file that was distributed with this source code.
7184 */
7185
7186namespace Symfony\Component\Form\Guess;
7187
7188/**
7189 * Contains a guessed value.
7190 *
7191 * @author Bernhard Schussek <bschussek@gmail.com>
7192 */
7193class ValueGuess extends Guess
7194{
7195 /**
7196 * @param int $confidence The confidence that the guessed class name is correct
7197 */
7198 public function __construct(
7199 private string|int|bool|null $value,
7200 int $confidence,
7201 ) {
7202 parent::__construct($confidence);
7203 }
7204
7205 /**
7206 * Returns the guessed value.
7207 */
7208 public function getValue(): string|int|bool|null
7209 {
7210 return $this->value;
7211 }
7212}
7213
7214------------------------------------------------------------------------------------------------------------------------
7215 ./FormFactoryBuilder.php
7216------------------------------------------------------------------------------------------------------------------------
7217<?php
7218
7219/*
7220 * This file is part of the Symfony package.
7221 *
7222 * (c) Fabien Potencier <fabien@symfony.com>
7223 *
7224 * For the full copyright and license information, please view the LICENSE
7225 * file that was distributed with this source code.
7226 */
7227
7228namespace Symfony\Component\Form;
7229
7230use Symfony\Component\Form\Extension\Core\CoreExtension;
7231
7232/**
7233 * The default implementation of FormFactoryBuilderInterface.
7234 *
7235 * @author Bernhard Schussek <bschussek@gmail.com>
7236 */
7237class FormFactoryBuilder implements FormFactoryBuilderInterface
7238{
7239 private ResolvedFormTypeFactoryInterface $resolvedTypeFactory;
7240
7241 /**
7242 * @var FormExtensionInterface[]
7243 */
7244 private array $extensions = [];
7245
7246 /**
7247 * @var FormTypeInterface[]
7248 */
7249 private array $types = [];
7250
7251 /**
7252 * @var FormTypeExtensionInterface[][]
7253 */
7254 private array $typeExtensions = [];
7255
7256 /**
7257 * @var FormTypeGuesserInterface[]
7258 */
7259 private array $typeGuessers = [];
7260
7261 public function __construct(
7262 private bool $forceCoreExtension = false,
7263 ) {
7264 }
7265
7266 public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory): static
7267 {
7268 $this->resolvedTypeFactory = $resolvedTypeFactory;
7269
7270 return $this;
7271 }
7272
7273 public function addExtension(FormExtensionInterface $extension): static
7274 {
7275 $this->extensions[] = $extension;
7276
7277 return $this;
7278 }
7279
7280 public function addExtensions(array $extensions): static
7281 {
7282 $this->extensions = array_merge($this->extensions, $extensions);
7283
7284 return $this;
7285 }
7286
7287 public function addType(FormTypeInterface $type): static
7288 {
7289 $this->types[] = $type;
7290
7291 return $this;
7292 }
7293
7294 public function addTypes(array $types): static
7295 {
7296 foreach ($types as $type) {
7297 $this->types[] = $type;
7298 }
7299
7300 return $this;
7301 }
7302
7303 public function addTypeExtension(FormTypeExtensionInterface $typeExtension): static
7304 {
7305 foreach ($typeExtension::getExtendedTypes() as $extendedType) {
7306 $this->typeExtensions[$extendedType][] = $typeExtension;
7307 }
7308
7309 return $this;
7310 }
7311
7312 public function addTypeExtensions(array $typeExtensions): static
7313 {
7314 foreach ($typeExtensions as $typeExtension) {
7315 $this->addTypeExtension($typeExtension);
7316 }
7317
7318 return $this;
7319 }
7320
7321 public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser): static
7322 {
7323 $this->typeGuessers[] = $typeGuesser;
7324
7325 return $this;
7326 }
7327
7328 public function addTypeGuessers(array $typeGuessers): static
7329 {
7330 $this->typeGuessers = array_merge($this->typeGuessers, $typeGuessers);
7331
7332 return $this;
7333 }
7334
7335 public function getFormFactory(): FormFactoryInterface
7336 {
7337 $extensions = $this->extensions;
7338
7339 if ($this->forceCoreExtension) {
7340 $hasCoreExtension = false;
7341
7342 foreach ($extensions as $extension) {
7343 if ($extension instanceof CoreExtension) {
7344 $hasCoreExtension = true;
7345 break;
7346 }
7347 }
7348
7349 if (!$hasCoreExtension) {
7350 array_unshift($extensions, new CoreExtension());
7351 }
7352 }
7353
7354 if (\count($this->types) > 0 || \count($this->typeExtensions) > 0 || \count($this->typeGuessers) > 0) {
7355 if (\count($this->typeGuessers) > 1) {
7356 $typeGuesser = new FormTypeGuesserChain($this->typeGuessers);
7357 } else {
7358 $typeGuesser = $this->typeGuessers[0] ?? null;
7359 }
7360
7361 $extensions[] = new PreloadedExtension($this->types, $this->typeExtensions, $typeGuesser);
7362 }
7363
7364 $registry = new FormRegistry($extensions, $this->resolvedTypeFactory ?? new ResolvedFormTypeFactory());
7365
7366 return new FormFactory($registry);
7367 }
7368}
7369
7370------------------------------------------------------------------------------------------------------------------------
7371 ./AbstractExtension.php
7372------------------------------------------------------------------------------------------------------------------------
7373<?php
7374
7375/*
7376 * This file is part of the Symfony package.
7377 *
7378 * (c) Fabien Potencier <fabien@symfony.com>
7379 *
7380 * For the full copyright and license information, please view the LICENSE
7381 * file that was distributed with this source code.
7382 */
7383
7384namespace Symfony\Component\Form;
7385
7386use Symfony\Component\Form\Exception\InvalidArgumentException;
7387use Symfony\Component\Form\Exception\UnexpectedTypeException;
7388
7389/**
7390 * @author Bernhard Schussek <bschussek@gmail.com>
7391 */
7392abstract class AbstractExtension implements FormExtensionInterface
7393{
7394 /**
7395 * The types provided by this extension.
7396 *
7397 * @var FormTypeInterface[]
7398 */
7399 private array $types;
7400
7401 /**
7402 * The type extensions provided by this extension.
7403 *
7404 * @var FormTypeExtensionInterface[][]
7405 */
7406 private array $typeExtensions;
7407
7408 /**
7409 * The type guesser provided by this extension.
7410 */
7411 private ?FormTypeGuesserInterface $typeGuesser = null;
7412
7413 /**
7414 * Whether the type guesser has been loaded.
7415 */
7416 private bool $typeGuesserLoaded = false;
7417
7418 public function getType(string $name): FormTypeInterface
7419 {
7420 if (!isset($this->types)) {
7421 $this->initTypes();
7422 }
7423
7424 if (!isset($this->types[$name])) {
7425 throw new InvalidArgumentException(\sprintf('The type "%s" cannot be loaded by this extension.', $name));
7426 }
7427
7428 return $this->types[$name];
7429 }
7430
7431 public function hasType(string $name): bool
7432 {
7433 if (!isset($this->types)) {
7434 $this->initTypes();
7435 }
7436
7437 return isset($this->types[$name]);
7438 }
7439
7440 public function getTypeExtensions(string $name): array
7441 {
7442 if (!isset($this->typeExtensions)) {
7443 $this->initTypeExtensions();
7444 }
7445
7446 return $this->typeExtensions[$name]
7447 ?? [];
7448 }
7449
7450 public function hasTypeExtensions(string $name): bool
7451 {
7452 if (!isset($this->typeExtensions)) {
7453 $this->initTypeExtensions();
7454 }
7455
7456 return isset($this->typeExtensions[$name]) && \count($this->typeExtensions[$name]) > 0;
7457 }
7458
7459 public function getTypeGuesser(): ?FormTypeGuesserInterface
7460 {
7461 if (!$this->typeGuesserLoaded) {
7462 $this->initTypeGuesser();
7463 }
7464
7465 return $this->typeGuesser;
7466 }
7467
7468 /**
7469 * Registers the types.
7470 *
7471 * @return FormTypeInterface[]
7472 */
7473 protected function loadTypes(): array
7474 {
7475 return [];
7476 }
7477
7478 /**
7479 * Registers the type extensions.
7480 *
7481 * @return FormTypeExtensionInterface[]
7482 */
7483 protected function loadTypeExtensions(): array
7484 {
7485 return [];
7486 }
7487
7488 /**
7489 * Registers the type guesser.
7490 */
7491 protected function loadTypeGuesser(): ?FormTypeGuesserInterface
7492 {
7493 return null;
7494 }
7495
7496 /**
7497 * Initializes the types.
7498 *
7499 * @throws UnexpectedTypeException if any registered type is not an instance of FormTypeInterface
7500 */
7501 private function initTypes(): void
7502 {
7503 $this->types = [];
7504
7505 foreach ($this->loadTypes() as $type) {
7506 if (!$type instanceof FormTypeInterface) {
7507 throw new UnexpectedTypeException($type, FormTypeInterface::class);
7508 }
7509
7510 $this->types[$type::class] = $type;
7511 }
7512 }
7513
7514 /**
7515 * Initializes the type extensions.
7516 *
7517 * @throws UnexpectedTypeException if any registered type extension is not
7518 * an instance of FormTypeExtensionInterface
7519 */
7520 private function initTypeExtensions(): void
7521 {
7522 $this->typeExtensions = [];
7523
7524 foreach ($this->loadTypeExtensions() as $extension) {
7525 if (!$extension instanceof FormTypeExtensionInterface) {
7526 throw new UnexpectedTypeException($extension, FormTypeExtensionInterface::class);
7527 }
7528
7529 foreach ($extension::getExtendedTypes() as $extendedType) {
7530 $this->typeExtensions[$extendedType][] = $extension;
7531 }
7532 }
7533 }
7534
7535 /**
7536 * Initializes the type guesser.
7537 *
7538 * @throws UnexpectedTypeException if the type guesser is not an instance of FormTypeGuesserInterface
7539 */
7540 private function initTypeGuesser(): void
7541 {
7542 $this->typeGuesserLoaded = true;
7543
7544 $this->typeGuesser = $this->loadTypeGuesser();
7545 if (null !== $this->typeGuesser && !$this->typeGuesser instanceof FormTypeGuesserInterface) {
7546 throw new UnexpectedTypeException($this->typeGuesser, FormTypeGuesserInterface::class);
7547 }
7548 }
7549}
7550
7551------------------------------------------------------------------------------------------------------------------------
7552 ./NativeRequestHandler.php
7553------------------------------------------------------------------------------------------------------------------------
7554<?php
7555
7556/*
7557 * This file is part of the Symfony package.
7558 *
7559 * (c) Fabien Potencier <fabien@symfony.com>
7560 *
7561 * For the full copyright and license information, please view the LICENSE
7562 * file that was distributed with this source code.
7563 */
7564
7565namespace Symfony\Component\Form;
7566
7567use Symfony\Component\Form\Exception\UnexpectedTypeException;
7568use Symfony\Component\Form\Util\FormUtil;
7569use Symfony\Component\Form\Util\ServerParams;
7570
7571/**
7572 * A request handler using PHP super globals $_GET, $_POST and $_SERVER.
7573 *
7574 * @author Bernhard Schussek <bschussek@gmail.com>
7575 */
7576class NativeRequestHandler implements RequestHandlerInterface
7577{
7578 private ServerParams $serverParams;
7579
7580 /**
7581 * The allowed keys of the $_FILES array.
7582 */
7583 private const FILE_KEYS = [
7584 'error',
7585 'full_path',
7586 'name',
7587 'size',
7588 'tmp_name',
7589 'type',
7590 ];
7591
7592 public function __construct(?ServerParams $params = null)
7593 {
7594 $this->serverParams = $params ?? new ServerParams();
7595 }
7596
7597 /**
7598 * @throws UnexpectedTypeException If the $request is not null
7599 */
7600 public function handleRequest(FormInterface $form, mixed $request = null): void
7601 {
7602 if (null !== $request) {
7603 throw new UnexpectedTypeException($request, 'null');
7604 }
7605
7606 $name = $form->getName();
7607 $method = $form->getConfig()->getMethod();
7608
7609 if ($method !== self::getRequestMethod()) {
7610 return;
7611 }
7612
7613 // For request methods that must not have a request body we fetch data
7614 // from the query string. Otherwise we look for data in the request body.
7615 if ('GET' === $method || 'HEAD' === $method || 'TRACE' === $method) {
7616 if ('' === $name) {
7617 $data = $_GET;
7618 } else {
7619 // Don't submit GET requests if the form's name does not exist
7620 // in the request
7621 if (!isset($_GET[$name])) {
7622 return;
7623 }
7624
7625 $data = $_GET[$name];
7626 }
7627 } else {
7628 // Mark the form with an error if the uploaded size was too large
7629 // This is done here and not in FormValidator because $_POST is
7630 // empty when that error occurs. Hence the form is never submitted.
7631 if ($this->serverParams->hasPostMaxSizeBeenExceeded()) {
7632 // Submit the form, but don't clear the default values
7633 $form->submit(null, false);
7634
7635 $form->addError(new FormError(
7636 $form->getConfig()->getOption('upload_max_size_message')(),
7637 null,
7638 ['{{ max }}' => $this->serverParams->getNormalizedIniPostMaxSize()]
7639 ));
7640
7641 return;
7642 }
7643
7644 $fixedFiles = [];
7645 foreach ($_FILES as $fileKey => $file) {
7646 $fixedFiles[$fileKey] = self::stripEmptyFiles(self::fixPhpFilesArray($file));
7647 }
7648
7649 if ('' === $name) {
7650 $params = $_POST;
7651 $files = $fixedFiles;
7652 } elseif (\array_key_exists($name, $_POST) || \array_key_exists($name, $fixedFiles)) {
7653 $default = $form->getConfig()->getCompound() ? [] : null;
7654 $params = \array_key_exists($name, $_POST) ? $_POST[$name] : $default;
7655 $files = \array_key_exists($name, $fixedFiles) ? $fixedFiles[$name] : $default;
7656 } else {
7657 // Don't submit the form if it is not present in the request
7658 return;
7659 }
7660
7661 if (\is_array($params) && \is_array($files)) {
7662 $data = FormUtil::mergeParamsAndFiles($params, $files);
7663 } else {
7664 $data = $params ?: $files;
7665 }
7666 }
7667
7668 // Don't auto-submit the form unless at least one field is present.
7669 if ('' === $name && \count(array_intersect_key($data, $form->all())) <= 0) {
7670 return;
7671 }
7672
7673 if (\is_array($data) && \array_key_exists('_method', $data) && $method === $data['_method'] && !$form->has('_method')) {
7674 unset($data['_method']);
7675 }
7676
7677 $form->submit($data, 'PATCH' !== $method);
7678 }
7679
7680 public function isFileUpload(mixed $data): bool
7681 {
7682 // POST data will always be strings or arrays of strings. Thus, we can be sure
7683 // that the submitted data is a file upload if the "error" value is an integer
7684 // (this value must have been injected by PHP itself).
7685 return \is_array($data) && isset($data['error']) && \is_int($data['error']);
7686 }
7687
7688 public function getUploadFileError(mixed $data): ?int
7689 {
7690 if (!\is_array($data)) {
7691 return null;
7692 }
7693
7694 if (!isset($data['error'])) {
7695 return null;
7696 }
7697
7698 if (!\is_int($data['error'])) {
7699 return null;
7700 }
7701
7702 if (\UPLOAD_ERR_OK === $data['error']) {
7703 return null;
7704 }
7705
7706 return $data['error'];
7707 }
7708
7709 private static function getRequestMethod(): string
7710 {
7711 $method = isset($_SERVER['REQUEST_METHOD'])
7712 ? strtoupper($_SERVER['REQUEST_METHOD'])
7713 : 'GET';
7714
7715 if ('POST' === $method && isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
7716 $method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
7717 }
7718
7719 return $method;
7720 }
7721
7722 /**
7723 * Fixes a malformed PHP $_FILES array.
7724 *
7725 * PHP has a bug that the format of the $_FILES array differs, depending on
7726 * whether the uploaded file fields had normal field names or array-like
7727 * field names ("normal" vs. "parent[child]").
7728 *
7729 * This method fixes the array to look like the "normal" $_FILES array.
7730 *
7731 * It's safe to pass an already converted array, in which case this method
7732 * just returns the original array unmodified.
7733 *
7734 * This method is identical to {@link \Symfony\Component\HttpFoundation\FileBag::fixPhpFilesArray}
7735 * and should be kept as such in order to port fixes quickly and easily.
7736 */
7737 private static function fixPhpFilesArray(mixed $data): mixed
7738 {
7739 if (!\is_array($data)) {
7740 return $data;
7741 }
7742
7743 $keys = array_keys($data + ['full_path' => null]);
7744 sort($keys);
7745
7746 if (self::FILE_KEYS !== $keys || !isset($data['name']) || !\is_array($data['name'])) {
7747 return $data;
7748 }
7749
7750 $files = $data;
7751 foreach (self::FILE_KEYS as $k) {
7752 unset($files[$k]);
7753 }
7754
7755 foreach ($data['name'] as $key => $name) {
7756 $files[$key] = self::fixPhpFilesArray([
7757 'error' => $data['error'][$key],
7758 'name' => $name,
7759 'type' => $data['type'][$key],
7760 'tmp_name' => $data['tmp_name'][$key],
7761 'size' => $data['size'][$key],
7762 ] + (isset($data['full_path'][$key]) ? [
7763 'full_path' => $data['full_path'][$key],
7764 ] : []));
7765 }
7766
7767 return $files;
7768 }
7769
7770 private static function stripEmptyFiles(mixed $data): mixed
7771 {
7772 if (!\is_array($data)) {
7773 return $data;
7774 }
7775
7776 $keys = array_keys($data + ['full_path' => null]);
7777 sort($keys);
7778
7779 if (self::FILE_KEYS === $keys) {
7780 if (\UPLOAD_ERR_NO_FILE === $data['error']) {
7781 return null;
7782 }
7783
7784 return $data;
7785 }
7786
7787 foreach ($data as $key => $value) {
7788 $data[$key] = self::stripEmptyFiles($value);
7789 }
7790
7791 return $data;
7792 }
7793}
7794
7795------------------------------------------------------------------------------------------------------------------------
7796 ./SubmitButton.php
7797------------------------------------------------------------------------------------------------------------------------
7798<?php
7799
7800/*
7801 * This file is part of the Symfony package.
7802 *
7803 * (c) Fabien Potencier <fabien@symfony.com>
7804 *
7805 * For the full copyright and license information, please view the LICENSE
7806 * file that was distributed with this source code.
7807 */
7808
7809namespace Symfony\Component\Form;
7810
7811/**
7812 * A button that submits the form.
7813 *
7814 * @author Bernhard Schussek <bschussek@gmail.com>
7815 */
7816class SubmitButton extends Button implements ClickableInterface
7817{
7818 private bool $clicked = false;
7819
7820 public function isClicked(): bool
7821 {
7822 return $this->clicked;
7823 }
7824
7825 /**
7826 * Submits data to the button.
7827 *
7828 * @return $this
7829 *
7830 * @throws Exception\AlreadySubmittedException if the form has already been submitted
7831 */
7832 public function submit(array|string|null $submittedData, bool $clearMissing = true): static
7833 {
7834 if ($this->getConfig()->getDisabled()) {
7835 $this->clicked = false;
7836
7837 return $this;
7838 }
7839
7840 parent::submit($submittedData, $clearMissing);
7841
7842 $this->clicked = null !== $submittedData;
7843
7844 return $this;
7845 }
7846}
7847
7848------------------------------------------------------------------------------------------------------------------------
7849 ./Test/Traits/ValidatorExtensionTrait.php
7850------------------------------------------------------------------------------------------------------------------------
7851<?php
7852
7853/*
7854 * This file is part of the Symfony package.
7855 *
7856 * (c) Fabien Potencier <fabien@symfony.com>
7857 *
7858 * For the full copyright and license information, please view the LICENSE
7859 * file that was distributed with this source code.
7860 */
7861
7862namespace Symfony\Component\Form\Test\Traits;
7863
7864use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
7865use Symfony\Component\Form\Test\TypeTestCase;
7866use Symfony\Component\Validator\ConstraintViolationList;
7867use Symfony\Component\Validator\Mapping\ClassMetadata;
7868use Symfony\Component\Validator\Validator\ValidatorInterface;
7869
7870trait ValidatorExtensionTrait
7871{
7872 protected ValidatorInterface $validator;
7873
7874 protected function getValidatorExtension(): ValidatorExtension
7875 {
7876 if (!interface_exists(ValidatorInterface::class)) {
7877 throw new \Exception('In order to use the "ValidatorExtensionTrait", the symfony/validator component must be installed.');
7878 }
7879
7880 if (!$this instanceof TypeTestCase) {
7881 throw new \Exception(\sprintf('The trait "ValidatorExtensionTrait" can only be added to a class that extends "%s".', TypeTestCase::class));
7882 }
7883
7884 $this->validator = $this->createMock(ValidatorInterface::class);
7885 $metadata = $this->getMockBuilder(ClassMetadata::class)->setConstructorArgs([''])->onlyMethods(['addPropertyConstraint'])->getMock();
7886 $this->validator->expects($this->any())->method('getMetadataFor')->willReturn($metadata);
7887 $this->validator->expects($this->any())->method('validate')->willReturn(new ConstraintViolationList());
7888
7889 return new ValidatorExtension($this->validator, false);
7890 }
7891}
7892
7893------------------------------------------------------------------------------------------------------------------------
7894 ./Test/FormPerformanceTestCase.php
7895------------------------------------------------------------------------------------------------------------------------
7896<?php
7897
7898/*
7899 * This file is part of the Symfony package.
7900 *
7901 * (c) Fabien Potencier <fabien@symfony.com>
7902 *
7903 * For the full copyright and license information, please view the LICENSE
7904 * file that was distributed with this source code.
7905 */
7906
7907namespace Symfony\Component\Form\Test;
7908
7909/**
7910 * Base class for performance tests.
7911 *
7912 * Copied from Doctrine 2's OrmPerformanceTestCase.
7913 *
7914 * @author robo
7915 * @author Bernhard Schussek <bschussek@gmail.com>
7916 */
7917abstract class FormPerformanceTestCase extends FormIntegrationTestCase
7918{
7919 private float $startTime;
7920 protected int $maxRunningTime = 0;
7921
7922 protected function setUp(): void
7923 {
7924 parent::setUp();
7925
7926 $this->startTime = microtime(true);
7927 }
7928
7929 protected function assertPostConditions(): void
7930 {
7931 parent::assertPostConditions();
7932
7933 $time = microtime(true) - $this->startTime;
7934
7935 if (0 != $this->maxRunningTime && $time > $this->maxRunningTime) {
7936 $this->fail(\sprintf('expected running time: <= %s but was: %s', $this->maxRunningTime, $time));
7937 }
7938
7939 $this->expectNotToPerformAssertions();
7940 }
7941
7942 /**
7943 * @throws \InvalidArgumentException
7944 */
7945 public function setMaxRunningTime(int $maxRunningTime): void
7946 {
7947 if ($maxRunningTime < 0) {
7948 throw new \InvalidArgumentException();
7949 }
7950
7951 $this->maxRunningTime = $maxRunningTime;
7952 }
7953
7954 public function getMaxRunningTime(): int
7955 {
7956 return $this->maxRunningTime;
7957 }
7958}
7959
7960------------------------------------------------------------------------------------------------------------------------
7961 ./Test/FormBuilderInterface.php
7962------------------------------------------------------------------------------------------------------------------------
7963<?php
7964
7965/*
7966 * This file is part of the Symfony package.
7967 *
7968 * (c) Fabien Potencier <fabien@symfony.com>
7969 *
7970 * For the full copyright and license information, please view the LICENSE
7971 * file that was distributed with this source code.
7972 */
7973
7974namespace Symfony\Component\Form\Test;
7975
7976use Symfony\Component\Form\FormBuilderInterface as BaseFormBuilderInterface;
7977
7978interface FormBuilderInterface extends \Iterator, BaseFormBuilderInterface
7979{
7980}
7981
7982------------------------------------------------------------------------------------------------------------------------
7983 ./Test/FormIntegrationTestCase.php
7984------------------------------------------------------------------------------------------------------------------------
7985<?php
7986
7987/*
7988 * This file is part of the Symfony package.
7989 *
7990 * (c) Fabien Potencier <fabien@symfony.com>
7991 *
7992 * For the full copyright and license information, please view the LICENSE
7993 * file that was distributed with this source code.
7994 */
7995
7996namespace Symfony\Component\Form\Test;
7997
7998use PHPUnit\Framework\TestCase;
7999use Symfony\Component\Form\FormExtensionInterface;
8000use Symfony\Component\Form\FormFactoryInterface;
8001use Symfony\Component\Form\Forms;
8002use Symfony\Component\Form\FormTypeExtensionInterface;
8003use Symfony\Component\Form\FormTypeGuesserInterface;
8004use Symfony\Component\Form\FormTypeInterface;
8005
8006/**
8007 * @author Bernhard Schussek <bschussek@gmail.com>
8008 */
8009abstract class FormIntegrationTestCase extends TestCase
8010{
8011 protected FormFactoryInterface $factory;
8012
8013 protected function setUp(): void
8014 {
8015 $this->factory = Forms::createFormFactoryBuilder()
8016 ->addExtensions($this->getExtensions())
8017 ->addTypeExtensions($this->getTypeExtensions())
8018 ->addTypes($this->getTypes())
8019 ->addTypeGuessers($this->getTypeGuessers())
8020 ->getFormFactory();
8021 }
8022
8023 /**
8024 * @return FormExtensionInterface[]
8025 */
8026 protected function getExtensions()
8027 {
8028 return [];
8029 }
8030
8031 /**
8032 * @return FormTypeExtensionInterface[]
8033 */
8034 protected function getTypeExtensions()
8035 {
8036 return [];
8037 }
8038
8039 /**
8040 * @return FormTypeInterface[]
8041 */
8042 protected function getTypes()
8043 {
8044 return [];
8045 }
8046
8047 /**
8048 * @return FormTypeGuesserInterface[]
8049 */
8050 protected function getTypeGuessers()
8051 {
8052 return [];
8053 }
8054}
8055
8056------------------------------------------------------------------------------------------------------------------------
8057 ./Test/FormInterface.php
8058------------------------------------------------------------------------------------------------------------------------
8059<?php
8060
8061/*
8062 * This file is part of the Symfony package.
8063 *
8064 * (c) Fabien Potencier <fabien@symfony.com>
8065 *
8066 * For the full copyright and license information, please view the LICENSE
8067 * file that was distributed with this source code.
8068 */
8069
8070namespace Symfony\Component\Form\Test;
8071
8072use Symfony\Component\Form\FormInterface as BaseFormInterface;
8073
8074interface FormInterface extends \Iterator, BaseFormInterface
8075{
8076}
8077
8078------------------------------------------------------------------------------------------------------------------------
8079 ./Test/TypeTestCase.php
8080------------------------------------------------------------------------------------------------------------------------
8081<?php
8082
8083/*
8084 * This file is part of the Symfony package.
8085 *
8086 * (c) Fabien Potencier <fabien@symfony.com>
8087 *
8088 * For the full copyright and license information, please view the LICENSE
8089 * file that was distributed with this source code.
8090 */
8091
8092namespace Symfony\Component\Form\Test;
8093
8094use Symfony\Component\EventDispatcher\EventDispatcherInterface;
8095use Symfony\Component\Form\FormBuilder;
8096use Symfony\Component\Form\FormExtensionInterface;
8097use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
8098
8099abstract class TypeTestCase extends FormIntegrationTestCase
8100{
8101 protected FormBuilder $builder;
8102 protected EventDispatcherInterface $dispatcher;
8103
8104 protected function setUp(): void
8105 {
8106 parent::setUp();
8107
8108 $this->dispatcher = $this->createMock(EventDispatcherInterface::class);
8109 $this->builder = new FormBuilder('', null, $this->dispatcher, $this->factory);
8110 }
8111
8112 /**
8113 * @return FormExtensionInterface[]
8114 */
8115 protected function getExtensions()
8116 {
8117 $extensions = [];
8118
8119 if (\in_array(ValidatorExtensionTrait::class, class_uses($this), true)) {
8120 $extensions[] = $this->getValidatorExtension();
8121 }
8122
8123 return $extensions;
8124 }
8125
8126 /**
8127 * @return void
8128 */
8129 public static function assertDateTimeEquals(\DateTime $expected, \DateTime $actual)
8130 {
8131 self::assertEquals($expected->format('c'), $actual->format('c'));
8132 }
8133
8134 /**
8135 * @return void
8136 */
8137 public static function assertDateIntervalEquals(\DateInterval $expected, \DateInterval $actual)
8138 {
8139 self::assertEquals($expected->format('%RP%yY%mM%dDT%hH%iM%sS'), $actual->format('%RP%yY%mM%dDT%hH%iM%sS'));
8140 }
8141}
8142
8143------------------------------------------------------------------------------------------------------------------------
8144 ./FormConfigInterface.php
8145------------------------------------------------------------------------------------------------------------------------
8146<?php
8147
8148/*
8149 * This file is part of the Symfony package.
8150 *
8151 * (c) Fabien Potencier <fabien@symfony.com>
8152 *
8153 * For the full copyright and license information, please view the LICENSE
8154 * file that was distributed with this source code.
8155 */
8156
8157namespace Symfony\Component\Form;
8158
8159use Symfony\Component\EventDispatcher\EventDispatcherInterface;
8160use Symfony\Component\PropertyAccess\PropertyPathInterface;
8161
8162/**
8163 * The configuration of a {@link Form} object.
8164 *
8165 * @author Bernhard Schussek <bschussek@gmail.com>
8166 */
8167interface FormConfigInterface
8168{
8169 /**
8170 * Returns the event dispatcher used to dispatch form events.
8171 */
8172 public function getEventDispatcher(): EventDispatcherInterface;
8173
8174 /**
8175 * Returns the name of the form used as HTTP parameter.
8176 */
8177 public function getName(): string;
8178
8179 /**
8180 * Returns the property path that the form should be mapped to.
8181 */
8182 public function getPropertyPath(): ?PropertyPathInterface;
8183
8184 /**
8185 * Returns whether the form should be mapped to an element of its
8186 * parent's data.
8187 */
8188 public function getMapped(): bool;
8189
8190 /**
8191 * Returns whether the form's data should be modified by reference.
8192 */
8193 public function getByReference(): bool;
8194
8195 /**
8196 * Returns whether the form should read and write the data of its parent.
8197 */
8198 public function getInheritData(): bool;
8199
8200 /**
8201 * Returns whether the form is compound.
8202 *
8203 * This property is independent of whether the form actually has
8204 * children. A form can be compound and have no children at all, like
8205 * for example an empty collection form.
8206 * The contrary is not possible, a form which is not compound
8207 * cannot have any children.
8208 */
8209 public function getCompound(): bool;
8210
8211 /**
8212 * Returns the resolved form type used to construct the form.
8213 */
8214 public function getType(): ResolvedFormTypeInterface;
8215
8216 /**
8217 * Returns the view transformers of the form.
8218 *
8219 * @return DataTransformerInterface[]
8220 */
8221 public function getViewTransformers(): array;
8222
8223 /**
8224 * Returns the model transformers of the form.
8225 *
8226 * @return DataTransformerInterface[]
8227 */
8228 public function getModelTransformers(): array;
8229
8230 /**
8231 * Returns the data mapper of the compound form or null for a simple form.
8232 */
8233 public function getDataMapper(): ?DataMapperInterface;
8234
8235 /**
8236 * Returns whether the form is required.
8237 */
8238 public function getRequired(): bool;
8239
8240 /**
8241 * Returns whether the form is disabled.
8242 */
8243 public function getDisabled(): bool;
8244
8245 /**
8246 * Returns whether errors attached to the form will bubble to its parent.
8247 */
8248 public function getErrorBubbling(): bool;
8249
8250 /**
8251 * Used when the view data is empty on submission.
8252 *
8253 * When the form is compound it will also be used to map the
8254 * children data.
8255 *
8256 * The empty data must match the view format as it will passed to the first view transformer's
8257 * "reverseTransform" method.
8258 */
8259 public function getEmptyData(): mixed;
8260
8261 /**
8262 * Returns additional attributes of the form.
8263 */
8264 public function getAttributes(): array;
8265
8266 /**
8267 * Returns whether the attribute with the given name exists.
8268 */
8269 public function hasAttribute(string $name): bool;
8270
8271 /**
8272 * Returns the value of the given attribute.
8273 */
8274 public function getAttribute(string $name, mixed $default = null): mixed;
8275
8276 /**
8277 * Returns the initial data of the form.
8278 */
8279 public function getData(): mixed;
8280
8281 /**
8282 * Returns the class of the view data or null if the data is scalar or an array.
8283 */
8284 public function getDataClass(): ?string;
8285
8286 /**
8287 * Returns whether the form's data is locked.
8288 *
8289 * A form with locked data is restricted to the data passed in
8290 * this configuration. The data can only be modified then by
8291 * submitting the form.
8292 */
8293 public function getDataLocked(): bool;
8294
8295 /**
8296 * Returns the form factory used for creating new forms.
8297 */
8298 public function getFormFactory(): FormFactoryInterface;
8299
8300 /**
8301 * Returns the target URL of the form.
8302 */
8303 public function getAction(): string;
8304
8305 /**
8306 * Returns the HTTP method used by the form.
8307 */
8308 public function getMethod(): string;
8309
8310 /**
8311 * Returns the request handler used by the form.
8312 */
8313 public function getRequestHandler(): RequestHandlerInterface;
8314
8315 /**
8316 * Returns whether the form should be initialized upon creation.
8317 */
8318 public function getAutoInitialize(): bool;
8319
8320 /**
8321 * Returns all options passed during the construction of the form.
8322 *
8323 * @return array<string, mixed> The passed options
8324 */
8325 public function getOptions(): array;
8326
8327 /**
8328 * Returns whether a specific option exists.
8329 */
8330 public function hasOption(string $name): bool;
8331
8332 /**
8333 * Returns the value of a specific option.
8334 */
8335 public function getOption(string $name, mixed $default = null): mixed;
8336
8337 /**
8338 * Returns a callable that takes the model data as argument and that returns if it is empty or not.
8339 */
8340 public function getIsEmptyCallback(): ?callable;
8341}
8342
8343------------------------------------------------------------------------------------------------------------------------
8344 ./Button.php
8345------------------------------------------------------------------------------------------------------------------------
8346<?php
8347
8348/*
8349 * This file is part of the Symfony package.
8350 *
8351 * (c) Fabien Potencier <fabien@symfony.com>
8352 *
8353 * For the full copyright and license information, please view the LICENSE
8354 * file that was distributed with this source code.
8355 */
8356
8357namespace Symfony\Component\Form;
8358
8359use Symfony\Component\Form\Exception\AlreadySubmittedException;
8360use Symfony\Component\Form\Exception\BadMethodCallException;
8361use Symfony\Component\Form\Exception\TransformationFailedException;
8362use Symfony\Component\PropertyAccess\PropertyPathInterface;
8363
8364/**
8365 * A form button.
8366 *
8367 * @author Bernhard Schussek <bschussek@gmail.com>
8368 *
8369 * @implements \IteratorAggregate<string, FormInterface>
8370 */
8371class Button implements \IteratorAggregate, FormInterface
8372{
8373 private ?FormInterface $parent = null;
8374 private bool $submitted = false;
8375
8376 /**
8377 * Creates a new button from a form configuration.
8378 */
8379 public function __construct(
8380 private FormConfigInterface $config,
8381 ) {
8382 }
8383
8384 /**
8385 * Unsupported method.
8386 */
8387 public function offsetExists(mixed $offset): bool
8388 {
8389 return false;
8390 }
8391
8392 /**
8393 * Unsupported method.
8394 *
8395 * This method should not be invoked.
8396 *
8397 * @throws BadMethodCallException
8398 */
8399 public function offsetGet(mixed $offset): FormInterface
8400 {
8401 throw new BadMethodCallException('Buttons cannot have children.');
8402 }
8403
8404 /**
8405 * Unsupported method.
8406 *
8407 * This method should not be invoked.
8408 *
8409 * @throws BadMethodCallException
8410 */
8411 public function offsetSet(mixed $offset, mixed $value): void
8412 {
8413 throw new BadMethodCallException('Buttons cannot have children.');
8414 }
8415
8416 /**
8417 * Unsupported method.
8418 *
8419 * This method should not be invoked.
8420 *
8421 * @throws BadMethodCallException
8422 */
8423 public function offsetUnset(mixed $offset): void
8424 {
8425 throw new BadMethodCallException('Buttons cannot have children.');
8426 }
8427
8428 public function setParent(?FormInterface $parent): static
8429 {
8430 if ($this->submitted) {
8431 throw new AlreadySubmittedException('You cannot set the parent of a submitted button.');
8432 }
8433
8434 $this->parent = $parent;
8435
8436 return $this;
8437 }
8438
8439 public function getParent(): ?FormInterface
8440 {
8441 return $this->parent;
8442 }
8443
8444 /**
8445 * Unsupported method.
8446 *
8447 * This method should not be invoked.
8448 *
8449 * @throws BadMethodCallException
8450 */
8451 public function add(string|FormInterface $child, ?string $type = null, array $options = []): static
8452 {
8453 throw new BadMethodCallException('Buttons cannot have children.');
8454 }
8455
8456 /**
8457 * Unsupported method.
8458 *
8459 * This method should not be invoked.
8460 *
8461 * @throws BadMethodCallException
8462 */
8463 public function get(string $name): FormInterface
8464 {
8465 throw new BadMethodCallException('Buttons cannot have children.');
8466 }
8467
8468 /**
8469 * Unsupported method.
8470 */
8471 public function has(string $name): bool
8472 {
8473 return false;
8474 }
8475
8476 /**
8477 * Unsupported method.
8478 *
8479 * This method should not be invoked.
8480 *
8481 * @throws BadMethodCallException
8482 */
8483 public function remove(string $name): static
8484 {
8485 throw new BadMethodCallException('Buttons cannot have children.');
8486 }
8487
8488 public function all(): array
8489 {
8490 return [];
8491 }
8492
8493 public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator
8494 {
8495 return new FormErrorIterator($this, []);
8496 }
8497
8498 /**
8499 * Unsupported method.
8500 *
8501 * This method should not be invoked.
8502 *
8503 * @return $this
8504 */
8505 public function setData(mixed $modelData): static
8506 {
8507 // no-op, called during initialization of the form tree
8508 return $this;
8509 }
8510
8511 /**
8512 * Unsupported method.
8513 */
8514 public function getData(): mixed
8515 {
8516 return null;
8517 }
8518
8519 /**
8520 * Unsupported method.
8521 */
8522 public function getNormData(): mixed
8523 {
8524 return null;
8525 }
8526
8527 /**
8528 * Unsupported method.
8529 */
8530 public function getViewData(): mixed
8531 {
8532 return null;
8533 }
8534
8535 /**
8536 * Unsupported method.
8537 */
8538 public function getExtraData(): array
8539 {
8540 return [];
8541 }
8542
8543 /**
8544 * Returns the button's configuration.
8545 */
8546 public function getConfig(): FormConfigInterface
8547 {
8548 return $this->config;
8549 }
8550
8551 /**
8552 * Returns whether the button is submitted.
8553 */
8554 public function isSubmitted(): bool
8555 {
8556 return $this->submitted;
8557 }
8558
8559 /**
8560 * Returns the name by which the button is identified in forms.
8561 */
8562 public function getName(): string
8563 {
8564 return $this->config->getName();
8565 }
8566
8567 /**
8568 * Unsupported method.
8569 */
8570 public function getPropertyPath(): ?PropertyPathInterface
8571 {
8572 return null;
8573 }
8574
8575 /**
8576 * Unsupported method.
8577 *
8578 * @throws BadMethodCallException
8579 */
8580 public function addError(FormError $error): static
8581 {
8582 throw new BadMethodCallException('Buttons cannot have errors.');
8583 }
8584
8585 /**
8586 * Unsupported method.
8587 */
8588 public function isValid(): bool
8589 {
8590 return true;
8591 }
8592
8593 /**
8594 * Unsupported method.
8595 */
8596 public function isRequired(): bool
8597 {
8598 return false;
8599 }
8600
8601 public function isDisabled(): bool
8602 {
8603 if ($this->parent?->isDisabled()) {
8604 return true;
8605 }
8606
8607 return $this->config->getDisabled();
8608 }
8609
8610 /**
8611 * Unsupported method.
8612 */
8613 public function isEmpty(): bool
8614 {
8615 return true;
8616 }
8617
8618 /**
8619 * Unsupported method.
8620 */
8621 public function isSynchronized(): bool
8622 {
8623 return true;
8624 }
8625
8626 /**
8627 * Unsupported method.
8628 */
8629 public function getTransformationFailure(): ?TransformationFailedException
8630 {
8631 return null;
8632 }
8633
8634 /**
8635 * Unsupported method.
8636 *
8637 * @throws BadMethodCallException
8638 */
8639 public function initialize(): static
8640 {
8641 throw new BadMethodCallException('Buttons cannot be initialized. Call initialize() on the root form instead.');
8642 }
8643
8644 /**
8645 * Unsupported method.
8646 *
8647 * @throws BadMethodCallException
8648 */
8649 public function handleRequest(mixed $request = null): static
8650 {
8651 throw new BadMethodCallException('Buttons cannot handle requests. Call handleRequest() on the root form instead.');
8652 }
8653
8654 /**
8655 * Submits data to the button.
8656 *
8657 * @return $this
8658 *
8659 * @throws AlreadySubmittedException if the button has already been submitted
8660 */
8661 public function submit(array|string|null $submittedData, bool $clearMissing = true): static
8662 {
8663 if ($this->submitted) {
8664 throw new AlreadySubmittedException('A form can only be submitted once.');
8665 }
8666
8667 $this->submitted = true;
8668
8669 return $this;
8670 }
8671
8672 public function getRoot(): FormInterface
8673 {
8674 return $this->parent ? $this->parent->getRoot() : $this;
8675 }
8676
8677 public function isRoot(): bool
8678 {
8679 return null === $this->parent;
8680 }
8681
8682 public function createView(?FormView $parent = null): FormView
8683 {
8684 if (null === $parent && $this->parent) {
8685 $parent = $this->parent->createView();
8686 }
8687
8688 $type = $this->config->getType();
8689 $options = $this->config->getOptions();
8690
8691 $view = $type->createView($this, $parent);
8692
8693 $type->buildView($view, $this, $options);
8694 $type->finishView($view, $this, $options);
8695
8696 return $view;
8697 }
8698
8699 /**
8700 * Unsupported method.
8701 */
8702 public function count(): int
8703 {
8704 return 0;
8705 }
8706
8707 /**
8708 * Unsupported method.
8709 */
8710 public function getIterator(): \EmptyIterator
8711 {
8712 return new \EmptyIterator();
8713 }
8714}
8715
8716------------------------------------------------------------------------------------------------------------------------
8717 ./DataMapperInterface.php
8718------------------------------------------------------------------------------------------------------------------------
8719<?php
8720
8721/*
8722 * This file is part of the Symfony package.
8723 *
8724 * (c) Fabien Potencier <fabien@symfony.com>
8725 *
8726 * For the full copyright and license information, please view the LICENSE
8727 * file that was distributed with this source code.
8728 */
8729
8730namespace Symfony\Component\Form;
8731
8732/**
8733 * @author Bernhard Schussek <bschussek@gmail.com>
8734 */
8735interface DataMapperInterface
8736{
8737 /**
8738 * Maps the view data of a compound form to its children.
8739 *
8740 * The method is responsible for calling {@link FormInterface::setData()}
8741 * on the children of compound forms, defining their underlying model data.
8742 *
8743 * @param mixed $viewData View data of the compound form being initialized
8744 * @param \Traversable<mixed, FormInterface> $forms A list of {@link FormInterface} instances
8745 *
8746 * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported
8747 */
8748 public function mapDataToForms(mixed $viewData, \Traversable $forms): void;
8749
8750 /**
8751 * Maps the model data of a list of children forms into the view data of their parent.
8752 *
8753 * This is the internal cascade call of FormInterface::submit for compound forms, since they
8754 * cannot be bound to any input nor the request as scalar, but their children may:
8755 *
8756 * $compoundForm->submit($arrayOfChildrenViewData)
8757 * // inside:
8758 * $childForm->submit($childViewData);
8759 * // for each entry, do the same and/or reverse transform
8760 * $this->dataMapper->mapFormsToData($compoundForm, $compoundInitialViewData)
8761 * // then reverse transform
8762 *
8763 * When a simple form is submitted the following is happening:
8764 *
8765 * $simpleForm->submit($submittedViewData)
8766 * // inside:
8767 * $this->viewData = $submittedViewData
8768 * // then reverse transform
8769 *
8770 * The model data can be an array or an object, so this second argument is always passed
8771 * by reference.
8772 *
8773 * @param \Traversable<mixed, FormInterface> $forms A list of {@link FormInterface} instances
8774 * @param mixed &$viewData The compound form's view data that get mapped
8775 * its children model data
8776 *
8777 * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported
8778 */
8779 public function mapFormsToData(\Traversable $forms, mixed &$viewData): void;
8780}
8781
8782------------------------------------------------------------------------------------------------------------------------
8783 ./FormErrorIterator.php
8784------------------------------------------------------------------------------------------------------------------------
8785<?php
8786
8787/*
8788 * This file is part of the Symfony package.
8789 *
8790 * (c) Fabien Potencier <fabien@symfony.com>
8791 *
8792 * For the full copyright and license information, please view the LICENSE
8793 * file that was distributed with this source code.
8794 */
8795
8796namespace Symfony\Component\Form;
8797
8798use Symfony\Component\Form\Exception\BadMethodCallException;
8799use Symfony\Component\Form\Exception\InvalidArgumentException;
8800use Symfony\Component\Form\Exception\LogicException;
8801use Symfony\Component\Form\Exception\OutOfBoundsException;
8802use Symfony\Component\Validator\ConstraintViolation;
8803
8804/**
8805 * Iterates over the errors of a form.
8806 *
8807 * This class supports recursive iteration. In order to iterate recursively,
8808 * pass a structure of {@link FormError} and {@link FormErrorIterator} objects
8809 * to the $errors constructor argument.
8810 *
8811 * You can also wrap the iterator into a {@link \RecursiveIteratorIterator} to
8812 * flatten the recursive structure into a flat list of errors.
8813 *
8814 * @author Bernhard Schussek <bschussek@gmail.com>
8815 *
8816 * @template T of FormError|FormErrorIterator
8817 *
8818 * @implements \ArrayAccess<int, T>
8819 * @implements \RecursiveIterator<int, T>
8820 * @implements \SeekableIterator<int, T>
8821 */
8822class FormErrorIterator implements \RecursiveIterator, \SeekableIterator, \ArrayAccess, \Countable, \Stringable
8823{
8824 /**
8825 * The prefix used for indenting nested error messages.
8826 */
8827 public const INDENTATION = ' ';
8828
8829 /**
8830 * @var list<T>
8831 */
8832 private array $errors;
8833
8834 /**
8835 * @param list<T> $errors
8836 *
8837 * @throws InvalidArgumentException If the errors are invalid
8838 */
8839 public function __construct(
8840 private FormInterface $form,
8841 array $errors,
8842 ) {
8843 foreach ($errors as $error) {
8844 if (!($error instanceof FormError || $error instanceof self)) {
8845 throw new InvalidArgumentException(\sprintf('The errors must be instances of "Symfony\Component\Form\FormError" or "%s". Got: "%s".', __CLASS__, get_debug_type($error)));
8846 }
8847 }
8848
8849 $this->errors = $errors;
8850 }
8851
8852 /**
8853 * Returns all iterated error messages as string.
8854 */
8855 public function __toString(): string
8856 {
8857 $string = '';
8858
8859 foreach ($this->errors as $error) {
8860 if ($error instanceof FormError) {
8861 $string .= 'ERROR: '.$error->getMessage()."\n";
8862 } else {
8863 /* @var self $error */
8864 $string .= $error->getForm()->getName().":\n";
8865 $string .= self::indent((string) $error);
8866 }
8867 }
8868
8869 return $string;
8870 }
8871
8872 /**
8873 * Returns the iterated form.
8874 */
8875 public function getForm(): FormInterface
8876 {
8877 return $this->form;
8878 }
8879
8880 /**
8881 * Returns the current element of the iterator.
8882 *
8883 * @return T An error or an iterator containing nested errors
8884 */
8885 public function current(): FormError|self
8886 {
8887 return current($this->errors);
8888 }
8889
8890 /**
8891 * Advances the iterator to the next position.
8892 */
8893 public function next(): void
8894 {
8895 next($this->errors);
8896 }
8897
8898 /**
8899 * Returns the current position of the iterator.
8900 */
8901 public function key(): int
8902 {
8903 return key($this->errors);
8904 }
8905
8906 /**
8907 * Returns whether the iterator's position is valid.
8908 */
8909 public function valid(): bool
8910 {
8911 return null !== key($this->errors);
8912 }
8913
8914 /**
8915 * Sets the iterator's position to the beginning.
8916 *
8917 * This method detects if errors have been added to the form since the
8918 * construction of the iterator.
8919 */
8920 public function rewind(): void
8921 {
8922 reset($this->errors);
8923 }
8924
8925 /**
8926 * Returns whether a position exists in the iterator.
8927 *
8928 * @param int $position The position
8929 */
8930 public function offsetExists(mixed $position): bool
8931 {
8932 return isset($this->errors[$position]);
8933 }
8934
8935 /**
8936 * Returns the element at a position in the iterator.
8937 *
8938 * @param int $position The position
8939 *
8940 * @return T
8941 *
8942 * @throws OutOfBoundsException If the given position does not exist
8943 */
8944 public function offsetGet(mixed $position): FormError|self
8945 {
8946 if (!isset($this->errors[$position])) {
8947 throw new OutOfBoundsException('The offset '.$position.' does not exist.');
8948 }
8949
8950 return $this->errors[$position];
8951 }
8952
8953 /**
8954 * Unsupported method.
8955 *
8956 * @throws BadMethodCallException
8957 */
8958 public function offsetSet(mixed $position, mixed $value): void
8959 {
8960 throw new BadMethodCallException('The iterator doesn\'t support modification of elements.');
8961 }
8962
8963 /**
8964 * Unsupported method.
8965 *
8966 * @throws BadMethodCallException
8967 */
8968 public function offsetUnset(mixed $position): void
8969 {
8970 throw new BadMethodCallException('The iterator doesn\'t support modification of elements.');
8971 }
8972
8973 /**
8974 * Returns whether the current element of the iterator can be recursed
8975 * into.
8976 */
8977 public function hasChildren(): bool
8978 {
8979 return current($this->errors) instanceof self;
8980 }
8981
8982 public function getChildren(): self
8983 {
8984 if (!$this->hasChildren()) {
8985 throw new LogicException(\sprintf('The current element is not iterable. Use "%s" to get the current element.', self::class.'::current()'));
8986 }
8987
8988 /** @var self $children */
8989 $children = current($this->errors);
8990
8991 return $children;
8992 }
8993
8994 /**
8995 * Returns the number of elements in the iterator.
8996 *
8997 * Note that this is not the total number of errors, if the constructor
8998 * parameter $deep was set to true! In that case, you should wrap the
8999 * iterator into a {@link \RecursiveIteratorIterator} with the standard mode
9000 * {@link \RecursiveIteratorIterator::LEAVES_ONLY} and count the result.
9001 *
9002 * $iterator = new \RecursiveIteratorIterator($form->getErrors(true));
9003 * $count = count(iterator_to_array($iterator));
9004 *
9005 * Alternatively, set the constructor argument $flatten to true as well.
9006 *
9007 * $count = count($form->getErrors(true, true));
9008 */
9009 public function count(): int
9010 {
9011 return \count($this->errors);
9012 }
9013
9014 /**
9015 * Sets the position of the iterator.
9016 *
9017 * @throws OutOfBoundsException If the position is invalid
9018 */
9019 public function seek(int $position): void
9020 {
9021 if (!isset($this->errors[$position])) {
9022 throw new OutOfBoundsException('The offset '.$position.' does not exist.');
9023 }
9024
9025 reset($this->errors);
9026
9027 while ($position !== key($this->errors)) {
9028 next($this->errors);
9029 }
9030 }
9031
9032 /**
9033 * Creates iterator for errors with specific codes.
9034 *
9035 * @param string|string[] $codes The codes to find
9036 */
9037 public function findByCodes(string|array $codes): static
9038 {
9039 $codes = (array) $codes;
9040 $errors = [];
9041 foreach ($this as $error) {
9042 $cause = $error->getCause();
9043 if ($cause instanceof ConstraintViolation && \in_array($cause->getCode(), $codes, true)) {
9044 $errors[] = $error;
9045 }
9046 }
9047
9048 return new static($this->form, $errors);
9049 }
9050
9051 /**
9052 * Utility function for indenting multi-line strings.
9053 */
9054 private static function indent(string $string): string
9055 {
9056 return rtrim(self::INDENTATION.str_replace("\n", "\n".self::INDENTATION, $string), ' ');
9057 }
9058}
9059
9060------------------------------------------------------------------------------------------------------------------------
9061 ./FormConfigBuilder.php
9062------------------------------------------------------------------------------------------------------------------------
9063<?php
9064
9065/*
9066 * This file is part of the Symfony package.
9067 *
9068 * (c) Fabien Potencier <fabien@symfony.com>
9069 *
9070 * For the full copyright and license information, please view the LICENSE
9071 * file that was distributed with this source code.
9072 */
9073
9074namespace Symfony\Component\Form;
9075
9076use Symfony\Component\EventDispatcher\EventDispatcherInterface;
9077use Symfony\Component\EventDispatcher\EventSubscriberInterface;
9078use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
9079use Symfony\Component\Form\Exception\BadMethodCallException;
9080use Symfony\Component\Form\Exception\InvalidArgumentException;
9081use Symfony\Component\PropertyAccess\PropertyPath;
9082use Symfony\Component\PropertyAccess\PropertyPathInterface;
9083
9084/**
9085 * A basic form configuration.
9086 *
9087 * @author Bernhard Schussek <bschussek@gmail.com>
9088 */
9089class FormConfigBuilder implements FormConfigBuilderInterface
9090{
9091 protected bool $locked = false;
9092
9093 /**
9094 * Caches a globally unique {@link NativeRequestHandler} instance.
9095 */
9096 private static NativeRequestHandler $nativeRequestHandler;
9097
9098 private string $name;
9099 private ?PropertyPathInterface $propertyPath = null;
9100 private bool $mapped = true;
9101 private bool $byReference = true;
9102 private bool $inheritData = false;
9103 private bool $compound = false;
9104 private ResolvedFormTypeInterface $type;
9105 private array $viewTransformers = [];
9106 private array $modelTransformers = [];
9107 private ?DataMapperInterface $dataMapper = null;
9108 private bool $required = true;
9109 private bool $disabled = false;
9110 private bool $errorBubbling = false;
9111 private mixed $emptyData = null;
9112 private array $attributes = [];
9113 private mixed $data = null;
9114 private ?string $dataClass;
9115 private bool $dataLocked = false;
9116 private FormFactoryInterface $formFactory;
9117 private string $action = '';
9118 private string $method = 'POST';
9119 private RequestHandlerInterface $requestHandler;
9120 private bool $autoInitialize = false;
9121 private ?\Closure $isEmptyCallback = null;
9122
9123 /**
9124 * Creates an empty form configuration.
9125 *
9126 * @param string|null $name The form name
9127 * @param string|null $dataClass The class of the form's data
9128 *
9129 * @throws InvalidArgumentException if the data class is not a valid class or if
9130 * the name contains invalid characters
9131 */
9132 public function __construct(
9133 ?string $name,
9134 ?string $dataClass,
9135 private EventDispatcherInterface $dispatcher,
9136 private array $options = [],
9137 ) {
9138 self::validateName($name);
9139
9140 if (null !== $dataClass && !class_exists($dataClass) && !interface_exists($dataClass, false)) {
9141 throw new InvalidArgumentException(\sprintf('Class "%s" not found. Is the "data_class" form option set correctly?', $dataClass));
9142 }
9143
9144 $this->name = (string) $name;
9145 $this->dataClass = $dataClass;
9146 }
9147
9148 public function addEventListener(string $eventName, callable $listener, int $priority = 0): static
9149 {
9150 if ($this->locked) {
9151 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9152 }
9153
9154 $this->dispatcher->addListener($eventName, $listener, $priority);
9155
9156 return $this;
9157 }
9158
9159 public function addEventSubscriber(EventSubscriberInterface $subscriber): static
9160 {
9161 if ($this->locked) {
9162 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9163 }
9164
9165 $this->dispatcher->addSubscriber($subscriber);
9166
9167 return $this;
9168 }
9169
9170 public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): static
9171 {
9172 if ($this->locked) {
9173 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9174 }
9175
9176 if ($forcePrepend) {
9177 array_unshift($this->viewTransformers, $viewTransformer);
9178 } else {
9179 $this->viewTransformers[] = $viewTransformer;
9180 }
9181
9182 return $this;
9183 }
9184
9185 public function resetViewTransformers(): static
9186 {
9187 if ($this->locked) {
9188 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9189 }
9190
9191 $this->viewTransformers = [];
9192
9193 return $this;
9194 }
9195
9196 public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): static
9197 {
9198 if ($this->locked) {
9199 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9200 }
9201
9202 if ($forceAppend) {
9203 $this->modelTransformers[] = $modelTransformer;
9204 } else {
9205 array_unshift($this->modelTransformers, $modelTransformer);
9206 }
9207
9208 return $this;
9209 }
9210
9211 public function resetModelTransformers(): static
9212 {
9213 if ($this->locked) {
9214 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9215 }
9216
9217 $this->modelTransformers = [];
9218
9219 return $this;
9220 }
9221
9222 public function getEventDispatcher(): EventDispatcherInterface
9223 {
9224 if ($this->locked && !$this->dispatcher instanceof ImmutableEventDispatcher) {
9225 $this->dispatcher = new ImmutableEventDispatcher($this->dispatcher);
9226 }
9227
9228 return $this->dispatcher;
9229 }
9230
9231 public function getName(): string
9232 {
9233 return $this->name;
9234 }
9235
9236 public function getPropertyPath(): ?PropertyPathInterface
9237 {
9238 return $this->propertyPath;
9239 }
9240
9241 public function getMapped(): bool
9242 {
9243 return $this->mapped;
9244 }
9245
9246 public function getByReference(): bool
9247 {
9248 return $this->byReference;
9249 }
9250
9251 public function getInheritData(): bool
9252 {
9253 return $this->inheritData;
9254 }
9255
9256 public function getCompound(): bool
9257 {
9258 return $this->compound;
9259 }
9260
9261 public function getType(): ResolvedFormTypeInterface
9262 {
9263 return $this->type;
9264 }
9265
9266 public function getViewTransformers(): array
9267 {
9268 return $this->viewTransformers;
9269 }
9270
9271 public function getModelTransformers(): array
9272 {
9273 return $this->modelTransformers;
9274 }
9275
9276 public function getDataMapper(): ?DataMapperInterface
9277 {
9278 return $this->dataMapper;
9279 }
9280
9281 public function getRequired(): bool
9282 {
9283 return $this->required;
9284 }
9285
9286 public function getDisabled(): bool
9287 {
9288 return $this->disabled;
9289 }
9290
9291 public function getErrorBubbling(): bool
9292 {
9293 return $this->errorBubbling;
9294 }
9295
9296 public function getEmptyData(): mixed
9297 {
9298 return $this->emptyData;
9299 }
9300
9301 public function getAttributes(): array
9302 {
9303 return $this->attributes;
9304 }
9305
9306 public function hasAttribute(string $name): bool
9307 {
9308 return \array_key_exists($name, $this->attributes);
9309 }
9310
9311 public function getAttribute(string $name, mixed $default = null): mixed
9312 {
9313 return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
9314 }
9315
9316 public function getData(): mixed
9317 {
9318 return $this->data;
9319 }
9320
9321 public function getDataClass(): ?string
9322 {
9323 return $this->dataClass;
9324 }
9325
9326 public function getDataLocked(): bool
9327 {
9328 return $this->dataLocked;
9329 }
9330
9331 public function getFormFactory(): FormFactoryInterface
9332 {
9333 if (!isset($this->formFactory)) {
9334 throw new BadMethodCallException('The form factory must be set before retrieving it.');
9335 }
9336
9337 return $this->formFactory;
9338 }
9339
9340 public function getAction(): string
9341 {
9342 return $this->action;
9343 }
9344
9345 public function getMethod(): string
9346 {
9347 return $this->method;
9348 }
9349
9350 public function getRequestHandler(): RequestHandlerInterface
9351 {
9352 return $this->requestHandler ??= self::$nativeRequestHandler ??= new NativeRequestHandler();
9353 }
9354
9355 public function getAutoInitialize(): bool
9356 {
9357 return $this->autoInitialize;
9358 }
9359
9360 public function getOptions(): array
9361 {
9362 return $this->options;
9363 }
9364
9365 public function hasOption(string $name): bool
9366 {
9367 return \array_key_exists($name, $this->options);
9368 }
9369
9370 public function getOption(string $name, mixed $default = null): mixed
9371 {
9372 return \array_key_exists($name, $this->options) ? $this->options[$name] : $default;
9373 }
9374
9375 public function getIsEmptyCallback(): ?callable
9376 {
9377 return $this->isEmptyCallback;
9378 }
9379
9380 /**
9381 * @return $this
9382 */
9383 public function setAttribute(string $name, mixed $value): static
9384 {
9385 if ($this->locked) {
9386 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9387 }
9388
9389 $this->attributes[$name] = $value;
9390
9391 return $this;
9392 }
9393
9394 /**
9395 * @return $this
9396 */
9397 public function setAttributes(array $attributes): static
9398 {
9399 if ($this->locked) {
9400 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9401 }
9402
9403 $this->attributes = $attributes;
9404
9405 return $this;
9406 }
9407
9408 /**
9409 * @return $this
9410 */
9411 public function setDataMapper(?DataMapperInterface $dataMapper): static
9412 {
9413 if ($this->locked) {
9414 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9415 }
9416
9417 $this->dataMapper = $dataMapper;
9418
9419 return $this;
9420 }
9421
9422 /**
9423 * @return $this
9424 */
9425 public function setDisabled(bool $disabled): static
9426 {
9427 if ($this->locked) {
9428 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9429 }
9430
9431 $this->disabled = $disabled;
9432
9433 return $this;
9434 }
9435
9436 /**
9437 * @return $this
9438 */
9439 public function setEmptyData(mixed $emptyData): static
9440 {
9441 if ($this->locked) {
9442 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9443 }
9444
9445 $this->emptyData = $emptyData;
9446
9447 return $this;
9448 }
9449
9450 /**
9451 * @return $this
9452 */
9453 public function setErrorBubbling(bool $errorBubbling): static
9454 {
9455 if ($this->locked) {
9456 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9457 }
9458
9459 $this->errorBubbling = $errorBubbling;
9460
9461 return $this;
9462 }
9463
9464 /**
9465 * @return $this
9466 */
9467 public function setRequired(bool $required): static
9468 {
9469 if ($this->locked) {
9470 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9471 }
9472
9473 $this->required = $required;
9474
9475 return $this;
9476 }
9477
9478 /**
9479 * @return $this
9480 */
9481 public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): static
9482 {
9483 if ($this->locked) {
9484 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9485 }
9486
9487 if (null !== $propertyPath && !$propertyPath instanceof PropertyPathInterface) {
9488 $propertyPath = new PropertyPath($propertyPath);
9489 }
9490
9491 $this->propertyPath = $propertyPath;
9492
9493 return $this;
9494 }
9495
9496 /**
9497 * @return $this
9498 */
9499 public function setMapped(bool $mapped): static
9500 {
9501 if ($this->locked) {
9502 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9503 }
9504
9505 $this->mapped = $mapped;
9506
9507 return $this;
9508 }
9509
9510 /**
9511 * @return $this
9512 */
9513 public function setByReference(bool $byReference): static
9514 {
9515 if ($this->locked) {
9516 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9517 }
9518
9519 $this->byReference = $byReference;
9520
9521 return $this;
9522 }
9523
9524 /**
9525 * @return $this
9526 */
9527 public function setInheritData(bool $inheritData): static
9528 {
9529 if ($this->locked) {
9530 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9531 }
9532
9533 $this->inheritData = $inheritData;
9534
9535 return $this;
9536 }
9537
9538 /**
9539 * @return $this
9540 */
9541 public function setCompound(bool $compound): static
9542 {
9543 if ($this->locked) {
9544 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9545 }
9546
9547 $this->compound = $compound;
9548
9549 return $this;
9550 }
9551
9552 /**
9553 * @return $this
9554 */
9555 public function setType(ResolvedFormTypeInterface $type): static
9556 {
9557 if ($this->locked) {
9558 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9559 }
9560
9561 $this->type = $type;
9562
9563 return $this;
9564 }
9565
9566 /**
9567 * @return $this
9568 */
9569 public function setData(mixed $data): static
9570 {
9571 if ($this->locked) {
9572 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9573 }
9574
9575 $this->data = $data;
9576
9577 return $this;
9578 }
9579
9580 /**
9581 * @return $this
9582 */
9583 public function setDataLocked(bool $locked): static
9584 {
9585 if ($this->locked) {
9586 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9587 }
9588
9589 $this->dataLocked = $locked;
9590
9591 return $this;
9592 }
9593
9594 /**
9595 * @return $this
9596 */
9597 public function setFormFactory(FormFactoryInterface $formFactory): static
9598 {
9599 if ($this->locked) {
9600 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9601 }
9602
9603 $this->formFactory = $formFactory;
9604
9605 return $this;
9606 }
9607
9608 /**
9609 * @return $this
9610 */
9611 public function setAction(string $action): static
9612 {
9613 if ($this->locked) {
9614 throw new BadMethodCallException('The config builder cannot be modified anymore.');
9615 }
9616
9617 $this->action = $action;
9618
9619 return $this;
9620 }
9621
9622 /**
9623 * @return $this
9624 */
9625 public function setMethod(string $method): static
9626 {
9627 if ($this->locked) {
9628 throw new BadMethodCallException('The config builder cannot be modified anymore.');
9629 }
9630
9631 $this->method = strtoupper($method);
9632
9633 return $this;
9634 }
9635
9636 /**
9637 * @return $this
9638 */
9639 public function setRequestHandler(RequestHandlerInterface $requestHandler): static
9640 {
9641 if ($this->locked) {
9642 throw new BadMethodCallException('The config builder cannot be modified anymore.');
9643 }
9644
9645 $this->requestHandler = $requestHandler;
9646
9647 return $this;
9648 }
9649
9650 /**
9651 * @return $this
9652 */
9653 public function setAutoInitialize(bool $initialize): static
9654 {
9655 if ($this->locked) {
9656 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9657 }
9658
9659 $this->autoInitialize = $initialize;
9660
9661 return $this;
9662 }
9663
9664 public function getFormConfig(): FormConfigInterface
9665 {
9666 if ($this->locked) {
9667 throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
9668 }
9669
9670 // This method should be idempotent, so clone the builder
9671 $config = clone $this;
9672 $config->locked = true;
9673
9674 return $config;
9675 }
9676
9677 /**
9678 * @return $this
9679 */
9680 public function setIsEmptyCallback(?callable $isEmptyCallback): static
9681 {
9682 $this->isEmptyCallback = null === $isEmptyCallback ? null : $isEmptyCallback(...);
9683
9684 return $this;
9685 }
9686
9687 /**
9688 * Validates whether the given variable is a valid form name.
9689 *
9690 * @throws InvalidArgumentException if the name contains invalid characters
9691 *
9692 * @internal
9693 */
9694 final public static function validateName(?string $name): void
9695 {
9696 if (!self::isValidName($name)) {
9697 throw new InvalidArgumentException(\sprintf('The name "%s" contains illegal characters. Names should start with a letter, digit or underscore and only contain letters, digits, numbers, underscores ("_"), hyphens ("-") and colons (":").', $name));
9698 }
9699 }
9700
9701 /**
9702 * Returns whether the given variable contains a valid form name.
9703 *
9704 * A name is accepted if it
9705 *
9706 * * is empty
9707 * * starts with a letter, digit or underscore
9708 * * contains only letters, digits, numbers, underscores ("_"),
9709 * hyphens ("-") and colons (":")
9710 */
9711 final public static function isValidName(?string $name): bool
9712 {
9713 return '' === $name || null === $name || preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name);
9714 }
9715}
9716
9717------------------------------------------------------------------------------------------------------------------------
9718 ./FormRegistry.php
9719------------------------------------------------------------------------------------------------------------------------
9720<?php
9721
9722/*
9723 * This file is part of the Symfony package.
9724 *
9725 * (c) Fabien Potencier <fabien@symfony.com>
9726 *
9727 * For the full copyright and license information, please view the LICENSE
9728 * file that was distributed with this source code.
9729 */
9730
9731namespace Symfony\Component\Form;
9732
9733use Symfony\Component\Form\Exception\ExceptionInterface;
9734use Symfony\Component\Form\Exception\InvalidArgumentException;
9735use Symfony\Component\Form\Exception\LogicException;
9736use Symfony\Component\Form\Exception\UnexpectedTypeException;
9737
9738/**
9739 * The central registry of the Form component.
9740 *
9741 * @author Bernhard Schussek <bschussek@gmail.com>
9742 */
9743class FormRegistry implements FormRegistryInterface
9744{
9745 /**
9746 * @var FormExtensionInterface[]
9747 */
9748 private array $extensions = [];
9749
9750 /**
9751 * @var ResolvedFormTypeInterface[]
9752 */
9753 private array $types = [];
9754
9755 private FormTypeGuesserInterface|false|null $guesser = false;
9756 private array $checkedTypes = [];
9757
9758 /**
9759 * @param FormExtensionInterface[] $extensions
9760 *
9761 * @throws UnexpectedTypeException if any extension does not implement FormExtensionInterface
9762 */
9763 public function __construct(
9764 array $extensions,
9765 private ResolvedFormTypeFactoryInterface $resolvedTypeFactory,
9766 ) {
9767 foreach ($extensions as $extension) {
9768 if (!$extension instanceof FormExtensionInterface) {
9769 throw new UnexpectedTypeException($extension, FormExtensionInterface::class);
9770 }
9771 }
9772
9773 $this->extensions = $extensions;
9774 }
9775
9776 public function getType(string $name): ResolvedFormTypeInterface
9777 {
9778 if (!isset($this->types[$name])) {
9779 $type = null;
9780
9781 foreach ($this->extensions as $extension) {
9782 if ($extension->hasType($name)) {
9783 $type = $extension->getType($name);
9784 break;
9785 }
9786 }
9787
9788 if (!$type) {
9789 // Support fully-qualified class names
9790 if (!class_exists($name)) {
9791 throw new InvalidArgumentException(\sprintf('Could not load type "%s": class does not exist.', $name));
9792 }
9793 if (!is_subclass_of($name, FormTypeInterface::class)) {
9794 throw new InvalidArgumentException(\sprintf('Could not load type "%s": class does not implement "Symfony\Component\Form\FormTypeInterface".', $name));
9795 }
9796
9797 $type = new $name();
9798 }
9799
9800 $this->types[$name] = $this->resolveType($type);
9801 }
9802
9803 return $this->types[$name];
9804 }
9805
9806 /**
9807 * Wraps a type into a ResolvedFormTypeInterface implementation and connects it with its parent type.
9808 */
9809 private function resolveType(FormTypeInterface $type): ResolvedFormTypeInterface
9810 {
9811 $parentType = $type->getParent();
9812 $fqcn = $type::class;
9813
9814 if (isset($this->checkedTypes[$fqcn])) {
9815 $types = implode(' > ', array_merge(array_keys($this->checkedTypes), [$fqcn]));
9816 throw new LogicException(\sprintf('Circular reference detected for form type "%s" (%s).', $fqcn, $types));
9817 }
9818
9819 $this->checkedTypes[$fqcn] = true;
9820
9821 $typeExtensions = [];
9822 try {
9823 foreach ($this->extensions as $extension) {
9824 $typeExtensions[] = $extension->getTypeExtensions($fqcn);
9825 }
9826
9827 return $this->resolvedTypeFactory->createResolvedType(
9828 $type,
9829 array_merge([], ...$typeExtensions),
9830 $parentType ? $this->getType($parentType) : null
9831 );
9832 } finally {
9833 unset($this->checkedTypes[$fqcn]);
9834 }
9835 }
9836
9837 public function hasType(string $name): bool
9838 {
9839 if (isset($this->types[$name])) {
9840 return true;
9841 }
9842
9843 try {
9844 $this->getType($name);
9845 } catch (ExceptionInterface) {
9846 return false;
9847 }
9848
9849 return true;
9850 }
9851
9852 public function getTypeGuesser(): ?FormTypeGuesserInterface
9853 {
9854 if (false === $this->guesser) {
9855 $guessers = [];
9856
9857 foreach ($this->extensions as $extension) {
9858 $guesser = $extension->getTypeGuesser();
9859
9860 if ($guesser) {
9861 $guessers[] = $guesser;
9862 }
9863 }
9864
9865 $this->guesser = $guessers ? new FormTypeGuesserChain($guessers) : null;
9866 }
9867
9868 return $this->guesser;
9869 }
9870
9871 public function getExtensions(): array
9872 {
9873 return $this->extensions;
9874 }
9875}
9876
9877------------------------------------------------------------------------------------------------------------------------
9878 ./FormView.php
9879------------------------------------------------------------------------------------------------------------------------
9880<?php
9881
9882/*
9883 * This file is part of the Symfony package.
9884 *
9885 * (c) Fabien Potencier <fabien@symfony.com>
9886 *
9887 * For the full copyright and license information, please view the LICENSE
9888 * file that was distributed with this source code.
9889 */
9890
9891namespace Symfony\Component\Form;
9892
9893use Symfony\Component\Form\Exception\BadMethodCallException;
9894
9895/**
9896 * @author Bernhard Schussek <bschussek@gmail.com>
9897 *
9898 * @implements \ArrayAccess<int|string, FormView>
9899 * @implements \IteratorAggregate<int|string, FormView>
9900 */
9901class FormView implements \ArrayAccess, \IteratorAggregate, \Countable
9902{
9903 /**
9904 * The variables assigned to this view.
9905 */
9906 public array $vars = [
9907 'value' => null,
9908 'attr' => [],
9909 ];
9910
9911 /**
9912 * The child views.
9913 *
9914 * @var array<int|string, FormView>
9915 */
9916 public array $children = [];
9917
9918 /**
9919 * Is the form attached to this renderer rendered?
9920 *
9921 * Rendering happens when either the widget or the row method was called.
9922 * Row implicitly includes widget, however certain rendering mechanisms
9923 * have to skip widget rendering when a row is rendered.
9924 */
9925 private bool $rendered = false;
9926
9927 private bool $methodRendered = false;
9928
9929 /**
9930 * @param FormView|null $parent The parent view
9931 */
9932 public function __construct(
9933 public ?self $parent = null,
9934 ) {
9935 }
9936
9937 /**
9938 * Returns whether the view was already rendered.
9939 */
9940 public function isRendered(): bool
9941 {
9942 if (true === $this->rendered || 0 === \count($this->children)) {
9943 return $this->rendered;
9944 }
9945
9946 foreach ($this->children as $child) {
9947 if (!$child->isRendered()) {
9948 return false;
9949 }
9950 }
9951
9952 return $this->rendered = true;
9953 }
9954
9955 /**
9956 * Marks the view as rendered.
9957 *
9958 * @return $this
9959 */
9960 public function setRendered(): static
9961 {
9962 $this->rendered = true;
9963
9964 return $this;
9965 }
9966
9967 public function isMethodRendered(): bool
9968 {
9969 return $this->methodRendered;
9970 }
9971
9972 public function setMethodRendered(): void
9973 {
9974 $this->methodRendered = true;
9975 }
9976
9977 /**
9978 * Returns a child by name (implements \ArrayAccess).
9979 *
9980 * @param int|string $name The child name
9981 */
9982 public function offsetGet(mixed $name): self
9983 {
9984 return $this->children[$name];
9985 }
9986
9987 /**
9988 * Returns whether the given child exists (implements \ArrayAccess).
9989 *
9990 * @param int|string $name The child name
9991 */
9992 public function offsetExists(mixed $name): bool
9993 {
9994 return isset($this->children[$name]);
9995 }
9996
9997 /**
9998 * Implements \ArrayAccess.
9999 *
10000 * @throws BadMethodCallException always as setting a child by name is not allowed
10001 */
10002 public function offsetSet(mixed $name, mixed $value): void
10003 {
10004 throw new BadMethodCallException('Not supported.');
10005 }
10006
10007 /**
10008 * Removes a child (implements \ArrayAccess).
10009 *
10010 * @param int|string $name The child name
10011 */
10012 public function offsetUnset(mixed $name): void
10013 {
10014 unset($this->children[$name]);
10015 }
10016
10017 /**
10018 * Returns an iterator to iterate over children (implements \IteratorAggregate).
10019 *
10020 * @return \ArrayIterator<int|string, FormView>
10021 */
10022 public function getIterator(): \ArrayIterator
10023 {
10024 return new \ArrayIterator($this->children);
10025 }
10026
10027 public function count(): int
10028 {
10029 return \count($this->children);
10030 }
10031}
10032
10033------------------------------------------------------------------------------------------------------------------------
10034 ./ButtonBuilder.php
10035------------------------------------------------------------------------------------------------------------------------
10036<?php
10037
10038/*
10039 * This file is part of the Symfony package.
10040 *
10041 * (c) Fabien Potencier <fabien@symfony.com>
10042 *
10043 * For the full copyright and license information, please view the LICENSE
10044 * file that was distributed with this source code.
10045 */
10046
10047namespace Symfony\Component\Form;
10048
10049use Symfony\Component\EventDispatcher\EventSubscriberInterface;
10050use Symfony\Component\Form\Exception\BadMethodCallException;
10051use Symfony\Component\Form\Exception\InvalidArgumentException;
10052use Symfony\Component\PropertyAccess\PropertyPathInterface;
10053
10054/**
10055 * A builder for {@link Button} instances.
10056 *
10057 * @author Bernhard Schussek <bschussek@gmail.com>
10058 *
10059 * @implements \IteratorAggregate<string, FormBuilderInterface>
10060 */
10061class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
10062{
10063 protected bool $locked = false;
10064
10065 private bool $disabled = false;
10066 private ResolvedFormTypeInterface $type;
10067 private string $name;
10068 private array $attributes = [];
10069
10070 /**
10071 * @throws InvalidArgumentException if the name is empty
10072 */
10073 public function __construct(
10074 ?string $name,
10075 private array $options = [],
10076 ) {
10077 if ('' === $name || null === $name) {
10078 throw new InvalidArgumentException('Buttons cannot have empty names.');
10079 }
10080
10081 $this->name = $name;
10082
10083 FormConfigBuilder::validateName($name);
10084 }
10085
10086 /**
10087 * Unsupported method.
10088 *
10089 * @throws BadMethodCallException
10090 */
10091 public function add(string|FormBuilderInterface $child, ?string $type = null, array $options = []): never
10092 {
10093 throw new BadMethodCallException('Buttons cannot have children.');
10094 }
10095
10096 /**
10097 * Unsupported method.
10098 *
10099 * @throws BadMethodCallException
10100 */
10101 public function create(string $name, ?string $type = null, array $options = []): never
10102 {
10103 throw new BadMethodCallException('Buttons cannot have children.');
10104 }
10105
10106 /**
10107 * Unsupported method.
10108 *
10109 * @throws BadMethodCallException
10110 */
10111 public function get(string $name): never
10112 {
10113 throw new BadMethodCallException('Buttons cannot have children.');
10114 }
10115
10116 /**
10117 * Unsupported method.
10118 *
10119 * @throws BadMethodCallException
10120 */
10121 public function remove(string $name): never
10122 {
10123 throw new BadMethodCallException('Buttons cannot have children.');
10124 }
10125
10126 /**
10127 * Unsupported method.
10128 */
10129 public function has(string $name): bool
10130 {
10131 return false;
10132 }
10133
10134 /**
10135 * Returns the children.
10136 */
10137 public function all(): array
10138 {
10139 return [];
10140 }
10141
10142 /**
10143 * Creates the button.
10144 */
10145 public function getForm(): Button
10146 {
10147 return new Button($this->getFormConfig());
10148 }
10149
10150 /**
10151 * Unsupported method.
10152 *
10153 * @throws BadMethodCallException
10154 */
10155 public function addEventListener(string $eventName, callable $listener, int $priority = 0): never
10156 {
10157 throw new BadMethodCallException('Buttons do not support event listeners.');
10158 }
10159
10160 /**
10161 * Unsupported method.
10162 *
10163 * @throws BadMethodCallException
10164 */
10165 public function addEventSubscriber(EventSubscriberInterface $subscriber): never
10166 {
10167 throw new BadMethodCallException('Buttons do not support event subscribers.');
10168 }
10169
10170 /**
10171 * Unsupported method.
10172 *
10173 * @throws BadMethodCallException
10174 */
10175 public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): never
10176 {
10177 throw new BadMethodCallException('Buttons do not support data transformers.');
10178 }
10179
10180 /**
10181 * Unsupported method.
10182 *
10183 * @throws BadMethodCallException
10184 */
10185 public function resetViewTransformers(): never
10186 {
10187 throw new BadMethodCallException('Buttons do not support data transformers.');
10188 }
10189
10190 /**
10191 * Unsupported method.
10192 *
10193 * @throws BadMethodCallException
10194 */
10195 public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): never
10196 {
10197 throw new BadMethodCallException('Buttons do not support data transformers.');
10198 }
10199
10200 /**
10201 * Unsupported method.
10202 *
10203 * @throws BadMethodCallException
10204 */
10205 public function resetModelTransformers(): never
10206 {
10207 throw new BadMethodCallException('Buttons do not support data transformers.');
10208 }
10209
10210 /**
10211 * @return $this
10212 */
10213 public function setAttribute(string $name, mixed $value): static
10214 {
10215 $this->attributes[$name] = $value;
10216
10217 return $this;
10218 }
10219
10220 /**
10221 * @return $this
10222 */
10223 public function setAttributes(array $attributes): static
10224 {
10225 $this->attributes = $attributes;
10226
10227 return $this;
10228 }
10229
10230 /**
10231 * Unsupported method.
10232 *
10233 * @throws BadMethodCallException
10234 */
10235 public function setDataMapper(?DataMapperInterface $dataMapper): never
10236 {
10237 throw new BadMethodCallException('Buttons do not support data mappers.');
10238 }
10239
10240 /**
10241 * Set whether the button is disabled.
10242 *
10243 * @return $this
10244 */
10245 public function setDisabled(bool $disabled): static
10246 {
10247 $this->disabled = $disabled;
10248
10249 return $this;
10250 }
10251
10252 /**
10253 * Unsupported method.
10254 *
10255 * @throws BadMethodCallException
10256 */
10257 public function setEmptyData(mixed $emptyData): never
10258 {
10259 throw new BadMethodCallException('Buttons do not support empty data.');
10260 }
10261
10262 /**
10263 * Unsupported method.
10264 *
10265 * @throws BadMethodCallException
10266 */
10267 public function setErrorBubbling(bool $errorBubbling): never
10268 {
10269 throw new BadMethodCallException('Buttons do not support error bubbling.');
10270 }
10271
10272 /**
10273 * Unsupported method.
10274 *
10275 * @throws BadMethodCallException
10276 */
10277 public function setRequired(bool $required): never
10278 {
10279 throw new BadMethodCallException('Buttons cannot be required.');
10280 }
10281
10282 /**
10283 * Unsupported method.
10284 *
10285 * @throws BadMethodCallException
10286 */
10287 public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): never
10288 {
10289 throw new BadMethodCallException('Buttons do not support property paths.');
10290 }
10291
10292 /**
10293 * Unsupported method.
10294 *
10295 * @throws BadMethodCallException
10296 */
10297 public function setMapped(bool $mapped): never
10298 {
10299 throw new BadMethodCallException('Buttons do not support data mapping.');
10300 }
10301
10302 /**
10303 * Unsupported method.
10304 *
10305 * @throws BadMethodCallException
10306 */
10307 public function setByReference(bool $byReference): never
10308 {
10309 throw new BadMethodCallException('Buttons do not support data mapping.');
10310 }
10311
10312 /**
10313 * Unsupported method.
10314 *
10315 * @throws BadMethodCallException
10316 */
10317 public function setCompound(bool $compound): never
10318 {
10319 throw new BadMethodCallException('Buttons cannot be compound.');
10320 }
10321
10322 /**
10323 * Sets the type of the button.
10324 *
10325 * @return $this
10326 */
10327 public function setType(ResolvedFormTypeInterface $type): static
10328 {
10329 $this->type = $type;
10330
10331 return $this;
10332 }
10333
10334 /**
10335 * Unsupported method.
10336 *
10337 * @throws BadMethodCallException
10338 */
10339 public function setData(mixed $data): never
10340 {
10341 throw new BadMethodCallException('Buttons do not support data.');
10342 }
10343
10344 /**
10345 * Unsupported method.
10346 *
10347 * @throws BadMethodCallException
10348 */
10349 public function setDataLocked(bool $locked): never
10350 {
10351 throw new BadMethodCallException('Buttons do not support data locking.');
10352 }
10353
10354 /**
10355 * Unsupported method.
10356 *
10357 * @throws BadMethodCallException
10358 */
10359 public function setFormFactory(FormFactoryInterface $formFactory): never
10360 {
10361 throw new BadMethodCallException('Buttons do not support form factories.');
10362 }
10363
10364 /**
10365 * Unsupported method.
10366 *
10367 * @throws BadMethodCallException
10368 */
10369 public function setAction(string $action): never
10370 {
10371 throw new BadMethodCallException('Buttons do not support actions.');
10372 }
10373
10374 /**
10375 * Unsupported method.
10376 *
10377 * @throws BadMethodCallException
10378 */
10379 public function setMethod(string $method): never
10380 {
10381 throw new BadMethodCallException('Buttons do not support methods.');
10382 }
10383
10384 /**
10385 * Unsupported method.
10386 *
10387 * @throws BadMethodCallException
10388 */
10389 public function setRequestHandler(RequestHandlerInterface $requestHandler): never
10390 {
10391 throw new BadMethodCallException('Buttons do not support request handlers.');
10392 }
10393
10394 /**
10395 * Unsupported method.
10396 *
10397 * @return $this
10398 *
10399 * @throws BadMethodCallException
10400 */
10401 public function setAutoInitialize(bool $initialize): static
10402 {
10403 if (true === $initialize) {
10404 throw new BadMethodCallException('Buttons do not support automatic initialization.');
10405 }
10406
10407 return $this;
10408 }
10409
10410 /**
10411 * Unsupported method.
10412 *
10413 * @throws BadMethodCallException
10414 */
10415 public function setInheritData(bool $inheritData): never
10416 {
10417 throw new BadMethodCallException('Buttons do not support data inheritance.');
10418 }
10419
10420 /**
10421 * Builds and returns the button configuration.
10422 */
10423 public function getFormConfig(): FormConfigInterface
10424 {
10425 // This method should be idempotent, so clone the builder
10426 $config = clone $this;
10427 $config->locked = true;
10428
10429 return $config;
10430 }
10431
10432 /**
10433 * Unsupported method.
10434 *
10435 * @throws BadMethodCallException
10436 */
10437 public function setIsEmptyCallback(?callable $isEmptyCallback): never
10438 {
10439 throw new BadMethodCallException('Buttons do not support "is empty" callback.');
10440 }
10441
10442 /**
10443 * Unsupported method.
10444 *
10445 * @throws BadMethodCallException
10446 */
10447 public function getEventDispatcher(): never
10448 {
10449 throw new BadMethodCallException('Buttons do not support event dispatching.');
10450 }
10451
10452 public function getName(): string
10453 {
10454 return $this->name;
10455 }
10456
10457 /**
10458 * Unsupported method.
10459 */
10460 public function getPropertyPath(): ?PropertyPathInterface
10461 {
10462 return null;
10463 }
10464
10465 /**
10466 * Unsupported method.
10467 */
10468 public function getMapped(): bool
10469 {
10470 return false;
10471 }
10472
10473 /**
10474 * Unsupported method.
10475 */
10476 public function getByReference(): bool
10477 {
10478 return false;
10479 }
10480
10481 /**
10482 * Unsupported method.
10483 */
10484 public function getCompound(): bool
10485 {
10486 return false;
10487 }
10488
10489 /**
10490 * Returns the form type used to construct the button.
10491 */
10492 public function getType(): ResolvedFormTypeInterface
10493 {
10494 return $this->type;
10495 }
10496
10497 /**
10498 * Unsupported method.
10499 */
10500 public function getViewTransformers(): array
10501 {
10502 return [];
10503 }
10504
10505 /**
10506 * Unsupported method.
10507 */
10508 public function getModelTransformers(): array
10509 {
10510 return [];
10511 }
10512
10513 /**
10514 * Unsupported method.
10515 */
10516 public function getDataMapper(): ?DataMapperInterface
10517 {
10518 return null;
10519 }
10520
10521 /**
10522 * Unsupported method.
10523 */
10524 public function getRequired(): bool
10525 {
10526 return false;
10527 }
10528
10529 /**
10530 * Returns whether the button is disabled.
10531 */
10532 public function getDisabled(): bool
10533 {
10534 return $this->disabled;
10535 }
10536
10537 /**
10538 * Unsupported method.
10539 */
10540 public function getErrorBubbling(): bool
10541 {
10542 return false;
10543 }
10544
10545 /**
10546 * Unsupported method.
10547 */
10548 public function getEmptyData(): mixed
10549 {
10550 return null;
10551 }
10552
10553 /**
10554 * Returns additional attributes of the button.
10555 */
10556 public function getAttributes(): array
10557 {
10558 return $this->attributes;
10559 }
10560
10561 /**
10562 * Returns whether the attribute with the given name exists.
10563 */
10564 public function hasAttribute(string $name): bool
10565 {
10566 return \array_key_exists($name, $this->attributes);
10567 }
10568
10569 /**
10570 * Returns the value of the given attribute.
10571 */
10572 public function getAttribute(string $name, mixed $default = null): mixed
10573 {
10574 return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
10575 }
10576
10577 /**
10578 * Unsupported method.
10579 */
10580 public function getData(): mixed
10581 {
10582 return null;
10583 }
10584
10585 /**
10586 * Unsupported method.
10587 */
10588 public function getDataClass(): ?string
10589 {
10590 return null;
10591 }
10592
10593 /**
10594 * Unsupported method.
10595 */
10596 public function getDataLocked(): bool
10597 {
10598 return false;
10599 }
10600
10601 /**
10602 * Unsupported method.
10603 */
10604 public function getFormFactory(): never
10605 {
10606 throw new BadMethodCallException('Buttons do not support adding children.');
10607 }
10608
10609 /**
10610 * Unsupported method.
10611 *
10612 * @throws BadMethodCallException
10613 */
10614 public function getAction(): never
10615 {
10616 throw new BadMethodCallException('Buttons do not support actions.');
10617 }
10618
10619 /**
10620 * Unsupported method.
10621 *
10622 * @throws BadMethodCallException
10623 */
10624 public function getMethod(): never
10625 {
10626 throw new BadMethodCallException('Buttons do not support methods.');
10627 }
10628
10629 /**
10630 * Unsupported method.
10631 *
10632 * @throws BadMethodCallException
10633 */
10634 public function getRequestHandler(): never
10635 {
10636 throw new BadMethodCallException('Buttons do not support request handlers.');
10637 }
10638
10639 /**
10640 * Unsupported method.
10641 */
10642 public function getAutoInitialize(): bool
10643 {
10644 return false;
10645 }
10646
10647 /**
10648 * Unsupported method.
10649 */
10650 public function getInheritData(): bool
10651 {
10652 return false;
10653 }
10654
10655 /**
10656 * Returns all options passed during the construction of the button.
10657 */
10658 public function getOptions(): array
10659 {
10660 return $this->options;
10661 }
10662
10663 /**
10664 * Returns whether a specific option exists.
10665 */
10666 public function hasOption(string $name): bool
10667 {
10668 return \array_key_exists($name, $this->options);
10669 }
10670
10671 /**
10672 * Returns the value of a specific option.
10673 */
10674 public function getOption(string $name, mixed $default = null): mixed
10675 {
10676 return \array_key_exists($name, $this->options) ? $this->options[$name] : $default;
10677 }
10678
10679 /**
10680 * Unsupported method.
10681 *
10682 * @throws BadMethodCallException
10683 */
10684 public function getIsEmptyCallback(): never
10685 {
10686 throw new BadMethodCallException('Buttons do not support "is empty" callback.');
10687 }
10688
10689 /**
10690 * Unsupported method.
10691 */
10692 public function count(): int
10693 {
10694 return 0;
10695 }
10696
10697 /**
10698 * Unsupported method.
10699 */
10700 public function getIterator(): \EmptyIterator
10701 {
10702 return new \EmptyIterator();
10703 }
10704}
10705
10706------------------------------------------------------------------------------------------------------------------------
10707 ./DataAccessorInterface.php
10708------------------------------------------------------------------------------------------------------------------------
10709<?php
10710
10711/*
10712 * This file is part of the Symfony package.
10713 *
10714 * (c) Fabien Potencier <fabien@symfony.com>
10715 *
10716 * For the full copyright and license information, please view the LICENSE
10717 * file that was distributed with this source code.
10718 */
10719
10720namespace Symfony\Component\Form;
10721
10722/**
10723 * Writes and reads values to/from an object or array bound to a form.
10724 *
10725 * @author Yonel Ceruto <yonelceruto@gmail.com>
10726 */
10727interface DataAccessorInterface
10728{
10729 /**
10730 * Returns the value at the end of the property of the object graph.
10731 *
10732 * @throws Exception\AccessException If unable to read from the given form data
10733 */
10734 public function getValue(object|array $viewData, FormInterface $form): mixed;
10735
10736 /**
10737 * Sets the value at the end of the property of the object graph.
10738 *
10739 * @throws Exception\AccessException If unable to write the given value
10740 */
10741 public function setValue(object|array &$viewData, mixed $value, FormInterface $form): void;
10742
10743 /**
10744 * Returns whether a value can be read from an object graph.
10745 *
10746 * Whenever this method returns true, {@link getValue()} is guaranteed not
10747 * to throw an exception when called with the same arguments.
10748 */
10749 public function isReadable(object|array $viewData, FormInterface $form): bool;
10750
10751 /**
10752 * Returns whether a value can be written at a given object graph.
10753 *
10754 * Whenever this method returns true, {@link setValue()} is guaranteed not
10755 * to throw an exception when called with the same arguments.
10756 */
10757 public function isWritable(object|array $viewData, FormInterface $form): bool;
10758}
10759
10760------------------------------------------------------------------------------------------------------------------------
10761 ./ReversedTransformer.php
10762------------------------------------------------------------------------------------------------------------------------
10763<?php
10764
10765/*
10766 * This file is part of the Symfony package.
10767 *
10768 * (c) Fabien Potencier <fabien@symfony.com>
10769 *
10770 * For the full copyright and license information, please view the LICENSE
10771 * file that was distributed with this source code.
10772 */
10773
10774namespace Symfony\Component\Form;
10775
10776/**
10777 * Reverses a transformer.
10778 *
10779 * When the transform() method is called, the reversed transformer's
10780 * reverseTransform() method is called and vice versa.
10781 *
10782 * @author Bernhard Schussek <bschussek@gmail.com>
10783 */
10784class ReversedTransformer implements DataTransformerInterface
10785{
10786 public function __construct(
10787 protected DataTransformerInterface $reversedTransformer,
10788 ) {
10789 }
10790
10791 public function transform(mixed $value): mixed
10792 {
10793 return $this->reversedTransformer->reverseTransform($value);
10794 }
10795
10796 public function reverseTransform(mixed $value): mixed
10797 {
10798 return $this->reversedTransformer->transform($value);
10799 }
10800}
10801
10802------------------------------------------------------------------------------------------------------------------------
10803 ./FormRendererEngineInterface.php
10804------------------------------------------------------------------------------------------------------------------------
10805<?php
10806
10807/*
10808 * This file is part of the Symfony package.
10809 *
10810 * (c) Fabien Potencier <fabien@symfony.com>
10811 *
10812 * For the full copyright and license information, please view the LICENSE
10813 * file that was distributed with this source code.
10814 */
10815
10816namespace Symfony\Component\Form;
10817
10818/**
10819 * Adapter for rendering form templates with a specific templating engine.
10820 *
10821 * @author Bernhard Schussek <bschussek@gmail.com>
10822 */
10823interface FormRendererEngineInterface
10824{
10825 /**
10826 * Sets the theme(s) to be used for rendering a view and its children.
10827 *
10828 * @param FormView $view The view to assign the theme(s) to
10829 * @param mixed $themes The theme(s). The type of these themes
10830 * is open to the implementation.
10831 */
10832 public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void;
10833
10834 /**
10835 * Returns the resource for a block name.
10836 *
10837 * The resource is first searched in the themes attached to $view, then
10838 * in the themes of its parent view and so on, until a resource was found.
10839 *
10840 * The type of the resource is decided by the implementation. The resource
10841 * is later passed to {@link renderBlock()} by the rendering algorithm.
10842 *
10843 * @param FormView $view The view for determining the used themes.
10844 * First the themes attached directly to the
10845 * view with {@link setTheme()} are considered,
10846 * then the ones of its parent etc.
10847 *
10848 * @return mixed the renderer resource or false, if none was found
10849 */
10850 public function getResourceForBlockName(FormView $view, string $blockName): mixed;
10851
10852 /**
10853 * Returns the resource for a block hierarchy.
10854 *
10855 * A block hierarchy is an array which starts with the root of the hierarchy
10856 * and continues with the child of that root, the child of that child etc.
10857 * The following is an example for a block hierarchy:
10858 *
10859 * form_widget
10860 * text_widget
10861 * url_widget
10862 *
10863 * In this example, "url_widget" is the most specific block, while the other
10864 * blocks are its ancestors in the hierarchy.
10865 *
10866 * The second parameter $hierarchyLevel determines the level of the hierarchy
10867 * that should be rendered. For example, if $hierarchyLevel is 2 for the
10868 * above hierarchy, the engine will first look for the block "url_widget",
10869 * then, if that does not exist, for the block "text_widget" etc.
10870 *
10871 * The type of the resource is decided by the implementation. The resource
10872 * is later passed to {@link renderBlock()} by the rendering algorithm.
10873 *
10874 * @param FormView $view The view for determining the used themes.
10875 * First the themes attached directly to
10876 * the view with {@link setTheme()} are
10877 * considered, then the ones of its parent etc.
10878 * @param string[] $blockNameHierarchy The block name hierarchy, with the root block
10879 * at the beginning
10880 * @param int $hierarchyLevel The level in the hierarchy at which to start
10881 * looking. Level 0 indicates the root block, i.e.
10882 * the first element of $blockNameHierarchy.
10883 *
10884 * @return mixed The renderer resource or false, if none was found
10885 */
10886 public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): mixed;
10887
10888 /**
10889 * Returns the hierarchy level at which a resource can be found.
10890 *
10891 * A block hierarchy is an array which starts with the root of the hierarchy
10892 * and continues with the child of that root, the child of that child etc.
10893 * The following is an example for a block hierarchy:
10894 *
10895 * form_widget
10896 * text_widget
10897 * url_widget
10898 *
10899 * The second parameter $hierarchyLevel determines the level of the hierarchy
10900 * that should be rendered.
10901 *
10902 * If we call this method with the hierarchy level 2, the engine will first
10903 * look for a resource for block "url_widget". If such a resource exists,
10904 * the method returns 2. Otherwise it tries to find a resource for block
10905 * "text_widget" (at level 1) and, again, returns 1 if a resource was found.
10906 * The method continues to look for resources until the root level was
10907 * reached and nothing was found. In this case false is returned.
10908 *
10909 * The type of the resource is decided by the implementation. The resource
10910 * is later passed to {@link renderBlock()} by the rendering algorithm.
10911 *
10912 * @param FormView $view The view for determining the used themes.
10913 * First the themes attached directly to
10914 * the view with {@link setTheme()} are
10915 * considered, then the ones of its parent etc.
10916 * @param string[] $blockNameHierarchy The block name hierarchy, with the root block
10917 * at the beginning
10918 * @param int $hierarchyLevel The level in the hierarchy at which to start
10919 * looking. Level 0 indicates the root block, i.e.
10920 * the first element of $blockNameHierarchy.
10921 */
10922 public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): int|false;
10923
10924 /**
10925 * Renders a block in the given renderer resource.
10926 *
10927 * The resource can be obtained by calling {@link getResourceForBlock()}
10928 * or {@link getResourceForBlockHierarchy()}. The type of the resource is
10929 * decided by the implementation.
10930 *
10931 * @param FormView $view The view to render
10932 * @param mixed $resource The renderer resource
10933 * @param array $variables The variables to pass to the template
10934 */
10935 public function renderBlock(FormView $view, mixed $resource, string $blockName, array $variables = []): string;
10936}
10937
10938------------------------------------------------------------------------------------------------------------------------
10939 ./PreloadedExtension.php
10940------------------------------------------------------------------------------------------------------------------------
10941<?php
10942
10943/*
10944 * This file is part of the Symfony package.
10945 *
10946 * (c) Fabien Potencier <fabien@symfony.com>
10947 *
10948 * For the full copyright and license information, please view the LICENSE
10949 * file that was distributed with this source code.
10950 */
10951
10952namespace Symfony\Component\Form;
10953
10954use Symfony\Component\Form\Exception\InvalidArgumentException;
10955
10956/**
10957 * A form extension with preloaded types, type extensions and type guessers.
10958 *
10959 * @author Bernhard Schussek <bschussek@gmail.com>
10960 */
10961class PreloadedExtension implements FormExtensionInterface
10962{
10963 private array $types = [];
10964
10965 /**
10966 * Creates a new preloaded extension.
10967 *
10968 * @param FormTypeInterface[] $types The types that the extension should support
10969 * @param FormTypeExtensionInterface[][] $typeExtensions The type extensions that the extension should support
10970 */
10971 public function __construct(
10972 array $types,
10973 private array $typeExtensions,
10974 private ?FormTypeGuesserInterface $typeGuesser = null,
10975 ) {
10976 foreach ($types as $type) {
10977 $this->types[$type::class] = $type;
10978 }
10979 }
10980
10981 public function getType(string $name): FormTypeInterface
10982 {
10983 if (!isset($this->types[$name])) {
10984 throw new InvalidArgumentException(\sprintf('The type "%s" cannot be loaded by this extension.', $name));
10985 }
10986
10987 return $this->types[$name];
10988 }
10989
10990 public function hasType(string $name): bool
10991 {
10992 return isset($this->types[$name]);
10993 }
10994
10995 public function getTypeExtensions(string $name): array
10996 {
10997 return $this->typeExtensions[$name]
10998 ?? [];
10999 }
11000
11001 public function hasTypeExtensions(string $name): bool
11002 {
11003 return !empty($this->typeExtensions[$name]);
11004 }
11005
11006 public function getTypeGuesser(): ?FormTypeGuesserInterface
11007 {
11008 return $this->typeGuesser;
11009 }
11010}
11011
11012------------------------------------------------------------------------------------------------------------------------
11013 ./DataTransformerInterface.php
11014------------------------------------------------------------------------------------------------------------------------
11015<?php
11016
11017/*
11018 * This file is part of the Symfony package.
11019 *
11020 * (c) Fabien Potencier <fabien@symfony.com>
11021 *
11022 * For the full copyright and license information, please view the LICENSE
11023 * file that was distributed with this source code.
11024 */
11025
11026namespace Symfony\Component\Form;
11027
11028use Symfony\Component\Form\Exception\TransformationFailedException;
11029
11030/**
11031 * Transforms a value between different representations.
11032 *
11033 * @author Bernhard Schussek <bschussek@gmail.com>
11034 *
11035 * @template TValue
11036 * @template TTransformedValue
11037 */
11038interface DataTransformerInterface
11039{
11040 /**
11041 * Transforms a value from the original representation to a transformed representation.
11042 *
11043 * This method is called when the form field is initialized with its default data, on
11044 * two occasions for two types of transformers:
11045 *
11046 * 1. Model transformers which normalize the model data.
11047 * This is mainly useful when the same form type (the same configuration)
11048 * has to handle different kind of underlying data, e.g The DateType can
11049 * deal with strings or \DateTime objects as input.
11050 *
11051 * 2. View transformers which adapt the normalized data to the view format.
11052 * a/ When the form is simple, the value returned by convention is used
11053 * directly in the view and thus can only be a string or an array. In
11054 * this case the data class should be null.
11055 *
11056 * b/ When the form is compound the returned value should be an array or
11057 * an object to be mapped to the children. Each property of the compound
11058 * data will be used as model data by each child and will be transformed
11059 * too. In this case data class should be the class of the object, or null
11060 * when it is an array.
11061 *
11062 * All transformers are called in a configured order from model data to view value.
11063 * At the end of this chain the view data will be validated against the data class
11064 * setting.
11065 *
11066 * This method must be able to deal with empty values. Usually this will
11067 * be NULL, but depending on your implementation other empty values are
11068 * possible as well (such as empty strings). The reasoning behind this is
11069 * that data transformers must be chainable. If the transform() method
11070 * of the first data transformer outputs NULL, the second must be able to
11071 * process that value.
11072 *
11073 * @param TValue|null $value The value in the original representation
11074 *
11075 * @return TTransformedValue|null
11076 *
11077 * @throws TransformationFailedException when the transformation fails
11078 */
11079 public function transform(mixed $value): mixed;
11080
11081 /**
11082 * Transforms a value from the transformed representation to its original
11083 * representation.
11084 *
11085 * This method is called when {@link Form::submit()} is called to transform the requests tainted data
11086 * into an acceptable format.
11087 *
11088 * The same transformers are called in the reverse order so the responsibility is to
11089 * return one of the types that would be expected as input of transform().
11090 *
11091 * This method must be able to deal with empty values. Usually this will
11092 * be an empty string, but depending on your implementation other empty
11093 * values are possible as well (such as NULL). The reasoning behind
11094 * this is that value transformers must be chainable. If the
11095 * reverseTransform() method of the first value transformer outputs an
11096 * empty string, the second value transformer must be able to process that
11097 * value.
11098 *
11099 * By convention, reverseTransform() should return NULL if an empty string
11100 * is passed.
11101 *
11102 * @param TTransformedValue|null $value The value in the transformed representation
11103 *
11104 * @return TValue|null
11105 *
11106 * @throws TransformationFailedException when the transformation fails
11107 */
11108 public function reverseTransform(mixed $value): mixed;
11109}
11110
11111------------------------------------------------------------------------------------------------------------------------
11112 ./SubmitButtonTypeInterface.php
11113------------------------------------------------------------------------------------------------------------------------
11114<?php
11115
11116/*
11117 * This file is part of the Symfony package.
11118 *
11119 * (c) Fabien Potencier <fabien@symfony.com>
11120 *
11121 * For the full copyright and license information, please view the LICENSE
11122 * file that was distributed with this source code.
11123 */
11124
11125namespace Symfony\Component\Form;
11126
11127/**
11128 * A type that should be converted into a {@link SubmitButton} instance.
11129 *
11130 * @author Bernhard Schussek <bschussek@gmail.com>
11131 */
11132interface SubmitButtonTypeInterface extends FormTypeInterface
11133{
11134}
11135
11136------------------------------------------------------------------------------------------------------------------------
11137 ./ResolvedFormTypeFactoryInterface.php
11138------------------------------------------------------------------------------------------------------------------------
11139<?php
11140
11141/*
11142 * This file is part of the Symfony package.
11143 *
11144 * (c) Fabien Potencier <fabien@symfony.com>
11145 *
11146 * For the full copyright and license information, please view the LICENSE
11147 * file that was distributed with this source code.
11148 */
11149
11150namespace Symfony\Component\Form;
11151
11152/**
11153 * Creates ResolvedFormTypeInterface instances.
11154 *
11155 * This interface allows you to use your custom ResolvedFormTypeInterface
11156 * implementation, within which you can customize the concrete FormBuilderInterface
11157 * implementations or FormView subclasses that are used by the framework.
11158 *
11159 * @author Bernhard Schussek <bschussek@gmail.com>
11160 */
11161interface ResolvedFormTypeFactoryInterface
11162{
11163 /**
11164 * Resolves a form type.
11165 *
11166 * @param FormTypeExtensionInterface[] $typeExtensions
11167 *
11168 * @throws Exception\UnexpectedTypeException if the types parent {@link FormTypeInterface::getParent()} is not a string
11169 * @throws Exception\InvalidArgumentException if the types parent cannot be retrieved from any extension
11170 */
11171 public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface;
11172}
11173
11174------------------------------------------------------------------------------------------------------------------------
11175 ./Console/Helper/DescriptorHelper.php
11176------------------------------------------------------------------------------------------------------------------------
11177<?php
11178
11179/*
11180 * This file is part of the Symfony package.
11181 *
11182 * (c) Fabien Potencier <fabien@symfony.com>
11183 *
11184 * For the full copyright and license information, please view the LICENSE
11185 * file that was distributed with this source code.
11186 */
11187
11188namespace Symfony\Component\Form\Console\Helper;
11189
11190use Symfony\Component\Console\Helper\DescriptorHelper as BaseDescriptorHelper;
11191use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
11192use Symfony\Component\Form\Console\Descriptor\JsonDescriptor;
11193use Symfony\Component\Form\Console\Descriptor\TextDescriptor;
11194
11195/**
11196 * @author Yonel Ceruto <yonelceruto@gmail.com>
11197 *
11198 * @internal
11199 */
11200class DescriptorHelper extends BaseDescriptorHelper
11201{
11202 public function __construct(?FileLinkFormatter $fileLinkFormatter = null)
11203 {
11204 $this
11205 ->register('txt', new TextDescriptor($fileLinkFormatter))
11206 ->register('json', new JsonDescriptor())
11207 ;
11208 }
11209}
11210
11211------------------------------------------------------------------------------------------------------------------------
11212 ./Console/Descriptor/JsonDescriptor.php
11213------------------------------------------------------------------------------------------------------------------------
11214<?php
11215
11216/*
11217 * This file is part of the Symfony package.
11218 *
11219 * (c) Fabien Potencier <fabien@symfony.com>
11220 *
11221 * For the full copyright and license information, please view the LICENSE
11222 * file that was distributed with this source code.
11223 */
11224
11225namespace Symfony\Component\Form\Console\Descriptor;
11226
11227use Symfony\Component\Form\ResolvedFormTypeInterface;
11228use Symfony\Component\OptionsResolver\OptionsResolver;
11229
11230/**
11231 * @author Yonel Ceruto <yonelceruto@gmail.com>
11232 *
11233 * @internal
11234 */
11235class JsonDescriptor extends Descriptor
11236{
11237 protected function describeDefaults(array $options): void
11238 {
11239 $data['builtin_form_types'] = $options['core_types'];
11240 $data['service_form_types'] = $options['service_types'];
11241 if (!$options['show_deprecated']) {
11242 $data['type_extensions'] = $options['extensions'];
11243 $data['type_guessers'] = $options['guessers'];
11244 }
11245
11246 $this->writeData($data, $options);
11247 }
11248
11249 protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void
11250 {
11251 $this->collectOptions($resolvedFormType);
11252
11253 if ($options['show_deprecated']) {
11254 $this->filterOptionsByDeprecated($resolvedFormType);
11255 }
11256
11257 $formOptions = [
11258 'own' => $this->ownOptions,
11259 'overridden' => $this->overriddenOptions,
11260 'parent' => $this->parentOptions,
11261 'extension' => $this->extensionOptions,
11262 'required' => $this->requiredOptions,
11263 ];
11264 $this->sortOptions($formOptions);
11265
11266 $data = [
11267 'class' => $resolvedFormType->getInnerType()::class,
11268 'block_prefix' => $resolvedFormType->getInnerType()->getBlockPrefix(),
11269 'options' => $formOptions,
11270 'parent_types' => $this->parents,
11271 'type_extensions' => $this->extensions,
11272 ];
11273
11274 $this->writeData($data, $options);
11275 }
11276
11277 protected function describeOption(OptionsResolver $optionsResolver, array $options): void
11278 {
11279 $definition = $this->getOptionDefinition($optionsResolver, $options['option']);
11280
11281 $map = [];
11282 if ($definition['deprecated']) {
11283 $map['deprecated'] = 'deprecated';
11284 if (\is_string($definition['deprecationMessage'])) {
11285 $map['deprecation_message'] = 'deprecationMessage';
11286 }
11287 }
11288 $map += [
11289 'info' => 'info',
11290 'required' => 'required',
11291 'default' => 'default',
11292 'allowed_types' => 'allowedTypes',
11293 'allowed_values' => 'allowedValues',
11294 ];
11295 foreach ($map as $label => $name) {
11296 if (\array_key_exists($name, $definition)) {
11297 $data[$label] = $definition[$name];
11298
11299 if ('default' === $name) {
11300 $data['is_lazy'] = isset($definition['lazy']);
11301 }
11302 }
11303 }
11304 $data['has_normalizer'] = isset($definition['normalizers']);
11305
11306 $this->writeData($data, $options);
11307 }
11308
11309 private function writeData(array $data, array $options): void
11310 {
11311 $flags = $options['json_encoding'] ?? 0;
11312
11313 $this->output->write(json_encode($data, $flags | \JSON_PRETTY_PRINT)."\n");
11314 }
11315
11316 private function sortOptions(array &$options): void
11317 {
11318 foreach ($options as &$opts) {
11319 $sorted = false;
11320 foreach ($opts as &$opt) {
11321 if (\is_array($opt)) {
11322 sort($opt);
11323 $sorted = true;
11324 }
11325 }
11326 if (!$sorted) {
11327 sort($opts);
11328 }
11329 }
11330 }
11331}
11332
11333------------------------------------------------------------------------------------------------------------------------
11334 ./Console/Descriptor/TextDescriptor.php
11335------------------------------------------------------------------------------------------------------------------------
11336<?php
11337
11338/*
11339 * This file is part of the Symfony package.
11340 *
11341 * (c) Fabien Potencier <fabien@symfony.com>
11342 *
11343 * For the full copyright and license information, please view the LICENSE
11344 * file that was distributed with this source code.
11345 */
11346
11347namespace Symfony\Component\Form\Console\Descriptor;
11348
11349use Symfony\Component\Console\Helper\Dumper;
11350use Symfony\Component\Console\Helper\TableSeparator;
11351use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
11352use Symfony\Component\Form\ResolvedFormTypeInterface;
11353use Symfony\Component\OptionsResolver\OptionsResolver;
11354
11355/**
11356 * @author Yonel Ceruto <yonelceruto@gmail.com>
11357 *
11358 * @internal
11359 */
11360class TextDescriptor extends Descriptor
11361{
11362 public function __construct(
11363 private readonly ?FileLinkFormatter $fileLinkFormatter = null,
11364 ) {
11365 }
11366
11367 protected function describeDefaults(array $options): void
11368 {
11369 if ($options['core_types']) {
11370 $this->output->section('Built-in form types (Symfony\Component\Form\Extension\Core\Type)');
11371 $shortClassNames = array_map(fn ($fqcn) => $this->formatClassLink($fqcn, \array_slice(explode('\\', $fqcn), -1)[0]), $options['core_types']);
11372 for ($i = 0, $loopsMax = \count($shortClassNames); $i * 5 < $loopsMax; ++$i) {
11373 $this->output->writeln(' '.implode(', ', \array_slice($shortClassNames, $i * 5, 5)));
11374 }
11375 }
11376
11377 if ($options['service_types']) {
11378 $this->output->section('Service form types');
11379 $this->output->listing(array_map($this->formatClassLink(...), $options['service_types']));
11380 }
11381
11382 if (!$options['show_deprecated']) {
11383 if ($options['extensions']) {
11384 $this->output->section('Type extensions');
11385 $this->output->listing(array_map($this->formatClassLink(...), $options['extensions']));
11386 }
11387
11388 if ($options['guessers']) {
11389 $this->output->section('Type guessers');
11390 $this->output->listing(array_map($this->formatClassLink(...), $options['guessers']));
11391 }
11392 }
11393 }
11394
11395 protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void
11396 {
11397 $this->collectOptions($resolvedFormType);
11398
11399 if ($options['show_deprecated']) {
11400 $this->filterOptionsByDeprecated($resolvedFormType);
11401 }
11402
11403 $formOptions = $this->normalizeAndSortOptionsColumns(array_filter([
11404 'own' => $this->ownOptions,
11405 'overridden' => $this->overriddenOptions,
11406 'parent' => $this->parentOptions,
11407 'extension' => $this->extensionOptions,
11408 ]));
11409
11410 // setting headers and column order
11411 $tableHeaders = array_intersect_key([
11412 'own' => 'Options',
11413 'overridden' => 'Overridden options',
11414 'parent' => 'Parent options',
11415 'extension' => 'Extension options',
11416 ], $formOptions);
11417
11418 $this->output->title(\sprintf('%s (Block prefix: "%s")', $resolvedFormType->getInnerType()::class, $resolvedFormType->getInnerType()->getBlockPrefix()));
11419
11420 if ($formOptions) {
11421 $this->output->table($tableHeaders, $this->buildTableRows($tableHeaders, $formOptions));
11422 }
11423
11424 if ($this->parents) {
11425 $this->output->section('Parent types');
11426 $this->output->listing(array_map($this->formatClassLink(...), $this->parents));
11427 }
11428
11429 if ($this->extensions) {
11430 $this->output->section('Type extensions');
11431 $this->output->listing(array_map($this->formatClassLink(...), $this->extensions));
11432 }
11433 }
11434
11435 protected function describeOption(OptionsResolver $optionsResolver, array $options): void
11436 {
11437 $definition = $this->getOptionDefinition($optionsResolver, $options['option']);
11438
11439 $dump = new Dumper($this->output);
11440 $map = [];
11441 if ($definition['deprecated']) {
11442 $map = [
11443 'Deprecated' => 'deprecated',
11444 'Deprecation package' => 'deprecationPackage',
11445 'Deprecation version' => 'deprecationVersion',
11446 'Deprecation message' => 'deprecationMessage',
11447 ];
11448 }
11449 $map += [
11450 'Info' => 'info',
11451 'Required' => 'required',
11452 'Default' => 'default',
11453 'Allowed types' => 'allowedTypes',
11454 'Allowed values' => 'allowedValues',
11455 'Normalizers' => 'normalizers',
11456 ];
11457 $rows = [];
11458 foreach ($map as $label => $name) {
11459 $value = \array_key_exists($name, $definition) ? $dump($definition[$name]) : '-';
11460 if ('default' === $name && isset($definition['lazy'])) {
11461 $value = "Value: $value\n\nClosure(s): ".$dump($definition['lazy']);
11462 }
11463
11464 $rows[] = ["<info>$label</info>", $value];
11465 $rows[] = new TableSeparator();
11466 }
11467 array_pop($rows);
11468
11469 $this->output->title(\sprintf('%s (%s)', $options['type']::class, $options['option']));
11470 $this->output->table([], $rows);
11471 }
11472
11473 private function buildTableRows(array $headers, array $options): array
11474 {
11475 $tableRows = [];
11476 $count = \count(max($options));
11477 for ($i = 0; $i < $count; ++$i) {
11478 $cells = [];
11479 foreach (array_keys($headers) as $group) {
11480 $option = $options[$group][$i] ?? null;
11481 if (\is_string($option) && \in_array($option, $this->requiredOptions, true)) {
11482 $option .= ' <info>(required)</info>';
11483 }
11484 $cells[] = $option;
11485 }
11486 $tableRows[] = $cells;
11487 }
11488
11489 return $tableRows;
11490 }
11491
11492 private function normalizeAndSortOptionsColumns(array $options): array
11493 {
11494 foreach ($options as $group => $opts) {
11495 $sorted = false;
11496 foreach ($opts as $class => $opt) {
11497 if (\is_string($class)) {
11498 unset($options[$group][$class]);
11499 }
11500
11501 if (!\is_array($opt) || 0 === \count($opt)) {
11502 continue;
11503 }
11504
11505 if (!$sorted) {
11506 $options[$group] = [];
11507 } else {
11508 $options[$group][] = null;
11509 }
11510 $options[$group][] = \sprintf('<info>%s</info>', (new \ReflectionClass($class))->getShortName());
11511 $options[$group][] = new TableSeparator();
11512
11513 sort($opt);
11514 $sorted = true;
11515 $options[$group] = array_merge($options[$group], $opt);
11516 }
11517
11518 if (!$sorted) {
11519 sort($options[$group]);
11520 }
11521 }
11522
11523 return $options;
11524 }
11525
11526 private function formatClassLink(string $class, ?string $text = null): string
11527 {
11528 $text ??= $class;
11529
11530 if ('' === $fileLink = $this->getFileLink($class)) {
11531 return $text;
11532 }
11533
11534 return \sprintf('<href=%s>%s</>', $fileLink, $text);
11535 }
11536
11537 private function getFileLink(string $class): string
11538 {
11539 if (null === $this->fileLinkFormatter) {
11540 return '';
11541 }
11542
11543 try {
11544 $r = new \ReflectionClass($class);
11545 } catch (\ReflectionException) {
11546 return '';
11547 }
11548
11549 return (string) $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine());
11550 }
11551}
11552
11553------------------------------------------------------------------------------------------------------------------------
11554 ./Console/Descriptor/Descriptor.php
11555------------------------------------------------------------------------------------------------------------------------
11556<?php
11557
11558/*
11559 * This file is part of the Symfony package.
11560 *
11561 * (c) Fabien Potencier <fabien@symfony.com>
11562 *
11563 * For the full copyright and license information, please view the LICENSE
11564 * file that was distributed with this source code.
11565 */
11566
11567namespace Symfony\Component\Form\Console\Descriptor;
11568
11569use Symfony\Component\Console\Descriptor\DescriptorInterface;
11570use Symfony\Component\Console\Input\ArrayInput;
11571use Symfony\Component\Console\Output\OutputInterface;
11572use Symfony\Component\Console\Style\OutputStyle;
11573use Symfony\Component\Console\Style\SymfonyStyle;
11574use Symfony\Component\Form\ResolvedFormTypeInterface;
11575use Symfony\Component\Form\Util\OptionsResolverWrapper;
11576use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector;
11577use Symfony\Component\OptionsResolver\Exception\NoConfigurationException;
11578use Symfony\Component\OptionsResolver\OptionsResolver;
11579
11580/**
11581 * @author Yonel Ceruto <yonelceruto@gmail.com>
11582 *
11583 * @internal
11584 */
11585abstract class Descriptor implements DescriptorInterface
11586{
11587 protected OutputStyle $output;
11588 protected array $ownOptions = [];
11589 protected array $overriddenOptions = [];
11590 protected array $parentOptions = [];
11591 protected array $extensionOptions = [];
11592 protected array $requiredOptions = [];
11593 protected array $parents = [];
11594 protected array $extensions = [];
11595
11596 public function describe(OutputInterface $output, ?object $object, array $options = []): void
11597 {
11598 $this->output = $output instanceof OutputStyle ? $output : new SymfonyStyle(new ArrayInput([]), $output);
11599
11600 match (true) {
11601 null === $object => $this->describeDefaults($options),
11602 $object instanceof ResolvedFormTypeInterface => $this->describeResolvedFormType($object, $options),
11603 $object instanceof OptionsResolver => $this->describeOption($object, $options),
11604 default => throw new \InvalidArgumentException(\sprintf('Object of type "%s" is not describable.', get_debug_type($object))),
11605 };
11606 }
11607
11608 abstract protected function describeDefaults(array $options): void;
11609
11610 abstract protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void;
11611
11612 abstract protected function describeOption(OptionsResolver $optionsResolver, array $options): void;
11613
11614 protected function collectOptions(ResolvedFormTypeInterface $type): void
11615 {
11616 $this->parents = [];
11617 $this->extensions = [];
11618
11619 if (null !== $type->getParent()) {
11620 $optionsResolver = clone $this->getParentOptionsResolver($type->getParent());
11621 } else {
11622 $optionsResolver = new OptionsResolver();
11623 }
11624
11625 $type->getInnerType()->configureOptions($ownOptionsResolver = new OptionsResolverWrapper());
11626 $this->ownOptions = array_diff($ownOptionsResolver->getDefinedOptions(), $optionsResolver->getDefinedOptions());
11627 $overriddenOptions = array_intersect(array_merge($ownOptionsResolver->getDefinedOptions(), $ownOptionsResolver->getUndefinedOptions()), $optionsResolver->getDefinedOptions());
11628
11629 $this->parentOptions = [];
11630 foreach ($this->parents as $class => $parentOptions) {
11631 $this->overriddenOptions[$class] = array_intersect($overriddenOptions, $parentOptions);
11632 $this->parentOptions[$class] = array_diff($parentOptions, $overriddenOptions);
11633 }
11634
11635 $type->getInnerType()->configureOptions($optionsResolver);
11636 $this->collectTypeExtensionsOptions($type, $optionsResolver);
11637 $this->extensionOptions = [];
11638 foreach ($this->extensions as $class => $extensionOptions) {
11639 $this->overriddenOptions[$class] = array_intersect($overriddenOptions, $extensionOptions);
11640 $this->extensionOptions[$class] = array_diff($extensionOptions, $overriddenOptions);
11641 }
11642
11643 $this->overriddenOptions = array_filter($this->overriddenOptions);
11644 $this->parentOptions = array_filter($this->parentOptions);
11645 $this->extensionOptions = array_filter($this->extensionOptions);
11646 $this->requiredOptions = $optionsResolver->getRequiredOptions();
11647
11648 $this->parents = array_keys($this->parents);
11649 $this->extensions = array_keys($this->extensions);
11650 }
11651
11652 protected function getOptionDefinition(OptionsResolver $optionsResolver, string $option): array
11653 {
11654 $definition = [];
11655
11656 if ($info = $optionsResolver->getInfo($option)) {
11657 $definition = [
11658 'info' => $info,
11659 ];
11660 }
11661
11662 $definition += [
11663 'required' => $optionsResolver->isRequired($option),
11664 'deprecated' => $optionsResolver->isDeprecated($option),
11665 ];
11666
11667 $introspector = new OptionsResolverIntrospector($optionsResolver);
11668
11669 $map = [
11670 'default' => 'getDefault',
11671 'lazy' => 'getLazyClosures',
11672 'allowedTypes' => 'getAllowedTypes',
11673 'allowedValues' => 'getAllowedValues',
11674 'normalizers' => 'getNormalizers',
11675 'deprecation' => 'getDeprecation',
11676 ];
11677
11678 foreach ($map as $key => $method) {
11679 try {
11680 $definition[$key] = $introspector->{$method}($option);
11681 } catch (NoConfigurationException) {
11682 // noop
11683 }
11684 }
11685
11686 if (isset($definition['deprecation']['message']) && \is_string($definition['deprecation']['message'])) {
11687 $definition['deprecationMessage'] = strtr($definition['deprecation']['message'], ['%name%' => $option]);
11688 $definition['deprecationPackage'] = $definition['deprecation']['package'];
11689 $definition['deprecationVersion'] = $definition['deprecation']['version'];
11690 }
11691
11692 return $definition;
11693 }
11694
11695 protected function filterOptionsByDeprecated(ResolvedFormTypeInterface $type): void
11696 {
11697 $deprecatedOptions = [];
11698 $resolver = $type->getOptionsResolver();
11699 foreach ($resolver->getDefinedOptions() as $option) {
11700 if ($resolver->isDeprecated($option)) {
11701 $deprecatedOptions[] = $option;
11702 }
11703 }
11704
11705 $filterByDeprecated = static function (array $options) use ($deprecatedOptions) {
11706 foreach ($options as $class => $opts) {
11707 if ($deprecated = array_intersect($deprecatedOptions, $opts)) {
11708 $options[$class] = $deprecated;
11709 } else {
11710 unset($options[$class]);
11711 }
11712 }
11713
11714 return $options;
11715 };
11716
11717 $this->ownOptions = array_intersect($deprecatedOptions, $this->ownOptions);
11718 $this->overriddenOptions = $filterByDeprecated($this->overriddenOptions);
11719 $this->parentOptions = $filterByDeprecated($this->parentOptions);
11720 $this->extensionOptions = $filterByDeprecated($this->extensionOptions);
11721 }
11722
11723 private function getParentOptionsResolver(ResolvedFormTypeInterface $type): OptionsResolver
11724 {
11725 $this->parents[$class = $type->getInnerType()::class] = [];
11726
11727 if (null !== $type->getParent()) {
11728 $optionsResolver = clone $this->getParentOptionsResolver($type->getParent());
11729 } else {
11730 $optionsResolver = new OptionsResolver();
11731 }
11732
11733 $inheritedOptions = $optionsResolver->getDefinedOptions();
11734 $type->getInnerType()->configureOptions($optionsResolver);
11735 $this->parents[$class] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions);
11736
11737 $this->collectTypeExtensionsOptions($type, $optionsResolver);
11738
11739 return $optionsResolver;
11740 }
11741
11742 private function collectTypeExtensionsOptions(ResolvedFormTypeInterface $type, OptionsResolver $optionsResolver): void
11743 {
11744 foreach ($type->getTypeExtensions() as $extension) {
11745 $inheritedOptions = $optionsResolver->getDefinedOptions();
11746 $extension->configureOptions($optionsResolver);
11747 $this->extensions[$extension::class] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions);
11748 }
11749 }
11750}
11751
11752------------------------------------------------------------------------------------------------------------------------
11753 ./ClickableInterface.php
11754------------------------------------------------------------------------------------------------------------------------
11755<?php
11756
11757/*
11758 * This file is part of the Symfony package.
11759 *
11760 * (c) Fabien Potencier <fabien@symfony.com>
11761 *
11762 * For the full copyright and license information, please view the LICENSE
11763 * file that was distributed with this source code.
11764 */
11765
11766namespace Symfony\Component\Form;
11767
11768/**
11769 * A clickable form element.
11770 *
11771 * @author Bernhard Schussek <bschussek@gmail.com>
11772 */
11773interface ClickableInterface
11774{
11775 /**
11776 * Returns whether this element was clicked.
11777 */
11778 public function isClicked(): bool;
11779}
11780
11781------------------------------------------------------------------------------------------------------------------------
11782 ./ButtonTypeInterface.php
11783------------------------------------------------------------------------------------------------------------------------
11784<?php
11785
11786/*
11787 * This file is part of the Symfony package.
11788 *
11789 * (c) Fabien Potencier <fabien@symfony.com>
11790 *
11791 * For the full copyright and license information, please view the LICENSE
11792 * file that was distributed with this source code.
11793 */
11794
11795namespace Symfony\Component\Form;
11796
11797/**
11798 * A type that should be converted into a {@link Button} instance.
11799 *
11800 * @author Bernhard Schussek <bschussek@gmail.com>
11801 */
11802interface ButtonTypeInterface extends FormTypeInterface
11803{
11804}
11805
11806------------------------------------------------------------------------------------------------------------------------
11807 ./Extension/DependencyInjection/DependencyInjectionExtension.php
11808------------------------------------------------------------------------------------------------------------------------
11809<?php
11810
11811/*
11812 * This file is part of the Symfony package.
11813 *
11814 * (c) Fabien Potencier <fabien@symfony.com>
11815 *
11816 * For the full copyright and license information, please view the LICENSE
11817 * file that was distributed with this source code.
11818 */
11819
11820namespace Symfony\Component\Form\Extension\DependencyInjection;
11821
11822use Psr\Container\ContainerInterface;
11823use Symfony\Component\Form\Exception\InvalidArgumentException;
11824use Symfony\Component\Form\FormExtensionInterface;
11825use Symfony\Component\Form\FormTypeExtensionInterface;
11826use Symfony\Component\Form\FormTypeGuesserChain;
11827use Symfony\Component\Form\FormTypeGuesserInterface;
11828use Symfony\Component\Form\FormTypeInterface;
11829
11830class DependencyInjectionExtension implements FormExtensionInterface
11831{
11832 private ?FormTypeGuesserChain $guesser = null;
11833 private bool $guesserLoaded = false;
11834
11835 /**
11836 * @param array<string, iterable<FormTypeExtensionInterface>> $typeExtensionServices
11837 */
11838 public function __construct(
11839 private ContainerInterface $typeContainer,
11840 private array $typeExtensionServices,
11841 private iterable $guesserServices,
11842 ) {
11843 }
11844
11845 public function getType(string $name): FormTypeInterface
11846 {
11847 if (!$this->typeContainer->has($name)) {
11848 throw new InvalidArgumentException(\sprintf('The field type "%s" is not registered in the service container.', $name));
11849 }
11850
11851 return $this->typeContainer->get($name);
11852 }
11853
11854 public function hasType(string $name): bool
11855 {
11856 return $this->typeContainer->has($name);
11857 }
11858
11859 public function getTypeExtensions(string $name): array
11860 {
11861 $extensions = [];
11862
11863 if (isset($this->typeExtensionServices[$name])) {
11864 foreach ($this->typeExtensionServices[$name] as $extension) {
11865 $extensions[] = $extension;
11866
11867 $extendedTypes = [];
11868 foreach ($extension::getExtendedTypes() as $extendedType) {
11869 $extendedTypes[] = $extendedType;
11870 }
11871
11872 // validate the result of getExtendedTypes() to ensure it is consistent with the service definition
11873 if (!\in_array($name, $extendedTypes, true)) {
11874 throw new InvalidArgumentException(\sprintf('The extended type "%s" specified for the type extension class "%s" does not match any of the actual extended types (["%s"]).', $name, $extension::class, implode('", "', $extendedTypes)));
11875 }
11876 }
11877 }
11878
11879 return $extensions;
11880 }
11881
11882 public function hasTypeExtensions(string $name): bool
11883 {
11884 return isset($this->typeExtensionServices[$name]);
11885 }
11886
11887 public function getTypeGuesser(): ?FormTypeGuesserInterface
11888 {
11889 if (!$this->guesserLoaded) {
11890 $this->guesserLoaded = true;
11891 $guessers = [];
11892
11893 foreach ($this->guesserServices as $serviceId => $service) {
11894 $guessers[] = $service;
11895 }
11896
11897 if ($guessers) {
11898 $this->guesser = new FormTypeGuesserChain($guessers);
11899 }
11900 }
11901
11902 return $this->guesser;
11903 }
11904}
11905
11906------------------------------------------------------------------------------------------------------------------------
11907 ./Extension/DataCollector/DataCollectorExtension.php
11908------------------------------------------------------------------------------------------------------------------------
11909<?php
11910
11911/*
11912 * This file is part of the Symfony package.
11913 *
11914 * (c) Fabien Potencier <fabien@symfony.com>
11915 *
11916 * For the full copyright and license information, please view the LICENSE
11917 * file that was distributed with this source code.
11918 */
11919
11920namespace Symfony\Component\Form\Extension\DataCollector;
11921
11922use Symfony\Component\Form\AbstractExtension;
11923
11924/**
11925 * Extension for collecting data of the forms on a page.
11926 *
11927 * @author Robert Schönthal <robert.schoenthal@gmail.com>
11928 * @author Bernhard Schussek <bschussek@gmail.com>
11929 */
11930class DataCollectorExtension extends AbstractExtension
11931{
11932 public function __construct(
11933 private FormDataCollectorInterface $dataCollector,
11934 ) {
11935 }
11936
11937 protected function loadTypeExtensions(): array
11938 {
11939 return [
11940 new Type\DataCollectorTypeExtension($this->dataCollector),
11941 ];
11942 }
11943}
11944
11945------------------------------------------------------------------------------------------------------------------------
11946 ./Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php
11947------------------------------------------------------------------------------------------------------------------------
11948<?php
11949
11950/*
11951 * This file is part of the Symfony package.
11952 *
11953 * (c) Fabien Potencier <fabien@symfony.com>
11954 *
11955 * For the full copyright and license information, please view the LICENSE
11956 * file that was distributed with this source code.
11957 */
11958
11959namespace Symfony\Component\Form\Extension\DataCollector\Proxy;
11960
11961use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
11962use Symfony\Component\Form\FormTypeInterface;
11963use Symfony\Component\Form\ResolvedFormTypeFactoryInterface;
11964use Symfony\Component\Form\ResolvedFormTypeInterface;
11965
11966/**
11967 * Proxy that wraps resolved types into {@link ResolvedTypeDataCollectorProxy}
11968 * instances.
11969 *
11970 * @author Bernhard Schussek <bschussek@gmail.com>
11971 */
11972class ResolvedTypeFactoryDataCollectorProxy implements ResolvedFormTypeFactoryInterface
11973{
11974 public function __construct(
11975 private ResolvedFormTypeFactoryInterface $proxiedFactory,
11976 private FormDataCollectorInterface $dataCollector,
11977 ) {
11978 }
11979
11980 public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface
11981 {
11982 return new ResolvedTypeDataCollectorProxy(
11983 $this->proxiedFactory->createResolvedType($type, $typeExtensions, $parent),
11984 $this->dataCollector
11985 );
11986 }
11987}
11988
11989------------------------------------------------------------------------------------------------------------------------
11990 ./Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php
11991------------------------------------------------------------------------------------------------------------------------
11992<?php
11993
11994/*
11995 * This file is part of the Symfony package.
11996 *
11997 * (c) Fabien Potencier <fabien@symfony.com>
11998 *
11999 * For the full copyright and license information, please view the LICENSE
12000 * file that was distributed with this source code.
12001 */
12002
12003namespace Symfony\Component\Form\Extension\DataCollector\Proxy;
12004
12005use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
12006use Symfony\Component\Form\FormBuilderInterface;
12007use Symfony\Component\Form\FormFactoryInterface;
12008use Symfony\Component\Form\FormInterface;
12009use Symfony\Component\Form\FormTypeInterface;
12010use Symfony\Component\Form\FormView;
12011use Symfony\Component\Form\ResolvedFormTypeInterface;
12012use Symfony\Component\OptionsResolver\OptionsResolver;
12013
12014/**
12015 * Proxy that invokes a data collector when creating a form and its view.
12016 *
12017 * @author Bernhard Schussek <bschussek@gmail.com>
12018 */
12019class ResolvedTypeDataCollectorProxy implements ResolvedFormTypeInterface
12020{
12021 public function __construct(
12022 private ResolvedFormTypeInterface $proxiedType,
12023 private FormDataCollectorInterface $dataCollector,
12024 ) {
12025 }
12026
12027 public function getBlockPrefix(): string
12028 {
12029 return $this->proxiedType->getBlockPrefix();
12030 }
12031
12032 public function getParent(): ?ResolvedFormTypeInterface
12033 {
12034 return $this->proxiedType->getParent();
12035 }
12036
12037 public function getInnerType(): FormTypeInterface
12038 {
12039 return $this->proxiedType->getInnerType();
12040 }
12041
12042 public function getTypeExtensions(): array
12043 {
12044 return $this->proxiedType->getTypeExtensions();
12045 }
12046
12047 public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface
12048 {
12049 $builder = $this->proxiedType->createBuilder($factory, $name, $options);
12050
12051 $builder->setAttribute('data_collector/passed_options', $options);
12052 $builder->setType($this);
12053
12054 return $builder;
12055 }
12056
12057 public function createView(FormInterface $form, ?FormView $parent = null): FormView
12058 {
12059 return $this->proxiedType->createView($form, $parent);
12060 }
12061
12062 public function buildForm(FormBuilderInterface $builder, array $options): void
12063 {
12064 $this->proxiedType->buildForm($builder, $options);
12065 }
12066
12067 public function buildView(FormView $view, FormInterface $form, array $options): void
12068 {
12069 $this->proxiedType->buildView($view, $form, $options);
12070 }
12071
12072 public function finishView(FormView $view, FormInterface $form, array $options): void
12073 {
12074 $this->proxiedType->finishView($view, $form, $options);
12075
12076 // Remember which view belongs to which form instance, so that we can
12077 // get the collected data for a view when its form instance is not
12078 // available (e.g. CSRF token)
12079 $this->dataCollector->associateFormWithView($form, $view);
12080
12081 // Since the CSRF token is only present in the FormView tree, we also
12082 // need to check the FormView tree instead of calling isRoot() on the
12083 // FormInterface tree
12084 if (null === $view->parent) {
12085 $this->dataCollector->collectViewVariables($view);
12086
12087 // Re-assemble data, in case FormView instances were added, for
12088 // which no FormInterface instances were present (e.g. CSRF token).
12089 // Since finishView() is called after finishing the views of all
12090 // children, we can safely assume that information has been
12091 // collected about the complete form tree.
12092 $this->dataCollector->buildFinalFormTree($form, $view);
12093 }
12094 }
12095
12096 public function getOptionsResolver(): OptionsResolver
12097 {
12098 return $this->proxiedType->getOptionsResolver();
12099 }
12100}
12101
12102------------------------------------------------------------------------------------------------------------------------
12103 ./Extension/DataCollector/Type/DataCollectorTypeExtension.php
12104------------------------------------------------------------------------------------------------------------------------
12105<?php
12106
12107/*
12108 * This file is part of the Symfony package.
12109 *
12110 * (c) Fabien Potencier <fabien@symfony.com>
12111 *
12112 * For the full copyright and license information, please view the LICENSE
12113 * file that was distributed with this source code.
12114 */
12115
12116namespace Symfony\Component\Form\Extension\DataCollector\Type;
12117
12118use Symfony\Component\Form\AbstractTypeExtension;
12119use Symfony\Component\Form\Extension\Core\Type\FormType;
12120use Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener;
12121use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
12122use Symfony\Component\Form\FormBuilderInterface;
12123
12124/**
12125 * Type extension for collecting data of a form with this type.
12126 *
12127 * @author Robert Schönthal <robert.schoenthal@gmail.com>
12128 * @author Bernhard Schussek <bschussek@gmail.com>
12129 */
12130class DataCollectorTypeExtension extends AbstractTypeExtension
12131{
12132 private DataCollectorListener $listener;
12133
12134 public function __construct(FormDataCollectorInterface $dataCollector)
12135 {
12136 $this->listener = new DataCollectorListener($dataCollector);
12137 }
12138
12139 public function buildForm(FormBuilderInterface $builder, array $options): void
12140 {
12141 $builder->addEventSubscriber($this->listener);
12142 }
12143
12144 public static function getExtendedTypes(): iterable
12145 {
12146 return [FormType::class];
12147 }
12148}
12149
12150------------------------------------------------------------------------------------------------------------------------
12151 ./Extension/DataCollector/EventListener/DataCollectorListener.php
12152------------------------------------------------------------------------------------------------------------------------
12153<?php
12154
12155/*
12156 * This file is part of the Symfony package.
12157 *
12158 * (c) Fabien Potencier <fabien@symfony.com>
12159 *
12160 * For the full copyright and license information, please view the LICENSE
12161 * file that was distributed with this source code.
12162 */
12163
12164namespace Symfony\Component\Form\Extension\DataCollector\EventListener;
12165
12166use Symfony\Component\EventDispatcher\EventSubscriberInterface;
12167use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
12168use Symfony\Component\Form\FormEvent;
12169use Symfony\Component\Form\FormEvents;
12170
12171/**
12172 * Listener that invokes a data collector for the {@link FormEvents::POST_SET_DATA}
12173 * and {@link FormEvents::POST_SUBMIT} events.
12174 *
12175 * @author Bernhard Schussek <bschussek@gmail.com>
12176 */
12177class DataCollectorListener implements EventSubscriberInterface
12178{
12179 public function __construct(
12180 private FormDataCollectorInterface $dataCollector,
12181 ) {
12182 }
12183
12184 public static function getSubscribedEvents(): array
12185 {
12186 return [
12187 // Low priority in order to be called as late as possible
12188 FormEvents::POST_SET_DATA => ['postSetData', -255],
12189 // Low priority in order to be called as late as possible
12190 FormEvents::POST_SUBMIT => ['postSubmit', -255],
12191 ];
12192 }
12193
12194 /**
12195 * Listener for the {@link FormEvents::POST_SET_DATA} event.
12196 */
12197 public function postSetData(FormEvent $event): void
12198 {
12199 if ($event->getForm()->isRoot()) {
12200 // Collect basic information about each form
12201 $this->dataCollector->collectConfiguration($event->getForm());
12202
12203 // Collect the default data
12204 $this->dataCollector->collectDefaultData($event->getForm());
12205 }
12206 }
12207
12208 /**
12209 * Listener for the {@link FormEvents::POST_SUBMIT} event.
12210 */
12211 public function postSubmit(FormEvent $event): void
12212 {
12213 if ($event->getForm()->isRoot()) {
12214 // Collect the submitted data of each form
12215 $this->dataCollector->collectSubmittedData($event->getForm());
12216
12217 // Assemble a form tree
12218 // This is done again after the view is built, but we need it here as the view is not always created.
12219 $this->dataCollector->buildPreliminaryFormTree($event->getForm());
12220 }
12221 }
12222}
12223
12224------------------------------------------------------------------------------------------------------------------------
12225 ./Extension/DataCollector/FormDataExtractor.php
12226------------------------------------------------------------------------------------------------------------------------
12227<?php
12228
12229/*
12230 * This file is part of the Symfony package.
12231 *
12232 * (c) Fabien Potencier <fabien@symfony.com>
12233 *
12234 * For the full copyright and license information, please view the LICENSE
12235 * file that was distributed with this source code.
12236 */
12237
12238namespace Symfony\Component\Form\Extension\DataCollector;
12239
12240use Symfony\Component\Form\FormInterface;
12241use Symfony\Component\Form\FormView;
12242use Symfony\Component\Validator\ConstraintViolationInterface;
12243
12244/**
12245 * Default implementation of {@link FormDataExtractorInterface}.
12246 *
12247 * @author Bernhard Schussek <bschussek@gmail.com>
12248 */
12249class FormDataExtractor implements FormDataExtractorInterface
12250{
12251 public function extractConfiguration(FormInterface $form): array
12252 {
12253 $data = [
12254 'id' => $this->buildId($form),
12255 'name' => $form->getName(),
12256 'type_class' => $form->getConfig()->getType()->getInnerType()::class,
12257 'synchronized' => $form->isSynchronized(),
12258 'passed_options' => [],
12259 'resolved_options' => [],
12260 ];
12261
12262 foreach ($form->getConfig()->getAttribute('data_collector/passed_options', []) as $option => $value) {
12263 $data['passed_options'][$option] = $value;
12264 }
12265
12266 foreach ($form->getConfig()->getOptions() as $option => $value) {
12267 $data['resolved_options'][$option] = $value;
12268 }
12269
12270 ksort($data['passed_options']);
12271 ksort($data['resolved_options']);
12272
12273 return $data;
12274 }
12275
12276 public function extractDefaultData(FormInterface $form): array
12277 {
12278 $data = [
12279 'default_data' => [
12280 'norm' => $form->getNormData(),
12281 ],
12282 'submitted_data' => [],
12283 ];
12284
12285 if ($form->getData() !== $form->getNormData()) {
12286 $data['default_data']['model'] = $form->getData();
12287 }
12288
12289 if ($form->getViewData() !== $form->getNormData()) {
12290 $data['default_data']['view'] = $form->getViewData();
12291 }
12292
12293 return $data;
12294 }
12295
12296 public function extractSubmittedData(FormInterface $form): array
12297 {
12298 $data = [
12299 'submitted_data' => [
12300 'norm' => $form->getNormData(),
12301 ],
12302 'errors' => [],
12303 ];
12304
12305 if ($form->getViewData() !== $form->getNormData()) {
12306 $data['submitted_data']['view'] = $form->getViewData();
12307 }
12308
12309 if ($form->getData() !== $form->getNormData()) {
12310 $data['submitted_data']['model'] = $form->getData();
12311 }
12312
12313 foreach ($form->getErrors() as $error) {
12314 $errorData = [
12315 'message' => $error->getMessage(),
12316 'origin' => \is_object($error->getOrigin())
12317 ? spl_object_hash($error->getOrigin())
12318 : null,
12319 'trace' => [],
12320 ];
12321
12322 $cause = $error->getCause();
12323
12324 while (null !== $cause) {
12325 if ($cause instanceof ConstraintViolationInterface) {
12326 $errorData['trace'][] = $cause;
12327 $cause = method_exists($cause, 'getCause') ? $cause->getCause() : null;
12328
12329 continue;
12330 }
12331
12332 $errorData['trace'][] = $cause;
12333 if ($cause instanceof \Exception) {
12334 $cause = $cause->getPrevious();
12335
12336 continue;
12337 }
12338
12339 break;
12340 }
12341
12342 $data['errors'][] = $errorData;
12343 }
12344
12345 $data['synchronized'] = $form->isSynchronized();
12346
12347 return $data;
12348 }
12349
12350 public function extractViewVariables(FormView $view): array
12351 {
12352 $data = [
12353 'id' => $view->vars['id'] ?? null,
12354 'name' => $view->vars['name'] ?? null,
12355 'view_vars' => [],
12356 ];
12357
12358 foreach ($view->vars as $varName => $value) {
12359 $data['view_vars'][$varName] = $value;
12360 }
12361
12362 ksort($data['view_vars']);
12363
12364 return $data;
12365 }
12366
12367 /**
12368 * Recursively builds an HTML ID for a form.
12369 */
12370 private function buildId(FormInterface $form): string
12371 {
12372 $id = $form->getName();
12373
12374 if (null !== $form->getParent()) {
12375 $id = $this->buildId($form->getParent()).'_'.$id;
12376 }
12377
12378 return $id;
12379 }
12380}
12381
12382------------------------------------------------------------------------------------------------------------------------
12383 ./Extension/DataCollector/FormDataCollectorInterface.php
12384------------------------------------------------------------------------------------------------------------------------
12385<?php
12386
12387/*
12388 * This file is part of the Symfony package.
12389 *
12390 * (c) Fabien Potencier <fabien@symfony.com>
12391 *
12392 * For the full copyright and license information, please view the LICENSE
12393 * file that was distributed with this source code.
12394 */
12395
12396namespace Symfony\Component\Form\Extension\DataCollector;
12397
12398use Symfony\Component\Form\FormInterface;
12399use Symfony\Component\Form\FormView;
12400use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
12401use Symfony\Component\VarDumper\Cloner\Data;
12402
12403/**
12404 * Collects and structures information about forms.
12405 *
12406 * @author Bernhard Schussek <bschussek@gmail.com>
12407 */
12408interface FormDataCollectorInterface extends DataCollectorInterface
12409{
12410 /**
12411 * Stores configuration data of the given form and its children.
12412 */
12413 public function collectConfiguration(FormInterface $form): void;
12414
12415 /**
12416 * Stores the default data of the given form and its children.
12417 */
12418 public function collectDefaultData(FormInterface $form): void;
12419
12420 /**
12421 * Stores the submitted data of the given form and its children.
12422 */
12423 public function collectSubmittedData(FormInterface $form): void;
12424
12425 /**
12426 * Stores the view variables of the given form view and its children.
12427 */
12428 public function collectViewVariables(FormView $view): void;
12429
12430 /**
12431 * Specifies that the given objects represent the same conceptual form.
12432 */
12433 public function associateFormWithView(FormInterface $form, FormView $view): void;
12434
12435 /**
12436 * Assembles the data collected about the given form and its children as
12437 * a tree-like data structure.
12438 *
12439 * The result can be queried using {@link getData()}.
12440 */
12441 public function buildPreliminaryFormTree(FormInterface $form): void;
12442
12443 /**
12444 * Assembles the data collected about the given form and its children as
12445 * a tree-like data structure.
12446 *
12447 * The result can be queried using {@link getData()}.
12448 *
12449 * Contrary to {@link buildPreliminaryFormTree()}, a {@link FormView}
12450 * object has to be passed. The tree structure of this view object will be
12451 * used for structuring the resulting data. That means, if a child is
12452 * present in the view, but not in the form, it will be present in the final
12453 * data array anyway.
12454 *
12455 * When {@link FormView} instances are present in the view tree, for which
12456 * no corresponding {@link FormInterface} objects can be found in the form
12457 * tree, only the view data will be included in the result. If a
12458 * corresponding {@link FormInterface} exists otherwise, call
12459 * {@link associateFormWithView()} before calling this method.
12460 */
12461 public function buildFinalFormTree(FormInterface $form, FormView $view): void;
12462
12463 /**
12464 * Returns all collected data.
12465 */
12466 public function getData(): array|Data;
12467}
12468
12469------------------------------------------------------------------------------------------------------------------------
12470 ./Extension/DataCollector/FormDataCollector.php
12471------------------------------------------------------------------------------------------------------------------------
12472<?php
12473
12474/*
12475 * This file is part of the Symfony package.
12476 *
12477 * (c) Fabien Potencier <fabien@symfony.com>
12478 *
12479 * For the full copyright and license information, please view the LICENSE
12480 * file that was distributed with this source code.
12481 */
12482
12483namespace Symfony\Component\Form\Extension\DataCollector;
12484
12485use Symfony\Component\Form\FormInterface;
12486use Symfony\Component\Form\FormView;
12487use Symfony\Component\HttpFoundation\Request;
12488use Symfony\Component\HttpFoundation\Response;
12489use Symfony\Component\HttpKernel\DataCollector\DataCollector;
12490use Symfony\Component\Validator\ConstraintViolationInterface;
12491use Symfony\Component\VarDumper\Caster\Caster;
12492use Symfony\Component\VarDumper\Caster\ClassStub;
12493use Symfony\Component\VarDumper\Caster\StubCaster;
12494use Symfony\Component\VarDumper\Cloner\Data;
12495use Symfony\Component\VarDumper\Cloner\Stub;
12496
12497/**
12498 * Data collector for {@link FormInterface} instances.
12499 *
12500 * @author Robert Schönthal <robert.schoenthal@gmail.com>
12501 * @author Bernhard Schussek <bschussek@gmail.com>
12502 *
12503 * @final
12504 */
12505class FormDataCollector extends DataCollector implements FormDataCollectorInterface
12506{
12507 /**
12508 * Stores the collected data per {@link FormInterface} instance.
12509 *
12510 * Uses the hashes of the forms as keys. This is preferable over using
12511 * {@link \SplObjectStorage}, because in this way no references are kept
12512 * to the {@link FormInterface} instances.
12513 */
12514 private array $dataByForm;
12515
12516 /**
12517 * Stores the collected data per {@link FormView} instance.
12518 *
12519 * Uses the hashes of the views as keys. This is preferable over using
12520 * {@link \SplObjectStorage}, because in this way no references are kept
12521 * to the {@link FormView} instances.
12522 */
12523 private array $dataByView;
12524
12525 /**
12526 * Connects {@link FormView} with {@link FormInterface} instances.
12527 *
12528 * Uses the hashes of the views as keys and the hashes of the forms as
12529 * values. This is preferable over storing the objects directly, because
12530 * this way they can safely be discarded by the GC.
12531 */
12532 private array $formsByView;
12533
12534 public function __construct(
12535 private FormDataExtractorInterface $dataExtractor,
12536 ) {
12537 if (!class_exists(ClassStub::class)) {
12538 throw new \LogicException(\sprintf('The VarDumper component is needed for using the "%s" class. Install symfony/var-dumper version 3.4 or above.', __CLASS__));
12539 }
12540
12541 $this->reset();
12542 }
12543
12544 /**
12545 * Does nothing. The data is collected during the form event listeners.
12546 */
12547 public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
12548 {
12549 }
12550
12551 public function reset(): void
12552 {
12553 $this->data = [
12554 'forms' => [],
12555 'forms_by_hash' => [],
12556 'nb_errors' => 0,
12557 ];
12558 }
12559
12560 public function associateFormWithView(FormInterface $form, FormView $view): void
12561 {
12562 $this->formsByView[spl_object_hash($view)] = spl_object_hash($form);
12563 }
12564
12565 public function collectConfiguration(FormInterface $form): void
12566 {
12567 $hash = spl_object_hash($form);
12568
12569 if (!isset($this->dataByForm[$hash])) {
12570 $this->dataByForm[$hash] = [];
12571 }
12572
12573 $this->dataByForm[$hash] = array_replace(
12574 $this->dataByForm[$hash],
12575 $this->dataExtractor->extractConfiguration($form)
12576 );
12577
12578 foreach ($form as $child) {
12579 $this->collectConfiguration($child);
12580 }
12581 }
12582
12583 public function collectDefaultData(FormInterface $form): void
12584 {
12585 $hash = spl_object_hash($form);
12586
12587 if (!isset($this->dataByForm[$hash])) {
12588 // field was created by form event
12589 $this->collectConfiguration($form);
12590 }
12591
12592 $this->dataByForm[$hash] = array_replace(
12593 $this->dataByForm[$hash],
12594 $this->dataExtractor->extractDefaultData($form)
12595 );
12596
12597 foreach ($form as $child) {
12598 $this->collectDefaultData($child);
12599 }
12600 }
12601
12602 public function collectSubmittedData(FormInterface $form): void
12603 {
12604 $hash = spl_object_hash($form);
12605
12606 if (!isset($this->dataByForm[$hash])) {
12607 // field was created by form event
12608 $this->collectConfiguration($form);
12609 $this->collectDefaultData($form);
12610 }
12611
12612 $this->dataByForm[$hash] = array_replace(
12613 $this->dataByForm[$hash],
12614 $this->dataExtractor->extractSubmittedData($form)
12615 );
12616
12617 // Count errors
12618 if (isset($this->dataByForm[$hash]['errors'])) {
12619 $this->data['nb_errors'] += \count($this->dataByForm[$hash]['errors']);
12620 }
12621
12622 foreach ($form as $child) {
12623 $this->collectSubmittedData($child);
12624
12625 // Expand current form if there are children with errors
12626 if (empty($this->dataByForm[$hash]['has_children_error'])) {
12627 $childData = $this->dataByForm[spl_object_hash($child)];
12628 $this->dataByForm[$hash]['has_children_error'] = !empty($childData['has_children_error']) || !empty($childData['errors']);
12629 }
12630 }
12631 }
12632
12633 public function collectViewVariables(FormView $view): void
12634 {
12635 $hash = spl_object_hash($view);
12636
12637 if (!isset($this->dataByView[$hash])) {
12638 $this->dataByView[$hash] = [];
12639 }
12640
12641 $this->dataByView[$hash] = array_replace(
12642 $this->dataByView[$hash],
12643 $this->dataExtractor->extractViewVariables($view)
12644 );
12645
12646 foreach ($view->children as $child) {
12647 $this->collectViewVariables($child);
12648 }
12649 }
12650
12651 public function buildPreliminaryFormTree(FormInterface $form): void
12652 {
12653 $this->data['forms'][$form->getName()] = &$this->recursiveBuildPreliminaryFormTree($form, $this->data['forms_by_hash']);
12654 }
12655
12656 public function buildFinalFormTree(FormInterface $form, FormView $view): void
12657 {
12658 $this->data['forms'][$form->getName()] = &$this->recursiveBuildFinalFormTree($form, $view, $this->data['forms_by_hash']);
12659 }
12660
12661 public function getName(): string
12662 {
12663 return 'form';
12664 }
12665
12666 public function getData(): array|Data
12667 {
12668 return $this->data;
12669 }
12670
12671 /**
12672 * @internal
12673 */
12674 public function __sleep(): array
12675 {
12676 foreach ($this->data['forms_by_hash'] as &$form) {
12677 if (isset($form['type_class']) && !$form['type_class'] instanceof ClassStub) {
12678 $form['type_class'] = new ClassStub($form['type_class']);
12679 }
12680 }
12681
12682 $this->data = $this->cloneVar($this->data);
12683
12684 return parent::__sleep();
12685 }
12686
12687 protected function getCasters(): array
12688 {
12689 return parent::getCasters() + [
12690 \Exception::class => static function (\Exception $e, array $a, Stub $s) {
12691 foreach (["\0Exception\0previous", "\0Exception\0trace"] as $k) {
12692 if (isset($a[$k])) {
12693 unset($a[$k]);
12694 ++$s->cut;
12695 }
12696 }
12697
12698 return $a;
12699 },
12700 FormInterface::class => static fn (FormInterface $f, array $a) => [
12701 Caster::PREFIX_VIRTUAL.'name' => $f->getName(),
12702 Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub($f->getConfig()->getType()->getInnerType()::class),
12703 ],
12704 FormView::class => StubCaster::cutInternals(...),
12705 ConstraintViolationInterface::class => static fn (ConstraintViolationInterface $v, array $a) => [
12706 Caster::PREFIX_VIRTUAL.'root' => $v->getRoot(),
12707 Caster::PREFIX_VIRTUAL.'path' => $v->getPropertyPath(),
12708 Caster::PREFIX_VIRTUAL.'value' => $v->getInvalidValue(),
12709 ],
12710 ];
12711 }
12712
12713 private function &recursiveBuildPreliminaryFormTree(FormInterface $form, array &$outputByHash): array
12714 {
12715 $hash = spl_object_hash($form);
12716
12717 $output = &$outputByHash[$hash];
12718 $output = $this->dataByForm[$hash]
12719 ?? [];
12720
12721 $output['children'] = [];
12722
12723 foreach ($form as $name => $child) {
12724 $output['children'][$name] = &$this->recursiveBuildPreliminaryFormTree($child, $outputByHash);
12725 }
12726
12727 return $output;
12728 }
12729
12730 private function &recursiveBuildFinalFormTree(?FormInterface $form, FormView $view, array &$outputByHash): array
12731 {
12732 $viewHash = spl_object_hash($view);
12733 $formHash = null;
12734
12735 if (null !== $form) {
12736 $formHash = spl_object_hash($form);
12737 } elseif (isset($this->formsByView[$viewHash])) {
12738 // The FormInterface instance of the CSRF token is never contained in
12739 // the FormInterface tree of the form, so we need to get the
12740 // corresponding FormInterface instance for its view in a different way
12741 $formHash = $this->formsByView[$viewHash];
12742 }
12743 if (null !== $formHash) {
12744 $output = &$outputByHash[$formHash];
12745 }
12746
12747 $output = $this->dataByView[$viewHash]
12748 ?? [];
12749
12750 if (null !== $formHash) {
12751 $output = array_replace(
12752 $output,
12753 $this->dataByForm[$formHash]
12754 ?? []
12755 );
12756 }
12757
12758 $output['children'] = [];
12759
12760 foreach ($view->children as $name => $childView) {
12761 // The CSRF token, for example, is never added to the form tree.
12762 // It is only present in the view.
12763 $childForm = $form?->has($name) ? $form->get($name) : null;
12764
12765 $output['children'][$name] = &$this->recursiveBuildFinalFormTree($childForm, $childView, $outputByHash);
12766 }
12767
12768 return $output;
12769 }
12770}
12771
12772------------------------------------------------------------------------------------------------------------------------
12773 ./Extension/DataCollector/FormDataExtractorInterface.php
12774------------------------------------------------------------------------------------------------------------------------
12775<?php
12776
12777/*
12778 * This file is part of the Symfony package.
12779 *
12780 * (c) Fabien Potencier <fabien@symfony.com>
12781 *
12782 * For the full copyright and license information, please view the LICENSE
12783 * file that was distributed with this source code.
12784 */
12785
12786namespace Symfony\Component\Form\Extension\DataCollector;
12787
12788use Symfony\Component\Form\FormInterface;
12789use Symfony\Component\Form\FormView;
12790
12791/**
12792 * Extracts arrays of information out of forms.
12793 *
12794 * @author Bernhard Schussek <bschussek@gmail.com>
12795 */
12796interface FormDataExtractorInterface
12797{
12798 /**
12799 * Extracts the configuration data of a form.
12800 */
12801 public function extractConfiguration(FormInterface $form): array;
12802
12803 /**
12804 * Extracts the default data of a form.
12805 */
12806 public function extractDefaultData(FormInterface $form): array;
12807
12808 /**
12809 * Extracts the submitted data of a form.
12810 */
12811 public function extractSubmittedData(FormInterface $form): array;
12812
12813 /**
12814 * Extracts the view variables of a form.
12815 */
12816 public function extractViewVariables(FormView $view): array;
12817}
12818
12819------------------------------------------------------------------------------------------------------------------------
12820 ./Extension/HttpFoundation/HttpFoundationRequestHandler.php
12821------------------------------------------------------------------------------------------------------------------------
12822<?php
12823
12824/*
12825 * This file is part of the Symfony package.
12826 *
12827 * (c) Fabien Potencier <fabien@symfony.com>
12828 *
12829 * For the full copyright and license information, please view the LICENSE
12830 * file that was distributed with this source code.
12831 */
12832
12833namespace Symfony\Component\Form\Extension\HttpFoundation;
12834
12835use Symfony\Component\Form\Exception\UnexpectedTypeException;
12836use Symfony\Component\Form\FormError;
12837use Symfony\Component\Form\FormInterface;
12838use Symfony\Component\Form\RequestHandlerInterface;
12839use Symfony\Component\Form\Util\FormUtil;
12840use Symfony\Component\Form\Util\ServerParams;
12841use Symfony\Component\HttpFoundation\File\File;
12842use Symfony\Component\HttpFoundation\File\UploadedFile;
12843use Symfony\Component\HttpFoundation\Request;
12844
12845/**
12846 * A request processor using the {@link Request} class of the HttpFoundation
12847 * component.
12848 *
12849 * @author Bernhard Schussek <bschussek@gmail.com>
12850 */
12851class HttpFoundationRequestHandler implements RequestHandlerInterface
12852{
12853 private ServerParams $serverParams;
12854
12855 public function __construct(?ServerParams $serverParams = null)
12856 {
12857 $this->serverParams = $serverParams ?? new ServerParams();
12858 }
12859
12860 public function handleRequest(FormInterface $form, mixed $request = null): void
12861 {
12862 if (!$request instanceof Request) {
12863 throw new UnexpectedTypeException($request, Request::class);
12864 }
12865
12866 $name = $form->getName();
12867 $method = $form->getConfig()->getMethod();
12868
12869 if ($method !== $request->getMethod()) {
12870 return;
12871 }
12872
12873 // For request methods that must not have a request body we fetch data
12874 // from the query string. Otherwise we look for data in the request body.
12875 if ('GET' === $method || 'HEAD' === $method || 'TRACE' === $method) {
12876 if ('' === $name) {
12877 $data = $request->query->all();
12878 } else {
12879 // Don't submit GET requests if the form's name does not exist
12880 // in the request
12881 if (!$request->query->has($name)) {
12882 return;
12883 }
12884
12885 $data = $request->query->all()[$name];
12886 }
12887 } else {
12888 // Mark the form with an error if the uploaded size was too large
12889 // This is done here and not in FormValidator because $_POST is
12890 // empty when that error occurs. Hence the form is never submitted.
12891 if ($this->serverParams->hasPostMaxSizeBeenExceeded()) {
12892 // Submit the form, but don't clear the default values
12893 $form->submit(null, false);
12894
12895 $form->addError(new FormError(
12896 $form->getConfig()->getOption('upload_max_size_message')(),
12897 null,
12898 ['{{ max }}' => $this->serverParams->getNormalizedIniPostMaxSize()]
12899 ));
12900
12901 return;
12902 }
12903
12904 if ('' === $name) {
12905 $params = $request->request->all();
12906 $files = $request->files->all();
12907 } elseif ($request->request->has($name) || $request->files->has($name)) {
12908 $default = $form->getConfig()->getCompound() ? [] : null;
12909 $params = $request->request->all()[$name] ?? $default;
12910 $files = $request->files->get($name, $default);
12911 } else {
12912 // Don't submit the form if it is not present in the request
12913 return;
12914 }
12915
12916 if (\is_array($params) && \is_array($files)) {
12917 $data = FormUtil::mergeParamsAndFiles($params, $files);
12918 } else {
12919 $data = $params ?: $files;
12920 }
12921 }
12922
12923 // Don't auto-submit the form unless at least one field is present.
12924 if ('' === $name && \count(array_intersect_key($data, $form->all())) <= 0) {
12925 return;
12926 }
12927
12928 $form->submit($data, 'PATCH' !== $method);
12929 }
12930
12931 public function isFileUpload(mixed $data): bool
12932 {
12933 return $data instanceof File;
12934 }
12935
12936 public function getUploadFileError(mixed $data): ?int
12937 {
12938 if (!$data instanceof UploadedFile || $data->isValid()) {
12939 return null;
12940 }
12941
12942 return $data->getError();
12943 }
12944}
12945
12946------------------------------------------------------------------------------------------------------------------------
12947 ./Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php
12948------------------------------------------------------------------------------------------------------------------------
12949<?php
12950
12951/*
12952 * This file is part of the Symfony package.
12953 *
12954 * (c) Fabien Potencier <fabien@symfony.com>
12955 *
12956 * For the full copyright and license information, please view the LICENSE
12957 * file that was distributed with this source code.
12958 */
12959
12960namespace Symfony\Component\Form\Extension\HttpFoundation\Type;
12961
12962use Symfony\Component\Form\AbstractTypeExtension;
12963use Symfony\Component\Form\Extension\Core\Type\FormType;
12964use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
12965use Symfony\Component\Form\FormBuilderInterface;
12966use Symfony\Component\Form\RequestHandlerInterface;
12967
12968/**
12969 * @author Bernhard Schussek <bschussek@gmail.com>
12970 */
12971class FormTypeHttpFoundationExtension extends AbstractTypeExtension
12972{
12973 private RequestHandlerInterface $requestHandler;
12974
12975 public function __construct(?RequestHandlerInterface $requestHandler = null)
12976 {
12977 $this->requestHandler = $requestHandler ?? new HttpFoundationRequestHandler();
12978 }
12979
12980 public function buildForm(FormBuilderInterface $builder, array $options): void
12981 {
12982 $builder->setRequestHandler($this->requestHandler);
12983 }
12984
12985 public static function getExtendedTypes(): iterable
12986 {
12987 return [FormType::class];
12988 }
12989}
12990
12991------------------------------------------------------------------------------------------------------------------------
12992 ./Extension/HttpFoundation/HttpFoundationExtension.php
12993------------------------------------------------------------------------------------------------------------------------
12994<?php
12995
12996/*
12997 * This file is part of the Symfony package.
12998 *
12999 * (c) Fabien Potencier <fabien@symfony.com>
13000 *
13001 * For the full copyright and license information, please view the LICENSE
13002 * file that was distributed with this source code.
13003 */
13004
13005namespace Symfony\Component\Form\Extension\HttpFoundation;
13006
13007use Symfony\Component\Form\AbstractExtension;
13008
13009/**
13010 * Integrates the HttpFoundation component with the Form library.
13011 *
13012 * @author Bernhard Schussek <bschussek@gmail.com>
13013 */
13014class HttpFoundationExtension extends AbstractExtension
13015{
13016 protected function loadTypeExtensions(): array
13017 {
13018 return [
13019 new Type\FormTypeHttpFoundationExtension(),
13020 ];
13021 }
13022}
13023
13024------------------------------------------------------------------------------------------------------------------------
13025 ./Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php
13026------------------------------------------------------------------------------------------------------------------------
13027<?php
13028
13029/*
13030 * This file is part of the Symfony package.
13031 *
13032 * (c) Fabien Potencier <fabien@symfony.com>
13033 *
13034 * For the full copyright and license information, please view the LICENSE
13035 * file that was distributed with this source code.
13036 */
13037
13038namespace Symfony\Component\Form\Extension\HtmlSanitizer\Type;
13039
13040use Psr\Container\ContainerInterface;
13041use Symfony\Component\Form\AbstractTypeExtension;
13042use Symfony\Component\Form\Extension\Core\Type\TextType;
13043use Symfony\Component\Form\FormBuilderInterface;
13044use Symfony\Component\Form\FormEvent;
13045use Symfony\Component\Form\FormEvents;
13046use Symfony\Component\OptionsResolver\OptionsResolver;
13047
13048/**
13049 * @author Titouan Galopin <galopintitouan@gmail.com>
13050 */
13051class TextTypeHtmlSanitizerExtension extends AbstractTypeExtension
13052{
13053 public function __construct(
13054 private ContainerInterface $sanitizers,
13055 private string $defaultSanitizer = 'default',
13056 ) {
13057 }
13058
13059 public static function getExtendedTypes(): iterable
13060 {
13061 return [TextType::class];
13062 }
13063
13064 public function configureOptions(OptionsResolver $resolver): void
13065 {
13066 $resolver
13067 ->setDefaults(['sanitize_html' => false, 'sanitizer' => null])
13068 ->setAllowedTypes('sanitize_html', 'bool')
13069 ->setAllowedTypes('sanitizer', ['string', 'null'])
13070 ;
13071 }
13072
13073 public function buildForm(FormBuilderInterface $builder, array $options): void
13074 {
13075 if (!$options['sanitize_html']) {
13076 return;
13077 }
13078
13079 $sanitizers = $this->sanitizers;
13080 $sanitizer = $options['sanitizer'] ?? $this->defaultSanitizer;
13081
13082 $builder->addEventListener(
13083 FormEvents::PRE_SUBMIT,
13084 static function (FormEvent $event) use ($sanitizers, $sanitizer) {
13085 if (\is_scalar($data = $event->getData()) && '' !== trim($data)) {
13086 $event->setData($sanitizers->get($sanitizer)->sanitize($data));
13087 }
13088 },
13089 10000 /* as soon as possible */
13090 );
13091 }
13092}
13093
13094------------------------------------------------------------------------------------------------------------------------
13095 ./Extension/HtmlSanitizer/HtmlSanitizerExtension.php
13096------------------------------------------------------------------------------------------------------------------------
13097<?php
13098
13099/*
13100 * This file is part of the Symfony package.
13101 *
13102 * (c) Fabien Potencier <fabien@symfony.com>
13103 *
13104 * For the full copyright and license information, please view the LICENSE
13105 * file that was distributed with this source code.
13106 */
13107
13108namespace Symfony\Component\Form\Extension\HtmlSanitizer;
13109
13110use Psr\Container\ContainerInterface;
13111use Symfony\Component\Form\AbstractExtension;
13112
13113/**
13114 * Integrates the HtmlSanitizer component with the Form library.
13115 *
13116 * @author Nicolas Grekas <p@tchwork.com>
13117 */
13118class HtmlSanitizerExtension extends AbstractExtension
13119{
13120 public function __construct(
13121 private ContainerInterface $sanitizers,
13122 private string $defaultSanitizer = 'default',
13123 ) {
13124 }
13125
13126 protected function loadTypeExtensions(): array
13127 {
13128 return [
13129 new Type\TextTypeHtmlSanitizerExtension($this->sanitizers, $this->defaultSanitizer),
13130 ];
13131 }
13132}
13133
13134------------------------------------------------------------------------------------------------------------------------
13135 ./Extension/Csrf/Type/FormTypeCsrfExtension.php
13136------------------------------------------------------------------------------------------------------------------------
13137<?php
13138
13139/*
13140 * This file is part of the Symfony package.
13141 *
13142 * (c) Fabien Potencier <fabien@symfony.com>
13143 *
13144 * For the full copyright and license information, please view the LICENSE
13145 * file that was distributed with this source code.
13146 */
13147
13148namespace Symfony\Component\Form\Extension\Csrf\Type;
13149
13150use Symfony\Component\Form\AbstractTypeExtension;
13151use Symfony\Component\Form\Extension\Core\Type\FormType;
13152use Symfony\Component\Form\Extension\Core\Type\HiddenType;
13153use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
13154use Symfony\Component\Form\FormBuilderInterface;
13155use Symfony\Component\Form\FormInterface;
13156use Symfony\Component\Form\FormView;
13157use Symfony\Component\Form\Util\ServerParams;
13158use Symfony\Component\OptionsResolver\Options;
13159use Symfony\Component\OptionsResolver\OptionsResolver;
13160use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
13161use Symfony\Contracts\Translation\TranslatorInterface;
13162
13163/**
13164 * @author Bernhard Schussek <bschussek@gmail.com>
13165 */
13166class FormTypeCsrfExtension extends AbstractTypeExtension
13167{
13168 public function __construct(
13169 private CsrfTokenManagerInterface $defaultTokenManager,
13170 private bool $defaultEnabled = true,
13171 private string $defaultFieldName = '_token',
13172 private ?TranslatorInterface $translator = null,
13173 private ?string $translationDomain = null,
13174 private ?ServerParams $serverParams = null,
13175 private array $fieldAttr = [],
13176 private string|array|null $defaultTokenId = null,
13177 ) {
13178 }
13179
13180 /**
13181 * Adds a CSRF field to the form when the CSRF protection is enabled.
13182 */
13183 public function buildForm(FormBuilderInterface $builder, array $options): void
13184 {
13185 if (!$options['csrf_protection']) {
13186 return;
13187 }
13188
13189 $csrfTokenId = $options['csrf_token_id']
13190 ?: $this->defaultTokenId[$builder->getType()->getInnerType()::class]
13191 ?? $builder->getName()
13192 ?: $builder->getType()->getInnerType()::class;
13193 $builder->setAttribute('csrf_token_id', $csrfTokenId);
13194
13195 $builder
13196 ->addEventSubscriber(new CsrfValidationListener(
13197 $options['csrf_field_name'],
13198 $options['csrf_token_manager'],
13199 $csrfTokenId,
13200 $options['csrf_message'],
13201 $this->translator,
13202 $this->translationDomain,
13203 $this->serverParams
13204 ))
13205 ;
13206 }
13207
13208 /**
13209 * Adds a CSRF field to the root form view.
13210 */
13211 public function finishView(FormView $view, FormInterface $form, array $options): void
13212 {
13213 if ($options['csrf_protection'] && !$view->parent && $options['compound']) {
13214 $factory = $form->getConfig()->getFormFactory();
13215 $tokenId = $form->getConfig()->getAttribute('csrf_token_id');
13216 $data = (string) $options['csrf_token_manager']->getToken($tokenId);
13217
13218 $csrfForm = $factory->createNamed($options['csrf_field_name'], HiddenType::class, $data, [
13219 'block_prefix' => 'csrf_token',
13220 'mapped' => false,
13221 'attr' => $this->fieldAttr,
13222 ]);
13223
13224 $view->children[$options['csrf_field_name']] = $csrfForm->createView($view);
13225 }
13226 }
13227
13228 public function configureOptions(OptionsResolver $resolver): void
13229 {
13230 if (\is_string($defaultTokenId = $this->defaultTokenId) && $defaultTokenId) {
13231 $defaultTokenManager = $this->defaultTokenManager;
13232 $defaultTokenId = static fn (Options $options) => $options['csrf_token_manager'] === $defaultTokenManager ? $defaultTokenId : null;
13233 } else {
13234 $defaultTokenId = null;
13235 }
13236
13237 $resolver->setDefaults([
13238 'csrf_protection' => $this->defaultEnabled,
13239 'csrf_field_name' => $this->defaultFieldName,
13240 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.',
13241 'csrf_token_manager' => $this->defaultTokenManager,
13242 'csrf_token_id' => $defaultTokenId,
13243 ]);
13244
13245 $resolver->setAllowedTypes('csrf_protection', 'bool');
13246 $resolver->setAllowedTypes('csrf_field_name', 'string');
13247 $resolver->setAllowedTypes('csrf_message', 'string');
13248 $resolver->setAllowedTypes('csrf_token_manager', CsrfTokenManagerInterface::class);
13249 $resolver->setAllowedTypes('csrf_token_id', ['null', 'string']);
13250 }
13251
13252 public static function getExtendedTypes(): iterable
13253 {
13254 return [FormType::class];
13255 }
13256}
13257
13258------------------------------------------------------------------------------------------------------------------------
13259 ./Extension/Csrf/CsrfExtension.php
13260------------------------------------------------------------------------------------------------------------------------
13261<?php
13262
13263/*
13264 * This file is part of the Symfony package.
13265 *
13266 * (c) Fabien Potencier <fabien@symfony.com>
13267 *
13268 * For the full copyright and license information, please view the LICENSE
13269 * file that was distributed with this source code.
13270 */
13271
13272namespace Symfony\Component\Form\Extension\Csrf;
13273
13274use Symfony\Component\Form\AbstractExtension;
13275use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
13276use Symfony\Contracts\Translation\TranslatorInterface;
13277
13278/**
13279 * This extension protects forms by using a CSRF token.
13280 *
13281 * @author Bernhard Schussek <bschussek@gmail.com>
13282 */
13283class CsrfExtension extends AbstractExtension
13284{
13285 public function __construct(
13286 private CsrfTokenManagerInterface $tokenManager,
13287 private ?TranslatorInterface $translator = null,
13288 private ?string $translationDomain = null,
13289 ) {
13290 }
13291
13292 protected function loadTypeExtensions(): array
13293 {
13294 return [
13295 new Type\FormTypeCsrfExtension($this->tokenManager, true, '_token', $this->translator, $this->translationDomain),
13296 ];
13297 }
13298}
13299
13300------------------------------------------------------------------------------------------------------------------------
13301 ./Extension/Csrf/EventListener/CsrfValidationListener.php
13302------------------------------------------------------------------------------------------------------------------------
13303<?php
13304
13305/*
13306 * This file is part of the Symfony package.
13307 *
13308 * (c) Fabien Potencier <fabien@symfony.com>
13309 *
13310 * For the full copyright and license information, please view the LICENSE
13311 * file that was distributed with this source code.
13312 */
13313
13314namespace Symfony\Component\Form\Extension\Csrf\EventListener;
13315
13316use Symfony\Component\EventDispatcher\EventSubscriberInterface;
13317use Symfony\Component\Form\FormError;
13318use Symfony\Component\Form\FormEvent;
13319use Symfony\Component\Form\FormEvents;
13320use Symfony\Component\Form\Util\ServerParams;
13321use Symfony\Component\Security\Csrf\CsrfToken;
13322use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
13323use Symfony\Contracts\Translation\TranslatorInterface;
13324
13325/**
13326 * @author Bernhard Schussek <bschussek@gmail.com>
13327 */
13328class CsrfValidationListener implements EventSubscriberInterface
13329{
13330 private ServerParams $serverParams;
13331
13332 public static function getSubscribedEvents(): array
13333 {
13334 return [
13335 FormEvents::PRE_SUBMIT => 'preSubmit',
13336 ];
13337 }
13338
13339 public function __construct(
13340 private string $fieldName,
13341 private CsrfTokenManagerInterface $tokenManager,
13342 private string $tokenId,
13343 private string $errorMessage,
13344 private ?TranslatorInterface $translator = null,
13345 private ?string $translationDomain = null,
13346 ?ServerParams $serverParams = null,
13347 ) {
13348 $this->serverParams = $serverParams ?? new ServerParams();
13349 }
13350
13351 public function preSubmit(FormEvent $event): void
13352 {
13353 $form = $event->getForm();
13354 $postRequestSizeExceeded = 'POST' === $form->getConfig()->getMethod() && $this->serverParams->hasPostMaxSizeBeenExceeded();
13355
13356 if ($form->isRoot() && $form->getConfig()->getOption('compound') && !$postRequestSizeExceeded) {
13357 $data = $event->getData();
13358
13359 $csrfValue = \is_string($data[$this->fieldName] ?? null) ? $data[$this->fieldName] : null;
13360 $csrfToken = new CsrfToken($this->tokenId, $csrfValue);
13361
13362 if (null === $csrfValue || !$this->tokenManager->isTokenValid($csrfToken)) {
13363 $errorMessage = $this->errorMessage;
13364
13365 if (null !== $this->translator) {
13366 $errorMessage = $this->translator->trans($errorMessage, [], $this->translationDomain);
13367 }
13368
13369 $form->addError(new FormError($errorMessage, $errorMessage, [], null, $csrfToken));
13370 }
13371
13372 if (\is_array($data)) {
13373 unset($data[$this->fieldName]);
13374 $event->setData($data);
13375 }
13376 }
13377 }
13378}
13379
13380------------------------------------------------------------------------------------------------------------------------
13381 ./Extension/Core/EventListener/ResizeFormListener.php
13382------------------------------------------------------------------------------------------------------------------------
13383<?php
13384
13385/*
13386 * This file is part of the Symfony package.
13387 *
13388 * (c) Fabien Potencier <fabien@symfony.com>
13389 *
13390 * For the full copyright and license information, please view the LICENSE
13391 * file that was distributed with this source code.
13392 */
13393
13394namespace Symfony\Component\Form\Extension\Core\EventListener;
13395
13396use Symfony\Component\EventDispatcher\EventSubscriberInterface;
13397use Symfony\Component\Form\Event\PostSetDataEvent;
13398use Symfony\Component\Form\Exception\UnexpectedTypeException;
13399use Symfony\Component\Form\FormEvent;
13400use Symfony\Component\Form\FormEvents;
13401use Symfony\Component\Form\FormInterface;
13402
13403/**
13404 * Resize a collection form element based on the data sent from the client.
13405 *
13406 * @author Bernhard Schussek <bschussek@gmail.com>
13407 */
13408class ResizeFormListener implements EventSubscriberInterface
13409{
13410 protected array $prototypeOptions;
13411
13412 private \Closure|bool $deleteEmpty;
13413 // BC, to be removed in 8.0
13414 private bool $overridden = true;
13415 private bool $usePreSetData = false;
13416
13417 public function __construct(
13418 private string $type,
13419 private array $options = [],
13420 private bool $allowAdd = false,
13421 private bool $allowDelete = false,
13422 bool|callable $deleteEmpty = false,
13423 ?array $prototypeOptions = null,
13424 private bool $keepAsList = false,
13425 ) {
13426 $this->deleteEmpty = \is_bool($deleteEmpty) ? $deleteEmpty : $deleteEmpty(...);
13427 $this->prototypeOptions = $prototypeOptions ?? $options;
13428 }
13429
13430 public static function getSubscribedEvents(): array
13431 {
13432 return [
13433 FormEvents::PRE_SET_DATA => 'preSetData', // deprecated
13434 FormEvents::POST_SET_DATA => ['postSetData', 255], // as early as possible
13435 FormEvents::PRE_SUBMIT => 'preSubmit',
13436 // (MergeCollectionListener, MergeDoctrineCollectionListener)
13437 FormEvents::SUBMIT => ['onSubmit', 50],
13438 ];
13439 }
13440
13441 /**
13442 * @deprecated Since Symfony 7.2, use {@see postSetData()} instead.
13443 */
13444 public function preSetData(FormEvent $event): void
13445 {
13446 if (__CLASS__ === static::class
13447 || __CLASS__ === (new \ReflectionClass($this))->getMethod('preSetData')->getDeclaringClass()->name
13448 ) {
13449 // not a child class, or child class does not overload PRE_SET_DATA
13450 return;
13451 }
13452
13453 trigger_deprecation('symfony/form', '7.2', 'Calling "%s()" is deprecated, use "%s::postSetData()" instead.', __METHOD__, __CLASS__);
13454 // parent::preSetData() has been called
13455 $this->overridden = false;
13456 try {
13457 $this->postSetData($event);
13458 } finally {
13459 $this->usePreSetData = true;
13460 }
13461 }
13462
13463 /**
13464 * Remove FormEvent type hint in 8.0.
13465 *
13466 * @final since Symfony 7.2
13467 */
13468 public function postSetData(FormEvent|PostSetDataEvent $event): void
13469 {
13470 if (__CLASS__ !== static::class) {
13471 if ($this->overridden) {
13472 trigger_deprecation('symfony/form', '7.2', 'Calling "%s::preSetData()" is deprecated, use "%s::postSetData()" instead.', static::class, __CLASS__);
13473 // parent::preSetData() has not been called, noop
13474
13475 return;
13476 }
13477
13478 if ($this->usePreSetData) {
13479 // nothing else to do
13480 return;
13481 }
13482 }
13483
13484 $form = $event->getForm();
13485 $data = $event->getData() ?? [];
13486
13487 if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
13488 throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
13489 }
13490
13491 // First remove all rows
13492 foreach ($form as $name => $child) {
13493 $form->remove($name);
13494 }
13495
13496 // Then add all rows again in the correct order
13497 foreach ($data as $name => $value) {
13498 $form->add($name, $this->type, array_replace([
13499 'property_path' => '['.$name.']',
13500 ], $this->options));
13501 }
13502 }
13503
13504 public function preSubmit(FormEvent $event): void
13505 {
13506 $form = $event->getForm();
13507 $data = $event->getData();
13508
13509 if (!\is_array($data)) {
13510 $data = [];
13511 }
13512
13513 // Remove all empty rows
13514 if ($this->allowDelete) {
13515 foreach ($form as $name => $child) {
13516 if (!isset($data[$name])) {
13517 $form->remove($name);
13518 }
13519 }
13520 }
13521
13522 // Add all additional rows
13523 if ($this->allowAdd) {
13524 foreach ($data as $name => $value) {
13525 if (!$form->has($name)) {
13526 $form->add($name, $this->type, array_replace([
13527 'property_path' => '['.$name.']',
13528 ], $this->prototypeOptions));
13529 }
13530 }
13531 }
13532 }
13533
13534 public function onSubmit(FormEvent $event): void
13535 {
13536 $form = $event->getForm();
13537 $data = $event->getData() ?? [];
13538
13539 // At this point, $data is an array or an array-like object that already contains the
13540 // new entries, which were added by the data mapper. The data mapper ignores existing
13541 // entries, so we need to manually unset removed entries in the collection.
13542
13543 if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
13544 throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
13545 }
13546
13547 if ($this->deleteEmpty) {
13548 $previousData = $form->getData();
13549 /** @var FormInterface $child */
13550 foreach ($form as $name => $child) {
13551 if (!$child->isValid() || !$child->isSynchronized()) {
13552 continue;
13553 }
13554
13555 $isNew = !isset($previousData[$name]);
13556 $isEmpty = \is_callable($this->deleteEmpty) ? ($this->deleteEmpty)($child->getData()) : $child->isEmpty();
13557
13558 // $isNew can only be true if allowAdd is true, so we don't
13559 // need to check allowAdd again
13560 if ($isEmpty && ($isNew || $this->allowDelete)) {
13561 unset($data[$name]);
13562 $form->remove($name);
13563 }
13564 }
13565 }
13566
13567 // The data mapper only adds, but does not remove items, so do this
13568 // here
13569 if ($this->allowDelete) {
13570 $toDelete = [];
13571
13572 foreach ($data as $name => $child) {
13573 if (!$form->has($name)) {
13574 $toDelete[] = $name;
13575 }
13576 }
13577
13578 foreach ($toDelete as $name) {
13579 unset($data[$name]);
13580 }
13581 }
13582
13583 if ($this->keepAsList) {
13584 $formReindex = [];
13585 foreach ($form as $name => $child) {
13586 $formReindex[] = $child;
13587 $form->remove($name);
13588 }
13589 foreach ($formReindex as $index => $child) {
13590 $form->add($index, $this->type, array_replace([
13591 'property_path' => '['.$index.']',
13592 ], $this->options));
13593 }
13594 $data = array_values($data);
13595 }
13596
13597 $event->setData($data);
13598 }
13599}
13600
13601------------------------------------------------------------------------------------------------------------------------
13602 ./Extension/Core/EventListener/TrimListener.php
13603------------------------------------------------------------------------------------------------------------------------
13604<?php
13605
13606/*
13607 * This file is part of the Symfony package.
13608 *
13609 * (c) Fabien Potencier <fabien@symfony.com>
13610 *
13611 * For the full copyright and license information, please view the LICENSE
13612 * file that was distributed with this source code.
13613 */
13614
13615namespace Symfony\Component\Form\Extension\Core\EventListener;
13616
13617use Symfony\Component\EventDispatcher\EventSubscriberInterface;
13618use Symfony\Component\Form\FormEvent;
13619use Symfony\Component\Form\FormEvents;
13620use Symfony\Component\Form\Util\StringUtil;
13621
13622/**
13623 * Trims string data.
13624 *
13625 * @author Bernhard Schussek <bschussek@gmail.com>
13626 */
13627class TrimListener implements EventSubscriberInterface
13628{
13629 public function preSubmit(FormEvent $event): void
13630 {
13631 $data = $event->getData();
13632
13633 if (!\is_string($data)) {
13634 return;
13635 }
13636
13637 $event->setData(StringUtil::trim($data));
13638 }
13639
13640 public static function getSubscribedEvents(): array
13641 {
13642 return [FormEvents::PRE_SUBMIT => 'preSubmit'];
13643 }
13644}
13645
13646------------------------------------------------------------------------------------------------------------------------
13647 ./Extension/Core/EventListener/FixUrlProtocolListener.php
13648------------------------------------------------------------------------------------------------------------------------
13649<?php
13650
13651/*
13652 * This file is part of the Symfony package.
13653 *
13654 * (c) Fabien Potencier <fabien@symfony.com>
13655 *
13656 * For the full copyright and license information, please view the LICENSE
13657 * file that was distributed with this source code.
13658 */
13659
13660namespace Symfony\Component\Form\Extension\Core\EventListener;
13661
13662use Symfony\Component\EventDispatcher\EventSubscriberInterface;
13663use Symfony\Component\Form\FormEvent;
13664use Symfony\Component\Form\FormEvents;
13665
13666/**
13667 * Adds a protocol to a URL if it doesn't already have one.
13668 *
13669 * @author Bernhard Schussek <bschussek@gmail.com>
13670 */
13671class FixUrlProtocolListener implements EventSubscriberInterface
13672{
13673 /**
13674 * @param string|null $defaultProtocol The URL scheme to add when there is none or null to not modify the data
13675 */
13676 public function __construct(
13677 private ?string $defaultProtocol = 'http',
13678 ) {
13679 }
13680
13681 public function onSubmit(FormEvent $event): void
13682 {
13683 $data = $event->getData();
13684
13685 if ($this->defaultProtocol && $data && \is_string($data) && !preg_match('~^(?:[/.]|[\w+.-]+://|[^:/?@#]++@)~', $data)) {
13686 $event->setData($this->defaultProtocol.'://'.$data);
13687 }
13688 }
13689
13690 public static function getSubscribedEvents(): array
13691 {
13692 return [FormEvents::SUBMIT => 'onSubmit'];
13693 }
13694}
13695
13696------------------------------------------------------------------------------------------------------------------------
13697 ./Extension/Core/EventListener/MergeCollectionListener.php
13698------------------------------------------------------------------------------------------------------------------------
13699<?php
13700
13701/*
13702 * This file is part of the Symfony package.
13703 *
13704 * (c) Fabien Potencier <fabien@symfony.com>
13705 *
13706 * For the full copyright and license information, please view the LICENSE
13707 * file that was distributed with this source code.
13708 */
13709
13710namespace Symfony\Component\Form\Extension\Core\EventListener;
13711
13712use Symfony\Component\EventDispatcher\EventSubscriberInterface;
13713use Symfony\Component\Form\Exception\UnexpectedTypeException;
13714use Symfony\Component\Form\FormEvent;
13715use Symfony\Component\Form\FormEvents;
13716
13717/**
13718 * @author Bernhard Schussek <bschussek@gmail.com>
13719 */
13720class MergeCollectionListener implements EventSubscriberInterface
13721{
13722 /**
13723 * @param bool $allowAdd Whether values might be added to the collection
13724 * @param bool $allowDelete Whether values might be removed from the collection
13725 */
13726 public function __construct(
13727 private bool $allowAdd = false,
13728 private bool $allowDelete = false,
13729 ) {
13730 }
13731
13732 public static function getSubscribedEvents(): array
13733 {
13734 return [
13735 FormEvents::SUBMIT => 'onSubmit',
13736 ];
13737 }
13738
13739 public function onSubmit(FormEvent $event): void
13740 {
13741 $dataToMergeInto = $event->getForm()->getNormData();
13742 $data = $event->getData() ?? [];
13743
13744 if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
13745 throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
13746 }
13747
13748 if (null !== $dataToMergeInto && !\is_array($dataToMergeInto) && !($dataToMergeInto instanceof \Traversable && $dataToMergeInto instanceof \ArrayAccess)) {
13749 throw new UnexpectedTypeException($dataToMergeInto, 'array or (\Traversable and \ArrayAccess)');
13750 }
13751
13752 // If we are not allowed to change anything, return immediately
13753 if ($data === $dataToMergeInto || (!$this->allowAdd && !$this->allowDelete)) {
13754 $event->setData($dataToMergeInto);
13755
13756 return;
13757 }
13758
13759 if (null === $dataToMergeInto) {
13760 // No original data was set. Set it if allowed
13761 if ($this->allowAdd) {
13762 $dataToMergeInto = $data;
13763 }
13764 } else {
13765 // Calculate delta
13766 $itemsToAdd = \is_object($data) ? clone $data : $data;
13767 $itemsToDelete = [];
13768
13769 foreach ($dataToMergeInto as $beforeKey => $beforeItem) {
13770 foreach ($data as $afterKey => $afterItem) {
13771 if ($afterItem === $beforeItem) {
13772 // Item found, next original item
13773 unset($itemsToAdd[$afterKey]);
13774 continue 2;
13775 }
13776 }
13777
13778 // Item not found, remember for deletion
13779 $itemsToDelete[] = $beforeKey;
13780 }
13781
13782 // Remove deleted items before adding to free keys that are to be
13783 // replaced
13784 if ($this->allowDelete) {
13785 foreach ($itemsToDelete as $key) {
13786 unset($dataToMergeInto[$key]);
13787 }
13788 }
13789
13790 // Add remaining items
13791 if ($this->allowAdd) {
13792 foreach ($itemsToAdd as $key => $item) {
13793 if (!isset($dataToMergeInto[$key])) {
13794 $dataToMergeInto[$key] = $item;
13795 } else {
13796 $dataToMergeInto[] = $item;
13797 }
13798 }
13799 }
13800 }
13801
13802 $event->setData($dataToMergeInto);
13803 }
13804}
13805
13806------------------------------------------------------------------------------------------------------------------------
13807 ./Extension/Core/EventListener/TransformationFailureListener.php
13808------------------------------------------------------------------------------------------------------------------------
13809<?php
13810
13811/*
13812 * This file is part of the Symfony package.
13813 *
13814 * (c) Fabien Potencier <fabien@symfony.com>
13815 *
13816 * For the full copyright and license information, please view the LICENSE
13817 * file that was distributed with this source code.
13818 */
13819
13820namespace Symfony\Component\Form\Extension\Core\EventListener;
13821
13822use Symfony\Component\EventDispatcher\EventSubscriberInterface;
13823use Symfony\Component\Form\FormError;
13824use Symfony\Component\Form\FormEvent;
13825use Symfony\Component\Form\FormEvents;
13826use Symfony\Contracts\Translation\TranslatorInterface;
13827
13828/**
13829 * @author Christian Flothmann <christian.flothmann@sensiolabs.de>
13830 */
13831class TransformationFailureListener implements EventSubscriberInterface
13832{
13833 public function __construct(
13834 private ?TranslatorInterface $translator = null,
13835 ) {
13836 }
13837
13838 public static function getSubscribedEvents(): array
13839 {
13840 return [
13841 FormEvents::POST_SUBMIT => ['convertTransformationFailureToFormError', -1024],
13842 ];
13843 }
13844
13845 public function convertTransformationFailureToFormError(FormEvent $event): void
13846 {
13847 $form = $event->getForm();
13848
13849 if (null === $form->getTransformationFailure() || !$form->isValid()) {
13850 return;
13851 }
13852
13853 foreach ($form as $child) {
13854 if (!$child->isSynchronized()) {
13855 return;
13856 }
13857 }
13858
13859 $clientDataAsString = \is_scalar($form->getViewData()) ? (string) $form->getViewData() : get_debug_type($form->getViewData());
13860 $messageTemplate = $form->getConfig()->getOption('invalid_message', 'The value {{ value }} is not valid.');
13861 $messageParameters = array_replace(['{{ value }}' => $clientDataAsString], $form->getConfig()->getOption('invalid_message_parameters', []));
13862
13863 if (null !== $this->translator) {
13864 $message = $this->translator->trans($messageTemplate, $messageParameters);
13865 } else {
13866 $message = strtr($messageTemplate, $messageParameters);
13867 }
13868
13869 $form->addError(new FormError($message, $messageTemplate, $messageParameters, null, $form->getTransformationFailure()));
13870 }
13871}
13872
13873------------------------------------------------------------------------------------------------------------------------
13874 ./Extension/Core/CoreExtension.php
13875------------------------------------------------------------------------------------------------------------------------
13876<?php
13877
13878/*
13879 * This file is part of the Symfony package.
13880 *
13881 * (c) Fabien Potencier <fabien@symfony.com>
13882 *
13883 * For the full copyright and license information, please view the LICENSE
13884 * file that was distributed with this source code.
13885 */
13886
13887namespace Symfony\Component\Form\Extension\Core;
13888
13889use Symfony\Component\Form\AbstractExtension;
13890use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
13891use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
13892use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
13893use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
13894use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension;
13895use Symfony\Component\PropertyAccess\PropertyAccess;
13896use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
13897use Symfony\Contracts\Translation\TranslatorInterface;
13898
13899/**
13900 * Represents the main form extension, which loads the core functionality.
13901 *
13902 * @author Bernhard Schussek <bschussek@gmail.com>
13903 */
13904class CoreExtension extends AbstractExtension
13905{
13906 private PropertyAccessorInterface $propertyAccessor;
13907 private ChoiceListFactoryInterface $choiceListFactory;
13908
13909 public function __construct(
13910 ?PropertyAccessorInterface $propertyAccessor = null,
13911 ?ChoiceListFactoryInterface $choiceListFactory = null,
13912 private ?TranslatorInterface $translator = null,
13913 ) {
13914 $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
13915 $this->choiceListFactory = $choiceListFactory ?? new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
13916 }
13917
13918 protected function loadTypes(): array
13919 {
13920 return [
13921 new Type\FormType($this->propertyAccessor),
13922 new Type\BirthdayType(),
13923 new Type\CheckboxType(),
13924 new Type\ChoiceType($this->choiceListFactory, $this->translator),
13925 new Type\CollectionType(),
13926 new Type\CountryType(),
13927 new Type\DateIntervalType(),
13928 new Type\DateType(),
13929 new Type\DateTimeType(),
13930 new Type\EmailType(),
13931 new Type\HiddenType(),
13932 new Type\IntegerType(),
13933 new Type\LanguageType(),
13934 new Type\LocaleType(),
13935 new Type\MoneyType(),
13936 new Type\NumberType(),
13937 new Type\PasswordType(),
13938 new Type\PercentType(),
13939 new Type\RadioType(),
13940 new Type\RangeType(),
13941 new Type\RepeatedType(),
13942 new Type\SearchType(),
13943 new Type\TextareaType(),
13944 new Type\TextType(),
13945 new Type\TimeType(),
13946 new Type\TimezoneType(),
13947 new Type\UrlType(),
13948 new Type\FileType($this->translator),
13949 new Type\ButtonType(),
13950 new Type\SubmitType(),
13951 new Type\ResetType(),
13952 new Type\CurrencyType(),
13953 new Type\TelType(),
13954 new Type\ColorType($this->translator),
13955 new Type\WeekType(),
13956 ];
13957 }
13958
13959 protected function loadTypeExtensions(): array
13960 {
13961 return [
13962 new TransformationFailureExtension($this->translator),
13963 ];
13964 }
13965}
13966
13967------------------------------------------------------------------------------------------------------------------------
13968 ./Extension/Core/Type/IntegerType.php
13969------------------------------------------------------------------------------------------------------------------------
13970<?php
13971
13972/*
13973 * This file is part of the Symfony package.
13974 *
13975 * (c) Fabien Potencier <fabien@symfony.com>
13976 *
13977 * For the full copyright and license information, please view the LICENSE
13978 * file that was distributed with this source code.
13979 */
13980
13981namespace Symfony\Component\Form\Extension\Core\Type;
13982
13983use Symfony\Component\Form\AbstractType;
13984use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
13985use Symfony\Component\Form\FormBuilderInterface;
13986use Symfony\Component\Form\FormInterface;
13987use Symfony\Component\Form\FormView;
13988use Symfony\Component\OptionsResolver\OptionsResolver;
13989
13990class IntegerType extends AbstractType
13991{
13992 public function buildForm(FormBuilderInterface $builder, array $options): void
13993 {
13994 $builder->addViewTransformer(new IntegerToLocalizedStringTransformer($options['grouping'], $options['rounding_mode'], !$options['grouping'] ? 'en' : null));
13995 }
13996
13997 public function buildView(FormView $view, FormInterface $form, array $options): void
13998 {
13999 if ($options['grouping']) {
14000 $view->vars['type'] = 'text';
14001 }
14002 }
14003
14004 public function configureOptions(OptionsResolver $resolver): void
14005 {
14006 $resolver->setDefaults([
14007 'grouping' => false,
14008 // Integer cast rounds towards 0, so do the same when displaying fractions
14009 'rounding_mode' => \NumberFormatter::ROUND_DOWN,
14010 'compound' => false,
14011 'invalid_message' => 'Please enter an integer.',
14012 ]);
14013
14014 $resolver->setAllowedValues('rounding_mode', [
14015 \NumberFormatter::ROUND_FLOOR,
14016 \NumberFormatter::ROUND_DOWN,
14017 \NumberFormatter::ROUND_HALFDOWN,
14018 \NumberFormatter::ROUND_HALFEVEN,
14019 \NumberFormatter::ROUND_HALFUP,
14020 \NumberFormatter::ROUND_UP,
14021 \NumberFormatter::ROUND_CEILING,
14022 ]);
14023 }
14024
14025 public function getBlockPrefix(): string
14026 {
14027 return 'integer';
14028 }
14029}
14030
14031------------------------------------------------------------------------------------------------------------------------
14032 ./Extension/Core/Type/NumberType.php
14033------------------------------------------------------------------------------------------------------------------------
14034<?php
14035
14036/*
14037 * This file is part of the Symfony package.
14038 *
14039 * (c) Fabien Potencier <fabien@symfony.com>
14040 *
14041 * For the full copyright and license information, please view the LICENSE
14042 * file that was distributed with this source code.
14043 */
14044
14045namespace Symfony\Component\Form\Extension\Core\Type;
14046
14047use Symfony\Component\Form\AbstractType;
14048use Symfony\Component\Form\Exception\LogicException;
14049use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
14050use Symfony\Component\Form\Extension\Core\DataTransformer\StringToFloatTransformer;
14051use Symfony\Component\Form\FormBuilderInterface;
14052use Symfony\Component\Form\FormInterface;
14053use Symfony\Component\Form\FormView;
14054use Symfony\Component\OptionsResolver\Options;
14055use Symfony\Component\OptionsResolver\OptionsResolver;
14056
14057class NumberType extends AbstractType
14058{
14059 public function buildForm(FormBuilderInterface $builder, array $options): void
14060 {
14061 $builder->addViewTransformer(new NumberToLocalizedStringTransformer(
14062 $options['scale'],
14063 $options['grouping'],
14064 $options['rounding_mode'],
14065 $options['html5'] ? 'en' : null
14066 ));
14067
14068 if ('string' === $options['input']) {
14069 $builder->addModelTransformer(new StringToFloatTransformer($options['scale']));
14070 }
14071 }
14072
14073 public function buildView(FormView $view, FormInterface $form, array $options): void
14074 {
14075 if ($options['html5']) {
14076 $view->vars['type'] = 'number';
14077
14078 if (!isset($view->vars['attr']['step'])) {
14079 $view->vars['attr']['step'] = 'any';
14080 }
14081 } else {
14082 $view->vars['attr']['inputmode'] = 0 === $options['scale'] ? 'numeric' : 'decimal';
14083 }
14084 }
14085
14086 public function configureOptions(OptionsResolver $resolver): void
14087 {
14088 $resolver->setDefaults([
14089 // default scale is locale specific (usually around 3)
14090 'scale' => null,
14091 'grouping' => false,
14092 'rounding_mode' => \NumberFormatter::ROUND_HALFUP,
14093 'compound' => false,
14094 'input' => 'number',
14095 'html5' => false,
14096 'invalid_message' => 'Please enter a number.',
14097 ]);
14098
14099 $resolver->setAllowedValues('rounding_mode', [
14100 \NumberFormatter::ROUND_FLOOR,
14101 \NumberFormatter::ROUND_DOWN,
14102 \NumberFormatter::ROUND_HALFDOWN,
14103 \NumberFormatter::ROUND_HALFEVEN,
14104 \NumberFormatter::ROUND_HALFUP,
14105 \NumberFormatter::ROUND_UP,
14106 \NumberFormatter::ROUND_CEILING,
14107 ]);
14108 $resolver->setAllowedValues('input', ['number', 'string']);
14109 $resolver->setAllowedTypes('scale', ['null', 'int']);
14110 $resolver->setAllowedTypes('html5', 'bool');
14111
14112 $resolver->setNormalizer('grouping', static function (Options $options, $value) {
14113 if (true === $value && $options['html5']) {
14114 throw new LogicException('Cannot use the "grouping" option when the "html5" option is enabled.');
14115 }
14116
14117 return $value;
14118 });
14119 }
14120
14121 public function getBlockPrefix(): string
14122 {
14123 return 'number';
14124 }
14125}
14126
14127------------------------------------------------------------------------------------------------------------------------
14128 ./Extension/Core/Type/TransformationFailureExtension.php
14129------------------------------------------------------------------------------------------------------------------------
14130<?php
14131
14132/*
14133 * This file is part of the Symfony package.
14134 *
14135 * (c) Fabien Potencier <fabien@symfony.com>
14136 *
14137 * For the full copyright and license information, please view the LICENSE
14138 * file that was distributed with this source code.
14139 */
14140
14141namespace Symfony\Component\Form\Extension\Core\Type;
14142
14143use Symfony\Component\Form\AbstractTypeExtension;
14144use Symfony\Component\Form\Extension\Core\EventListener\TransformationFailureListener;
14145use Symfony\Component\Form\FormBuilderInterface;
14146use Symfony\Contracts\Translation\TranslatorInterface;
14147
14148/**
14149 * @author Christian Flothmann <christian.flothmann@sensiolabs.de>
14150 */
14151class TransformationFailureExtension extends AbstractTypeExtension
14152{
14153 public function __construct(
14154 private ?TranslatorInterface $translator = null,
14155 ) {
14156 }
14157
14158 public function buildForm(FormBuilderInterface $builder, array $options): void
14159 {
14160 if (!isset($options['constraints'])) {
14161 $builder->addEventSubscriber(new TransformationFailureListener($this->translator));
14162 }
14163 }
14164
14165 public static function getExtendedTypes(): iterable
14166 {
14167 return [FormType::class];
14168 }
14169}
14170
14171------------------------------------------------------------------------------------------------------------------------
14172 ./Extension/Core/Type/DateIntervalType.php
14173------------------------------------------------------------------------------------------------------------------------
14174<?php
14175
14176/*
14177 * This file is part of the Symfony package.
14178 *
14179 * (c) Fabien Potencier <fabien@symfony.com>
14180 *
14181 * For the full copyright and license information, please view the LICENSE
14182 * file that was distributed with this source code.
14183 */
14184
14185namespace Symfony\Component\Form\Extension\Core\Type;
14186
14187use Symfony\Component\Form\AbstractType;
14188use Symfony\Component\Form\Exception\InvalidConfigurationException;
14189use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToArrayTransformer;
14190use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToStringTransformer;
14191use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
14192use Symfony\Component\Form\FormBuilderInterface;
14193use Symfony\Component\Form\FormInterface;
14194use Symfony\Component\Form\FormView;
14195use Symfony\Component\Form\ReversedTransformer;
14196use Symfony\Component\OptionsResolver\Options;
14197use Symfony\Component\OptionsResolver\OptionsResolver;
14198
14199/**
14200 * @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
14201 */
14202class DateIntervalType extends AbstractType
14203{
14204 private const TIME_PARTS = [
14205 'years',
14206 'months',
14207 'weeks',
14208 'days',
14209 'hours',
14210 'minutes',
14211 'seconds',
14212 ];
14213 private const WIDGETS = [
14214 'text' => TextType::class,
14215 'integer' => IntegerType::class,
14216 'choice' => ChoiceType::class,
14217 ];
14218
14219 public function buildForm(FormBuilderInterface $builder, array $options): void
14220 {
14221 if (!$options['with_years'] && !$options['with_months'] && !$options['with_weeks'] && !$options['with_days'] && !$options['with_hours'] && !$options['with_minutes'] && !$options['with_seconds']) {
14222 throw new InvalidConfigurationException('You must enable at least one interval field.');
14223 }
14224 if ($options['with_invert'] && 'single_text' === $options['widget']) {
14225 throw new InvalidConfigurationException('The single_text widget does not support invertible intervals.');
14226 }
14227 if ($options['with_weeks'] && $options['with_days']) {
14228 throw new InvalidConfigurationException('You cannot enable weeks and days fields together.');
14229 }
14230 $format = 'P';
14231 $parts = [];
14232 if ($options['with_years']) {
14233 $format .= '%yY';
14234 $parts[] = 'years';
14235 }
14236 if ($options['with_months']) {
14237 $format .= '%mM';
14238 $parts[] = 'months';
14239 }
14240 if ($options['with_weeks']) {
14241 $format .= '%wW';
14242 $parts[] = 'weeks';
14243 }
14244 if ($options['with_days']) {
14245 $format .= '%dD';
14246 $parts[] = 'days';
14247 }
14248 if ($options['with_hours'] || $options['with_minutes'] || $options['with_seconds']) {
14249 $format .= 'T';
14250 }
14251 if ($options['with_hours']) {
14252 $format .= '%hH';
14253 $parts[] = 'hours';
14254 }
14255 if ($options['with_minutes']) {
14256 $format .= '%iM';
14257 $parts[] = 'minutes';
14258 }
14259 if ($options['with_seconds']) {
14260 $format .= '%sS';
14261 $parts[] = 'seconds';
14262 }
14263 if ($options['with_invert']) {
14264 $parts[] = 'invert';
14265 }
14266 if ('single_text' === $options['widget']) {
14267 $builder->addViewTransformer(new DateIntervalToStringTransformer($format));
14268 } else {
14269 foreach (self::TIME_PARTS as $part) {
14270 if ($options['with_'.$part]) {
14271 $childOptions = [
14272 'error_bubbling' => true,
14273 'label' => $options['labels'][$part],
14274 // Append generic carry-along options
14275 'required' => $options['required'],
14276 'translation_domain' => $options['translation_domain'],
14277 // when compound the array entries are ignored, we need to cascade the configuration here
14278 'empty_data' => $options['empty_data'][$part] ?? null,
14279 ];
14280 if ('choice' === $options['widget']) {
14281 $childOptions['choice_translation_domain'] = false;
14282 $childOptions['choices'] = $options[$part];
14283 $childOptions['placeholder'] = $options['placeholder'][$part];
14284 }
14285 $childForm = $builder->create($part, self::WIDGETS[$options['widget']], $childOptions);
14286 if ('integer' === $options['widget']) {
14287 $childForm->addModelTransformer(
14288 new ReversedTransformer(
14289 new IntegerToLocalizedStringTransformer()
14290 )
14291 );
14292 }
14293 $builder->add($childForm);
14294 }
14295 }
14296 if ($options['with_invert']) {
14297 $builder->add('invert', CheckboxType::class, [
14298 'label' => $options['labels']['invert'],
14299 'error_bubbling' => true,
14300 'required' => false,
14301 'translation_domain' => $options['translation_domain'],
14302 ]);
14303 }
14304 $builder->addViewTransformer(new DateIntervalToArrayTransformer($parts, 'text' === $options['widget']));
14305 }
14306 if ('string' === $options['input']) {
14307 $builder->addModelTransformer(
14308 new ReversedTransformer(
14309 new DateIntervalToStringTransformer($format)
14310 )
14311 );
14312 } elseif ('array' === $options['input']) {
14313 $builder->addModelTransformer(
14314 new ReversedTransformer(
14315 new DateIntervalToArrayTransformer($parts)
14316 )
14317 );
14318 }
14319 }
14320
14321 public function buildView(FormView $view, FormInterface $form, array $options): void
14322 {
14323 $vars = [
14324 'widget' => $options['widget'],
14325 'with_invert' => $options['with_invert'],
14326 ];
14327 foreach (self::TIME_PARTS as $part) {
14328 $vars['with_'.$part] = $options['with_'.$part];
14329 }
14330 $view->vars = array_replace($view->vars, $vars);
14331 }
14332
14333 public function configureOptions(OptionsResolver $resolver): void
14334 {
14335 $compound = static fn (Options $options) => 'single_text' !== $options['widget'];
14336 $emptyData = static fn (Options $options) => 'single_text' === $options['widget'] ? '' : [];
14337
14338 $placeholderDefault = static fn (Options $options) => $options['required'] ? null : '';
14339
14340 $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) {
14341 if (\is_array($placeholder)) {
14342 $default = $placeholderDefault($options);
14343
14344 return array_merge(array_fill_keys(self::TIME_PARTS, $default), $placeholder);
14345 }
14346
14347 return array_fill_keys(self::TIME_PARTS, $placeholder);
14348 };
14349
14350 $labelsNormalizer = static fn (Options $options, array $labels) => array_replace([
14351 'years' => null,
14352 'months' => null,
14353 'days' => null,
14354 'weeks' => null,
14355 'hours' => null,
14356 'minutes' => null,
14357 'seconds' => null,
14358 'invert' => 'Negative interval',
14359 ], array_filter($labels, static fn ($label) => null !== $label));
14360
14361 $resolver->setDefaults([
14362 'with_years' => true,
14363 'with_months' => true,
14364 'with_days' => true,
14365 'with_weeks' => false,
14366 'with_hours' => false,
14367 'with_minutes' => false,
14368 'with_seconds' => false,
14369 'with_invert' => false,
14370 'years' => range(0, 100),
14371 'months' => range(0, 12),
14372 'weeks' => range(0, 52),
14373 'days' => range(0, 31),
14374 'hours' => range(0, 24),
14375 'minutes' => range(0, 60),
14376 'seconds' => range(0, 60),
14377 'widget' => 'choice',
14378 'input' => 'dateinterval',
14379 'placeholder' => $placeholderDefault,
14380 'by_reference' => true,
14381 'error_bubbling' => false,
14382 // If initialized with a \DateInterval object, FormType initializes
14383 // this option to "\DateInterval". Since the internal, normalized
14384 // representation is not \DateInterval, but an array, we need to unset
14385 // this option.
14386 'data_class' => null,
14387 'compound' => $compound,
14388 'empty_data' => $emptyData,
14389 'labels' => [],
14390 'invalid_message' => 'Please choose a valid date interval.',
14391 ]);
14392 $resolver->setNormalizer('placeholder', $placeholderNormalizer);
14393 $resolver->setNormalizer('labels', $labelsNormalizer);
14394
14395 $resolver->setAllowedValues(
14396 'input',
14397 [
14398 'dateinterval',
14399 'string',
14400 'array',
14401 ]
14402 );
14403 $resolver->setAllowedValues(
14404 'widget',
14405 [
14406 'single_text',
14407 'text',
14408 'integer',
14409 'choice',
14410 ]
14411 );
14412 // Don't clone \DateInterval classes, as i.e. format()
14413 // does not work after that
14414 $resolver->setAllowedValues('by_reference', true);
14415
14416 $resolver->setAllowedTypes('years', 'array');
14417 $resolver->setAllowedTypes('months', 'array');
14418 $resolver->setAllowedTypes('weeks', 'array');
14419 $resolver->setAllowedTypes('days', 'array');
14420 $resolver->setAllowedTypes('hours', 'array');
14421 $resolver->setAllowedTypes('minutes', 'array');
14422 $resolver->setAllowedTypes('seconds', 'array');
14423 $resolver->setAllowedTypes('with_years', 'bool');
14424 $resolver->setAllowedTypes('with_months', 'bool');
14425 $resolver->setAllowedTypes('with_weeks', 'bool');
14426 $resolver->setAllowedTypes('with_days', 'bool');
14427 $resolver->setAllowedTypes('with_hours', 'bool');
14428 $resolver->setAllowedTypes('with_minutes', 'bool');
14429 $resolver->setAllowedTypes('with_seconds', 'bool');
14430 $resolver->setAllowedTypes('with_invert', 'bool');
14431 $resolver->setAllowedTypes('labels', 'array');
14432 }
14433
14434 public function getBlockPrefix(): string
14435 {
14436 return 'dateinterval';
14437 }
14438}
14439
14440------------------------------------------------------------------------------------------------------------------------
14441 ./Extension/Core/Type/EnumType.php
14442------------------------------------------------------------------------------------------------------------------------
14443<?php
14444
14445/*
14446 * This file is part of the Symfony package.
14447 *
14448 * (c) Fabien Potencier <fabien@symfony.com>
14449 *
14450 * For the full copyright and license information, please view the LICENSE
14451 * file that was distributed with this source code.
14452 */
14453
14454namespace Symfony\Component\Form\Extension\Core\Type;
14455
14456use Symfony\Component\Form\AbstractType;
14457use Symfony\Component\OptionsResolver\Options;
14458use Symfony\Component\OptionsResolver\OptionsResolver;
14459use Symfony\Contracts\Translation\TranslatableInterface;
14460
14461/**
14462 * A choice type for native PHP enums.
14463 *
14464 * @author Alexander M. Turek <me@derrabus.de>
14465 */
14466final class EnumType extends AbstractType
14467{
14468 public function configureOptions(OptionsResolver $resolver): void
14469 {
14470 $resolver
14471 ->setRequired(['class'])
14472 ->setAllowedTypes('class', 'string')
14473 ->setAllowedValues('class', enum_exists(...))
14474 ->setDefault('choices', static fn (Options $options): array => $options['class']::cases())
14475 ->setDefault('choice_label', static fn (\UnitEnum $choice) => $choice instanceof TranslatableInterface ? $choice : $choice->name)
14476 ->setDefault('choice_value', static function (Options $options): ?\Closure {
14477 if (!is_a($options['class'], \BackedEnum::class, true)) {
14478 return null;
14479 }
14480
14481 return static function (?\BackedEnum $choice): ?string {
14482 if (null === $choice) {
14483 return null;
14484 }
14485
14486 return (string) $choice->value;
14487 };
14488 })
14489 ;
14490 }
14491
14492 public function getParent(): string
14493 {
14494 return ChoiceType::class;
14495 }
14496}
14497
14498------------------------------------------------------------------------------------------------------------------------
14499 ./Extension/Core/Type/BaseType.php
14500------------------------------------------------------------------------------------------------------------------------
14501<?php
14502
14503/*
14504 * This file is part of the Symfony package.
14505 *
14506 * (c) Fabien Potencier <fabien@symfony.com>
14507 *
14508 * For the full copyright and license information, please view the LICENSE
14509 * file that was distributed with this source code.
14510 */
14511
14512namespace Symfony\Component\Form\Extension\Core\Type;
14513
14514use Symfony\Component\Form\AbstractRendererEngine;
14515use Symfony\Component\Form\AbstractType;
14516use Symfony\Component\Form\Exception\LogicException;
14517use Symfony\Component\Form\FormBuilderInterface;
14518use Symfony\Component\Form\FormInterface;
14519use Symfony\Component\Form\FormView;
14520use Symfony\Component\OptionsResolver\OptionsResolver;
14521
14522/**
14523 * Encapsulates common logic of {@link FormType} and {@link ButtonType}.
14524 *
14525 * This type does not appear in the form's type inheritance chain and as such
14526 * cannot be extended (via {@link \Symfony\Component\Form\FormExtensionInterface}) nor themed.
14527 *
14528 * @author Bernhard Schussek <bschussek@gmail.com>
14529 */
14530abstract class BaseType extends AbstractType
14531{
14532 public function buildForm(FormBuilderInterface $builder, array $options): void
14533 {
14534 $builder->setDisabled($options['disabled']);
14535 $builder->setAutoInitialize($options['auto_initialize']);
14536 }
14537
14538 public function buildView(FormView $view, FormInterface $form, array $options): void
14539 {
14540 $name = $form->getName();
14541 $blockName = $options['block_name'] ?: $form->getName();
14542 $translationDomain = $options['translation_domain'];
14543 $labelTranslationParameters = $options['label_translation_parameters'];
14544 $attrTranslationParameters = $options['attr_translation_parameters'];
14545 $labelFormat = $options['label_format'];
14546
14547 if ($view->parent) {
14548 if ('' !== ($parentFullName = $view->parent->vars['full_name'])) {
14549 $id = \sprintf('%s_%s', $view->parent->vars['id'], $name);
14550 $fullName = \sprintf('%s[%s]', $parentFullName, $name);
14551 $uniqueBlockPrefix = \sprintf('%s_%s', $view->parent->vars['unique_block_prefix'], $blockName);
14552 } else {
14553 $id = $name;
14554 $fullName = $name;
14555 $uniqueBlockPrefix = '_'.$blockName;
14556 }
14557
14558 $translationDomain ??= $view->parent->vars['translation_domain'];
14559
14560 $labelTranslationParameters = array_merge($view->parent->vars['label_translation_parameters'], $labelTranslationParameters);
14561 $attrTranslationParameters = array_merge($view->parent->vars['attr_translation_parameters'], $attrTranslationParameters);
14562
14563 if (!$labelFormat) {
14564 $labelFormat = $view->parent->vars['label_format'];
14565 }
14566
14567 $rootFormAttrOption = $form->getRoot()->getConfig()->getOption('form_attr');
14568 if ($options['form_attr'] || $rootFormAttrOption) {
14569 $options['attr']['form'] = \is_string($rootFormAttrOption) ? $rootFormAttrOption : $form->getRoot()->getName();
14570 if (empty($options['attr']['form'])) {
14571 throw new LogicException('"form_attr" option must be a string identifier on root form when it has no id.');
14572 }
14573 }
14574 } else {
14575 $id = \is_string($options['form_attr']) ? $options['form_attr'] : $name;
14576 $fullName = $name;
14577 $uniqueBlockPrefix = '_'.$blockName;
14578
14579 // Strip leading underscores and digits. These are allowed in
14580 // form names, but not in HTML4 ID attributes.
14581 // https://www.w3.org/TR/html401/struct/global#adef-id
14582 $id = ltrim($id, '_0123456789');
14583 }
14584
14585 $blockPrefixes = [];
14586 for ($type = $form->getConfig()->getType(); null !== $type; $type = $type->getParent()) {
14587 array_unshift($blockPrefixes, $type->getBlockPrefix());
14588 }
14589 if (null !== $options['block_prefix']) {
14590 $blockPrefixes[] = $options['block_prefix'];
14591 }
14592 $blockPrefixes[] = $uniqueBlockPrefix;
14593
14594 $view->vars = array_replace($view->vars, [
14595 'form' => $view,
14596 'id' => $id,
14597 'name' => $name,
14598 'full_name' => $fullName,
14599 'disabled' => $form->isDisabled(),
14600 'label' => $options['label'],
14601 'label_format' => $labelFormat,
14602 'label_html' => $options['label_html'],
14603 'multipart' => false,
14604 'attr' => $options['attr'],
14605 'block_prefixes' => $blockPrefixes,
14606 'unique_block_prefix' => $uniqueBlockPrefix,
14607 'row_attr' => $options['row_attr'],
14608 'translation_domain' => $translationDomain,
14609 'label_translation_parameters' => $labelTranslationParameters,
14610 'attr_translation_parameters' => $attrTranslationParameters,
14611 'priority' => $options['priority'],
14612 // Using the block name here speeds up performance in collection
14613 // forms, where each entry has the same full block name.
14614 // Including the type is important too, because if rows of a
14615 // collection form have different types (dynamically), they should
14616 // be rendered differently.
14617 // https://github.com/symfony/symfony/issues/5038
14618 AbstractRendererEngine::CACHE_KEY_VAR => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(),
14619 ]);
14620 }
14621
14622 public function configureOptions(OptionsResolver $resolver): void
14623 {
14624 $resolver->setDefaults([
14625 'block_name' => null,
14626 'block_prefix' => null,
14627 'disabled' => false,
14628 'label' => null,
14629 'label_format' => null,
14630 'row_attr' => [],
14631 'label_html' => false,
14632 'label_translation_parameters' => [],
14633 'attr_translation_parameters' => [],
14634 'attr' => [],
14635 'translation_domain' => null,
14636 'auto_initialize' => true,
14637 'priority' => 0,
14638 'form_attr' => false,
14639 ]);
14640
14641 $resolver->setAllowedTypes('block_prefix', ['null', 'string']);
14642 $resolver->setAllowedTypes('attr', 'array');
14643 $resolver->setAllowedTypes('row_attr', 'array');
14644 $resolver->setAllowedTypes('label_html', 'bool');
14645 $resolver->setAllowedTypes('priority', 'int');
14646 $resolver->setAllowedTypes('form_attr', ['bool', 'string']);
14647
14648 $resolver->setInfo('priority', 'The form rendering priority (higher priorities will be rendered first)');
14649 }
14650}
14651
14652------------------------------------------------------------------------------------------------------------------------
14653 ./Extension/Core/Type/UlidType.php
14654------------------------------------------------------------------------------------------------------------------------
14655<?php
14656
14657/*
14658 * This file is part of the Symfony package.
14659 *
14660 * (c) Fabien Potencier <fabien@symfony.com>
14661 *
14662 * For the full copyright and license information, please view the LICENSE
14663 * file that was distributed with this source code.
14664 */
14665
14666namespace Symfony\Component\Form\Extension\Core\Type;
14667
14668use Symfony\Component\Form\AbstractType;
14669use Symfony\Component\Form\Extension\Core\DataTransformer\UlidToStringTransformer;
14670use Symfony\Component\Form\FormBuilderInterface;
14671use Symfony\Component\OptionsResolver\OptionsResolver;
14672
14673/**
14674 * @author Pavel Dyakonov <wapinet@mail.ru>
14675 */
14676class UlidType extends AbstractType
14677{
14678 public function buildForm(FormBuilderInterface $builder, array $options): void
14679 {
14680 $builder
14681 ->addViewTransformer(new UlidToStringTransformer())
14682 ;
14683 }
14684
14685 public function configureOptions(OptionsResolver $resolver): void
14686 {
14687 $resolver->setDefaults([
14688 'compound' => false,
14689 'invalid_message' => 'Please enter a valid ULID.',
14690 ]);
14691 }
14692}
14693
14694------------------------------------------------------------------------------------------------------------------------
14695 ./Extension/Core/Type/UrlType.php
14696------------------------------------------------------------------------------------------------------------------------
14697<?php
14698
14699/*
14700 * This file is part of the Symfony package.
14701 *
14702 * (c) Fabien Potencier <fabien@symfony.com>
14703 *
14704 * For the full copyright and license information, please view the LICENSE
14705 * file that was distributed with this source code.
14706 */
14707
14708namespace Symfony\Component\Form\Extension\Core\Type;
14709
14710use Symfony\Component\Form\AbstractType;
14711use Symfony\Component\Form\Extension\Core\EventListener\FixUrlProtocolListener;
14712use Symfony\Component\Form\FormBuilderInterface;
14713use Symfony\Component\Form\FormInterface;
14714use Symfony\Component\Form\FormView;
14715use Symfony\Component\OptionsResolver\Options;
14716use Symfony\Component\OptionsResolver\OptionsResolver;
14717
14718class UrlType extends AbstractType
14719{
14720 public function buildForm(FormBuilderInterface $builder, array $options): void
14721 {
14722 if (null !== $options['default_protocol']) {
14723 $builder->addEventSubscriber(new FixUrlProtocolListener($options['default_protocol']));
14724 }
14725 }
14726
14727 public function buildView(FormView $view, FormInterface $form, array $options): void
14728 {
14729 if ($options['default_protocol']) {
14730 $view->vars['attr']['inputmode'] = 'url';
14731 $view->vars['type'] = 'text';
14732 }
14733 }
14734
14735 public function configureOptions(OptionsResolver $resolver): void
14736 {
14737 $resolver->setDefaults([
14738 'default_protocol' => static function (Options $options) {
14739 trigger_deprecation('symfony/form', '7.1', 'Not configuring the "default_protocol" option when using the UrlType is deprecated. It will default to "null" in 8.0.');
14740
14741 return 'http';
14742 },
14743 'invalid_message' => 'Please enter a valid URL.',
14744 ]);
14745
14746 $resolver->setAllowedTypes('default_protocol', ['null', 'string']);
14747 }
14748
14749 public function getParent(): ?string
14750 {
14751 return TextType::class;
14752 }
14753
14754 public function getBlockPrefix(): string
14755 {
14756 return 'url';
14757 }
14758}
14759
14760------------------------------------------------------------------------------------------------------------------------
14761 ./Extension/Core/Type/RepeatedType.php
14762------------------------------------------------------------------------------------------------------------------------
14763<?php
14764
14765/*
14766 * This file is part of the Symfony package.
14767 *
14768 * (c) Fabien Potencier <fabien@symfony.com>
14769 *
14770 * For the full copyright and license information, please view the LICENSE
14771 * file that was distributed with this source code.
14772 */
14773
14774namespace Symfony\Component\Form\Extension\Core\Type;
14775
14776use Symfony\Component\Form\AbstractType;
14777use Symfony\Component\Form\Extension\Core\DataTransformer\ValueToDuplicatesTransformer;
14778use Symfony\Component\Form\FormBuilderInterface;
14779use Symfony\Component\OptionsResolver\OptionsResolver;
14780
14781class RepeatedType extends AbstractType
14782{
14783 public function buildForm(FormBuilderInterface $builder, array $options): void
14784 {
14785 // Overwrite required option for child fields
14786 $options['first_options']['required'] = $options['required'];
14787 $options['second_options']['required'] = $options['required'];
14788
14789 if (!isset($options['options']['error_bubbling'])) {
14790 $options['options']['error_bubbling'] = $options['error_bubbling'];
14791 }
14792
14793 // children fields must always be mapped
14794 $defaultOptions = ['mapped' => true];
14795
14796 $builder
14797 ->addViewTransformer(new ValueToDuplicatesTransformer([
14798 $options['first_name'],
14799 $options['second_name'],
14800 ]))
14801 ->add($options['first_name'], $options['type'], array_merge($options['options'], $options['first_options'], $defaultOptions))
14802 ->add($options['second_name'], $options['type'], array_merge($options['options'], $options['second_options'], $defaultOptions))
14803 ;
14804 }
14805
14806 public function configureOptions(OptionsResolver $resolver): void
14807 {
14808 $resolver->setDefaults([
14809 'type' => TextType::class,
14810 'options' => [],
14811 'first_options' => [],
14812 'second_options' => [],
14813 'first_name' => 'first',
14814 'second_name' => 'second',
14815 'error_bubbling' => false,
14816 'invalid_message' => 'The values do not match.',
14817 ]);
14818
14819 $resolver->setAllowedTypes('options', 'array');
14820 $resolver->setAllowedTypes('first_options', 'array');
14821 $resolver->setAllowedTypes('second_options', 'array');
14822 }
14823
14824 public function getBlockPrefix(): string
14825 {
14826 return 'repeated';
14827 }
14828}
14829
14830------------------------------------------------------------------------------------------------------------------------
14831 ./Extension/Core/Type/ResetType.php
14832------------------------------------------------------------------------------------------------------------------------
14833<?php
14834
14835/*
14836 * This file is part of the Symfony package.
14837 *
14838 * (c) Fabien Potencier <fabien@symfony.com>
14839 *
14840 * For the full copyright and license information, please view the LICENSE
14841 * file that was distributed with this source code.
14842 */
14843
14844namespace Symfony\Component\Form\Extension\Core\Type;
14845
14846use Symfony\Component\Form\AbstractType;
14847use Symfony\Component\Form\ButtonTypeInterface;
14848
14849/**
14850 * A reset button.
14851 *
14852 * @author Bernhard Schussek <bschussek@gmail.com>
14853 */
14854class ResetType extends AbstractType implements ButtonTypeInterface
14855{
14856 public function getParent(): ?string
14857 {
14858 return ButtonType::class;
14859 }
14860
14861 public function getBlockPrefix(): string
14862 {
14863 return 'reset';
14864 }
14865}
14866
14867------------------------------------------------------------------------------------------------------------------------
14868 ./Extension/Core/Type/SubmitType.php
14869------------------------------------------------------------------------------------------------------------------------
14870<?php
14871
14872/*
14873 * This file is part of the Symfony package.
14874 *
14875 * (c) Fabien Potencier <fabien@symfony.com>
14876 *
14877 * For the full copyright and license information, please view the LICENSE
14878 * file that was distributed with this source code.
14879 */
14880
14881namespace Symfony\Component\Form\Extension\Core\Type;
14882
14883use Symfony\Component\Form\AbstractType;
14884use Symfony\Component\Form\FormInterface;
14885use Symfony\Component\Form\FormView;
14886use Symfony\Component\Form\SubmitButtonTypeInterface;
14887use Symfony\Component\OptionsResolver\OptionsResolver;
14888
14889/**
14890 * A submit button.
14891 *
14892 * @author Bernhard Schussek <bschussek@gmail.com>
14893 */
14894class SubmitType extends AbstractType implements SubmitButtonTypeInterface
14895{
14896 public function buildView(FormView $view, FormInterface $form, array $options): void
14897 {
14898 $view->vars['clicked'] = $form->isClicked();
14899
14900 if (!$options['validate']) {
14901 $view->vars['attr']['formnovalidate'] = true;
14902 }
14903 }
14904
14905 public function configureOptions(OptionsResolver $resolver): void
14906 {
14907 $resolver->setDefault('validate', true);
14908 $resolver->setAllowedTypes('validate', 'bool');
14909 }
14910
14911 public function getParent(): ?string
14912 {
14913 return ButtonType::class;
14914 }
14915
14916 public function getBlockPrefix(): string
14917 {
14918 return 'submit';
14919 }
14920}
14921
14922------------------------------------------------------------------------------------------------------------------------
14923 ./Extension/Core/Type/TimeType.php
14924------------------------------------------------------------------------------------------------------------------------
14925<?php
14926
14927/*
14928 * This file is part of the Symfony package.
14929 *
14930 * (c) Fabien Potencier <fabien@symfony.com>
14931 *
14932 * For the full copyright and license information, please view the LICENSE
14933 * file that was distributed with this source code.
14934 */
14935
14936namespace Symfony\Component\Form\Extension\Core\Type;
14937
14938use Symfony\Component\Form\AbstractType;
14939use Symfony\Component\Form\Exception\InvalidConfigurationException;
14940use Symfony\Component\Form\Exception\LogicException;
14941use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
14942use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
14943use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
14944use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
14945use Symfony\Component\Form\FormBuilderInterface;
14946use Symfony\Component\Form\FormEvent;
14947use Symfony\Component\Form\FormEvents;
14948use Symfony\Component\Form\FormInterface;
14949use Symfony\Component\Form\FormView;
14950use Symfony\Component\Form\ReversedTransformer;
14951use Symfony\Component\OptionsResolver\Options;
14952use Symfony\Component\OptionsResolver\OptionsResolver;
14953
14954class TimeType extends AbstractType
14955{
14956 private const WIDGETS = [
14957 'text' => TextType::class,
14958 'choice' => ChoiceType::class,
14959 ];
14960
14961 public function buildForm(FormBuilderInterface $builder, array $options): void
14962 {
14963 $parts = ['hour'];
14964 $format = 'H';
14965
14966 if ($options['with_seconds'] && !$options['with_minutes']) {
14967 throw new InvalidConfigurationException('You cannot disable minutes if you have enabled seconds.');
14968 }
14969
14970 if (null !== $options['reference_date'] && $options['reference_date']->getTimezone()->getName() !== $options['model_timezone']) {
14971 throw new InvalidConfigurationException(\sprintf('The configured "model_timezone" (%s) must match the timezone of the "reference_date" (%s).', $options['model_timezone'], $options['reference_date']->getTimezone()->getName()));
14972 }
14973
14974 if ($options['with_minutes']) {
14975 $format .= ':i';
14976 $parts[] = 'minute';
14977 }
14978
14979 if ($options['with_seconds']) {
14980 $format .= ':s';
14981 $parts[] = 'second';
14982 }
14983
14984 if ('single_text' === $options['widget']) {
14985 $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $e) use ($options) {
14986 $data = $e->getData();
14987 if ($data && preg_match('/^(?P<hours>\d{2}):(?P<minutes>\d{2})(?::(?P<seconds>\d{2})(?:\.\d+)?)?$/', $data, $matches)) {
14988 if ($options['with_seconds']) {
14989 // handle seconds ignored by user's browser when with_seconds enabled
14990 // https://codereview.chromium.org/450533009/
14991 $e->setData(\sprintf('%s:%s:%s', $matches['hours'], $matches['minutes'], $matches['seconds'] ?? '00'));
14992 } else {
14993 $e->setData(\sprintf('%s:%s', $matches['hours'], $matches['minutes']));
14994 }
14995 }
14996 });
14997
14998 $parseFormat = null;
14999
15000 if (null !== $options['reference_date']) {
15001 $parseFormat = 'Y-m-d '.$format;
15002
15003 $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($options) {
15004 $data = $event->getData();
15005
15006 if (preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $data)) {
15007 $event->setData($options['reference_date']->format('Y-m-d ').$data);
15008 }
15009 });
15010 }
15011
15012 $builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format, $parseFormat));
15013 } else {
15014 $hourOptions = $minuteOptions = $secondOptions = [
15015 'error_bubbling' => true,
15016 'empty_data' => '',
15017 ];
15018 // when the form is compound the entries of the array are ignored in favor of children data
15019 // so we need to handle the cascade setting here
15020 $emptyData = $builder->getEmptyData() ?: [];
15021
15022 if ($emptyData instanceof \Closure) {
15023 $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) {
15024 $emptyData = $emptyData($form->getParent());
15025
15026 return $emptyData[$option] ?? '';
15027 };
15028
15029 $hourOptions['empty_data'] = $lazyEmptyData('hour');
15030 } elseif (isset($emptyData['hour'])) {
15031 $hourOptions['empty_data'] = $emptyData['hour'];
15032 }
15033
15034 if (isset($options['invalid_message'])) {
15035 $hourOptions['invalid_message'] = $options['invalid_message'];
15036 $minuteOptions['invalid_message'] = $options['invalid_message'];
15037 $secondOptions['invalid_message'] = $options['invalid_message'];
15038 }
15039
15040 if (isset($options['invalid_message_parameters'])) {
15041 $hourOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
15042 $minuteOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
15043 $secondOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
15044 }
15045
15046 if ('choice' === $options['widget']) {
15047 $hours = $minutes = [];
15048
15049 foreach ($options['hours'] as $hour) {
15050 $hours[str_pad($hour, 2, '0', \STR_PAD_LEFT)] = $hour;
15051 }
15052
15053 // Only pass a subset of the options to children
15054 $hourOptions['choices'] = $hours;
15055 $hourOptions['placeholder'] = $options['placeholder']['hour'];
15056 $hourOptions['choice_translation_domain'] = $options['choice_translation_domain']['hour'];
15057
15058 if ($options['with_minutes']) {
15059 foreach ($options['minutes'] as $minute) {
15060 $minutes[str_pad($minute, 2, '0', \STR_PAD_LEFT)] = $minute;
15061 }
15062
15063 $minuteOptions['choices'] = $minutes;
15064 $minuteOptions['placeholder'] = $options['placeholder']['minute'];
15065 $minuteOptions['choice_translation_domain'] = $options['choice_translation_domain']['minute'];
15066 }
15067
15068 if ($options['with_seconds']) {
15069 $seconds = [];
15070
15071 foreach ($options['seconds'] as $second) {
15072 $seconds[str_pad($second, 2, '0', \STR_PAD_LEFT)] = $second;
15073 }
15074
15075 $secondOptions['choices'] = $seconds;
15076 $secondOptions['placeholder'] = $options['placeholder']['second'];
15077 $secondOptions['choice_translation_domain'] = $options['choice_translation_domain']['second'];
15078 }
15079
15080 // Append generic carry-along options
15081 foreach (['required', 'translation_domain'] as $passOpt) {
15082 $hourOptions[$passOpt] = $options[$passOpt];
15083
15084 if ($options['with_minutes']) {
15085 $minuteOptions[$passOpt] = $options[$passOpt];
15086 }
15087
15088 if ($options['with_seconds']) {
15089 $secondOptions[$passOpt] = $options[$passOpt];
15090 }
15091 }
15092 }
15093
15094 $builder->add('hour', self::WIDGETS[$options['widget']], $hourOptions);
15095
15096 if ($options['with_minutes']) {
15097 if ($emptyData instanceof \Closure) {
15098 $minuteOptions['empty_data'] = $lazyEmptyData('minute');
15099 } elseif (isset($emptyData['minute'])) {
15100 $minuteOptions['empty_data'] = $emptyData['minute'];
15101 }
15102 $builder->add('minute', self::WIDGETS[$options['widget']], $minuteOptions);
15103 }
15104
15105 if ($options['with_seconds']) {
15106 if ($emptyData instanceof \Closure) {
15107 $secondOptions['empty_data'] = $lazyEmptyData('second');
15108 } elseif (isset($emptyData['second'])) {
15109 $secondOptions['empty_data'] = $emptyData['second'];
15110 }
15111 $builder->add('second', self::WIDGETS[$options['widget']], $secondOptions);
15112 }
15113
15114 $builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget'], $options['reference_date']));
15115 }
15116
15117 if ('datetime_immutable' === $options['input']) {
15118 $builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
15119 } elseif ('string' === $options['input']) {
15120 $builder->addModelTransformer(new ReversedTransformer(
15121 new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format'])
15122 ));
15123 } elseif ('timestamp' === $options['input']) {
15124 $builder->addModelTransformer(new ReversedTransformer(
15125 new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
15126 ));
15127 } elseif ('array' === $options['input']) {
15128 $builder->addModelTransformer(new ReversedTransformer(
15129 new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts, 'text' === $options['widget'], $options['reference_date'])
15130 ));
15131 }
15132
15133 if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) {
15134 $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
15135 $date = $event->getData();
15136
15137 if (!$date instanceof \DateTimeInterface) {
15138 return;
15139 }
15140
15141 if ($date->getTimezone()->getName() !== $options['model_timezone']) {
15142 throw new LogicException(\sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', get_debug_type($date), $date->getTimezone()->getName(), $options['model_timezone']));
15143 }
15144 });
15145 }
15146 }
15147
15148 public function buildView(FormView $view, FormInterface $form, array $options): void
15149 {
15150 $view->vars = array_replace($view->vars, [
15151 'widget' => $options['widget'],
15152 'with_minutes' => $options['with_minutes'],
15153 'with_seconds' => $options['with_seconds'],
15154 ]);
15155
15156 // Change the input to an HTML5 time input if
15157 // * the widget is set to "single_text"
15158 // * the html5 is set to true
15159 if ($options['html5'] && 'single_text' === $options['widget']) {
15160 $view->vars['type'] = 'time';
15161
15162 // we need to force the browser to display the seconds by
15163 // adding the HTML attribute step if not already defined.
15164 // Otherwise the browser will not display and so not send the seconds
15165 // therefore the value will always be considered as invalid.
15166 if (!isset($view->vars['attr']['step'])) {
15167 if ($options['with_seconds']) {
15168 $view->vars['attr']['step'] = 1;
15169 } elseif (!$options['with_minutes']) {
15170 $view->vars['attr']['step'] = 3600;
15171 }
15172 }
15173 }
15174 }
15175
15176 public function configureOptions(OptionsResolver $resolver): void
15177 {
15178 $compound = static fn (Options $options) => 'single_text' !== $options['widget'];
15179
15180 $placeholderDefault = static fn (Options $options) => $options['required'] ? null : '';
15181
15182 $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) {
15183 if (\is_array($placeholder)) {
15184 $default = $placeholderDefault($options);
15185
15186 return array_merge(
15187 ['hour' => $default, 'minute' => $default, 'second' => $default],
15188 $placeholder
15189 );
15190 }
15191
15192 return [
15193 'hour' => $placeholder,
15194 'minute' => $placeholder,
15195 'second' => $placeholder,
15196 ];
15197 };
15198
15199 $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) {
15200 if (\is_array($choiceTranslationDomain)) {
15201 return array_replace(
15202 ['hour' => false, 'minute' => false, 'second' => false],
15203 $choiceTranslationDomain
15204 );
15205 }
15206
15207 return [
15208 'hour' => $choiceTranslationDomain,
15209 'minute' => $choiceTranslationDomain,
15210 'second' => $choiceTranslationDomain,
15211 ];
15212 };
15213
15214 $modelTimezone = static function (Options $options, $value): ?string {
15215 if (null !== $value) {
15216 return $value;
15217 }
15218
15219 if (null !== $options['reference_date']) {
15220 return $options['reference_date']->getTimezone()->getName();
15221 }
15222
15223 return null;
15224 };
15225
15226 $viewTimezone = static function (Options $options, $value): ?string {
15227 if (null !== $value) {
15228 return $value;
15229 }
15230
15231 if (null !== $options['model_timezone'] && null === $options['reference_date']) {
15232 return $options['model_timezone'];
15233 }
15234
15235 return null;
15236 };
15237
15238 $resolver->setDefaults([
15239 'hours' => range(0, 23),
15240 'minutes' => range(0, 59),
15241 'seconds' => range(0, 59),
15242 'widget' => 'single_text',
15243 'input' => 'datetime',
15244 'input_format' => 'H:i:s',
15245 'with_minutes' => true,
15246 'with_seconds' => false,
15247 'model_timezone' => $modelTimezone,
15248 'view_timezone' => $viewTimezone,
15249 'reference_date' => null,
15250 'placeholder' => $placeholderDefault,
15251 'html5' => true,
15252 // Don't modify \DateTime classes by reference, we treat
15253 // them like immutable value objects
15254 'by_reference' => false,
15255 'error_bubbling' => false,
15256 // If initialized with a \DateTime object, FormType initializes
15257 // this option to "\DateTime". Since the internal, normalized
15258 // representation is not \DateTime, but an array, we need to unset
15259 // this option.
15260 'data_class' => null,
15261 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '',
15262 'compound' => $compound,
15263 'choice_translation_domain' => false,
15264 'invalid_message' => 'Please enter a valid time.',
15265 ]);
15266
15267 $resolver->setNormalizer('view_timezone', static function (Options $options, $viewTimezone): ?string {
15268 if (null !== $options['model_timezone'] && $viewTimezone !== $options['model_timezone'] && null === $options['reference_date']) {
15269 throw new LogicException('Using different values for the "model_timezone" and "view_timezone" options without configuring a reference date is not supported.');
15270 }
15271
15272 return $viewTimezone;
15273 });
15274
15275 $resolver->setNormalizer('placeholder', $placeholderNormalizer);
15276 $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
15277
15278 $resolver->setAllowedValues('input', [
15279 'datetime',
15280 'datetime_immutable',
15281 'string',
15282 'timestamp',
15283 'array',
15284 ]);
15285 $resolver->setAllowedValues('widget', [
15286 'single_text',
15287 'text',
15288 'choice',
15289 ]);
15290
15291 $resolver->setAllowedTypes('hours', 'array');
15292 $resolver->setAllowedTypes('minutes', 'array');
15293 $resolver->setAllowedTypes('seconds', 'array');
15294 $resolver->setAllowedTypes('input_format', 'string');
15295 $resolver->setAllowedTypes('model_timezone', ['null', 'string']);
15296 $resolver->setAllowedTypes('view_timezone', ['null', 'string']);
15297 $resolver->setAllowedTypes('reference_date', ['null', \DateTimeInterface::class]);
15298 }
15299
15300 public function getBlockPrefix(): string
15301 {
15302 return 'time';
15303 }
15304}
15305
15306------------------------------------------------------------------------------------------------------------------------
15307 ./Extension/Core/Type/SearchType.php
15308------------------------------------------------------------------------------------------------------------------------
15309<?php
15310
15311/*
15312 * This file is part of the Symfony package.
15313 *
15314 * (c) Fabien Potencier <fabien@symfony.com>
15315 *
15316 * For the full copyright and license information, please view the LICENSE
15317 * file that was distributed with this source code.
15318 */
15319
15320namespace Symfony\Component\Form\Extension\Core\Type;
15321
15322use Symfony\Component\Form\AbstractType;
15323use Symfony\Component\OptionsResolver\OptionsResolver;
15324
15325class SearchType extends AbstractType
15326{
15327 public function configureOptions(OptionsResolver $resolver): void
15328 {
15329 $resolver->setDefaults([
15330 'invalid_message' => 'Please enter a valid search term.',
15331 ]);
15332 }
15333
15334 public function getParent(): ?string
15335 {
15336 return TextType::class;
15337 }
15338
15339 public function getBlockPrefix(): string
15340 {
15341 return 'search';
15342 }
15343}
15344
15345------------------------------------------------------------------------------------------------------------------------
15346 ./Extension/Core/Type/DateTimeType.php
15347------------------------------------------------------------------------------------------------------------------------
15348<?php
15349
15350/*
15351 * This file is part of the Symfony package.
15352 *
15353 * (c) Fabien Potencier <fabien@symfony.com>
15354 *
15355 * For the full copyright and license information, please view the LICENSE
15356 * file that was distributed with this source code.
15357 */
15358
15359namespace Symfony\Component\Form\Extension\Core\Type;
15360
15361use Symfony\Component\Form\AbstractType;
15362use Symfony\Component\Form\Exception\LogicException;
15363use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
15364use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
15365use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
15366use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
15367use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToHtml5LocalDateTimeTransformer;
15368use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
15369use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
15370use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
15371use Symfony\Component\Form\FormBuilderInterface;
15372use Symfony\Component\Form\FormEvent;
15373use Symfony\Component\Form\FormEvents;
15374use Symfony\Component\Form\FormInterface;
15375use Symfony\Component\Form\FormView;
15376use Symfony\Component\Form\ReversedTransformer;
15377use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
15378use Symfony\Component\OptionsResolver\Options;
15379use Symfony\Component\OptionsResolver\OptionsResolver;
15380
15381class DateTimeType extends AbstractType
15382{
15383 public const DEFAULT_DATE_FORMAT = \IntlDateFormatter::MEDIUM;
15384 public const DEFAULT_TIME_FORMAT = \IntlDateFormatter::MEDIUM;
15385
15386 /**
15387 * The HTML5 datetime-local format as defined in
15388 * http://w3c.github.io/html-reference/datatypes.html#form.data.datetime-local.
15389 */
15390 public const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
15391
15392 private const ACCEPTED_FORMATS = [
15393 \IntlDateFormatter::FULL,
15394 \IntlDateFormatter::LONG,
15395 \IntlDateFormatter::MEDIUM,
15396 \IntlDateFormatter::SHORT,
15397 ];
15398
15399 public function buildForm(FormBuilderInterface $builder, array $options): void
15400 {
15401 $parts = ['year', 'month', 'day', 'hour'];
15402 $dateParts = ['year', 'month', 'day'];
15403 $timeParts = ['hour'];
15404
15405 if ($options['with_minutes']) {
15406 $parts[] = 'minute';
15407 $timeParts[] = 'minute';
15408 }
15409
15410 if ($options['with_seconds']) {
15411 $parts[] = 'second';
15412 $timeParts[] = 'second';
15413 }
15414
15415 $dateFormat = \is_int($options['date_format']) ? $options['date_format'] : self::DEFAULT_DATE_FORMAT;
15416 $timeFormat = self::DEFAULT_TIME_FORMAT;
15417 $calendar = \IntlDateFormatter::GREGORIAN;
15418 $pattern = \is_string($options['format']) ? $options['format'] : null;
15419
15420 if (!\in_array($dateFormat, self::ACCEPTED_FORMATS, true)) {
15421 throw new InvalidOptionsException('The "date_format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
15422 }
15423
15424 if ('single_text' === $options['widget']) {
15425 if (self::HTML5_FORMAT === $pattern) {
15426 $builder->addViewTransformer(new DateTimeToHtml5LocalDateTimeTransformer(
15427 $options['model_timezone'],
15428 $options['view_timezone'],
15429 $options['with_seconds']
15430 ));
15431 } else {
15432 $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
15433 $options['model_timezone'],
15434 $options['view_timezone'],
15435 $dateFormat,
15436 $timeFormat,
15437 $calendar,
15438 $pattern
15439 ));
15440 }
15441 } else {
15442 // when the form is compound the entries of the array are ignored in favor of children data
15443 // so we need to handle the cascade setting here
15444 $emptyData = $builder->getEmptyData() ?: [];
15445 // Only pass a subset of the options to children
15446 $dateOptions = array_intersect_key($options, array_flip([
15447 'years',
15448 'months',
15449 'days',
15450 'placeholder',
15451 'choice_translation_domain',
15452 'required',
15453 'translation_domain',
15454 'html5',
15455 'invalid_message',
15456 'invalid_message_parameters',
15457 ]));
15458
15459 if ($emptyData instanceof \Closure) {
15460 $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) {
15461 $emptyData = $emptyData($form->getParent());
15462
15463 return $emptyData[$option] ?? '';
15464 };
15465
15466 $dateOptions['empty_data'] = $lazyEmptyData('date');
15467 } elseif (isset($emptyData['date'])) {
15468 $dateOptions['empty_data'] = $emptyData['date'];
15469 }
15470
15471 $timeOptions = array_intersect_key($options, array_flip([
15472 'hours',
15473 'minutes',
15474 'seconds',
15475 'with_minutes',
15476 'with_seconds',
15477 'placeholder',
15478 'choice_translation_domain',
15479 'required',
15480 'translation_domain',
15481 'html5',
15482 'invalid_message',
15483 'invalid_message_parameters',
15484 ]));
15485
15486 if ($emptyData instanceof \Closure) {
15487 $timeOptions['empty_data'] = $lazyEmptyData('time');
15488 } elseif (isset($emptyData['time'])) {
15489 $timeOptions['empty_data'] = $emptyData['time'];
15490 }
15491
15492 if (false === $options['label']) {
15493 $dateOptions['label'] = false;
15494 $timeOptions['label'] = false;
15495 }
15496
15497 $dateOptions['widget'] = $options['date_widget'] ?? $options['widget'] ?? 'choice';
15498 $timeOptions['widget'] = $options['time_widget'] ?? $options['widget'] ?? 'choice';
15499
15500 if (null !== $options['date_label']) {
15501 $dateOptions['label'] = $options['date_label'];
15502 }
15503
15504 if (null !== $options['time_label']) {
15505 $timeOptions['label'] = $options['time_label'];
15506 }
15507
15508 if (null !== $options['date_format']) {
15509 $dateOptions['format'] = $options['date_format'];
15510 }
15511
15512 $dateOptions['input'] = $timeOptions['input'] = 'array';
15513 $dateOptions['error_bubbling'] = $timeOptions['error_bubbling'] = true;
15514
15515 $builder
15516 ->addViewTransformer(new DataTransformerChain([
15517 new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts),
15518 new ArrayToPartsTransformer([
15519 'date' => $dateParts,
15520 'time' => $timeParts,
15521 ]),
15522 ]))
15523 ->add('date', DateType::class, $dateOptions)
15524 ->add('time', TimeType::class, $timeOptions)
15525 ;
15526 }
15527
15528 if ('datetime_immutable' === $options['input']) {
15529 $builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
15530 } elseif ('string' === $options['input']) {
15531 $builder->addModelTransformer(new ReversedTransformer(
15532 new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format'])
15533 ));
15534 } elseif ('timestamp' === $options['input']) {
15535 $builder->addModelTransformer(new ReversedTransformer(
15536 new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
15537 ));
15538 } elseif ('array' === $options['input']) {
15539 $builder->addModelTransformer(new ReversedTransformer(
15540 new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts)
15541 ));
15542 }
15543
15544 if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) {
15545 $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
15546 $date = $event->getData();
15547
15548 if (!$date instanceof \DateTimeInterface) {
15549 return;
15550 }
15551
15552 if ($date->getTimezone()->getName() !== $options['model_timezone']) {
15553 throw new LogicException(\sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', get_debug_type($date), $date->getTimezone()->getName(), $options['model_timezone']));
15554 }
15555 });
15556 }
15557 }
15558
15559 public function buildView(FormView $view, FormInterface $form, array $options): void
15560 {
15561 $view->vars['widget'] = $options['widget'];
15562
15563 // Change the input to an HTML5 datetime input if
15564 // * the widget is set to "single_text"
15565 // * the format matches the one expected by HTML5
15566 // * the html5 is set to true
15567 if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
15568 $view->vars['type'] = 'datetime-local';
15569
15570 // we need to force the browser to display the seconds by
15571 // adding the HTML attribute step if not already defined.
15572 // Otherwise the browser will not display and so not send the seconds
15573 // therefore the value will always be considered as invalid.
15574 if (!isset($view->vars['attr']['step'])) {
15575 if ($options['with_seconds']) {
15576 $view->vars['attr']['step'] = 1;
15577 } elseif (!$options['with_minutes']) {
15578 $view->vars['attr']['step'] = 3600;
15579 }
15580 }
15581 }
15582 }
15583
15584 public function configureOptions(OptionsResolver $resolver): void
15585 {
15586 $compound = static fn (Options $options) => 'single_text' !== $options['widget'];
15587
15588 $resolver->setDefaults([
15589 'input' => 'datetime',
15590 'model_timezone' => null,
15591 'view_timezone' => null,
15592 'format' => self::HTML5_FORMAT,
15593 'date_format' => null,
15594 'widget' => null,
15595 'date_widget' => null,
15596 'time_widget' => null,
15597 'with_minutes' => true,
15598 'with_seconds' => false,
15599 'html5' => true,
15600 // Don't modify \DateTime classes by reference, we treat
15601 // them like immutable value objects
15602 'by_reference' => false,
15603 'error_bubbling' => false,
15604 // If initialized with a \DateTime object, FormType initializes
15605 // this option to "\DateTime". Since the internal, normalized
15606 // representation is not \DateTime, but an array, we need to unset
15607 // this option.
15608 'data_class' => null,
15609 'compound' => $compound,
15610 'date_label' => null,
15611 'time_label' => null,
15612 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '',
15613 'input_format' => 'Y-m-d H:i:s',
15614 'invalid_message' => 'Please enter a valid date and time.',
15615 ]);
15616
15617 // Don't add some defaults in order to preserve the defaults
15618 // set in DateType and TimeType
15619 $resolver->setDefined([
15620 'placeholder',
15621 'choice_translation_domain',
15622 'years',
15623 'months',
15624 'days',
15625 'hours',
15626 'minutes',
15627 'seconds',
15628 ]);
15629
15630 $resolver->setAllowedValues('input', [
15631 'datetime',
15632 'datetime_immutable',
15633 'string',
15634 'timestamp',
15635 'array',
15636 ]);
15637 $resolver->setAllowedValues('date_widget', [
15638 null, // inherit default from DateType
15639 'single_text',
15640 'text',
15641 'choice',
15642 ]);
15643 $resolver->setAllowedValues('time_widget', [
15644 null, // inherit default from TimeType
15645 'single_text',
15646 'text',
15647 'choice',
15648 ]);
15649 // This option will overwrite "date_widget" and "time_widget" options
15650 $resolver->setAllowedValues('widget', [
15651 null, // default, don't overwrite options
15652 'single_text',
15653 'text',
15654 'choice',
15655 ]);
15656
15657 $resolver->setAllowedTypes('input_format', 'string');
15658
15659 $resolver->setNormalizer('date_format', static function (Options $options, $dateFormat) {
15660 if (null !== $dateFormat && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
15661 throw new LogicException(\sprintf('Cannot use the "date_format" option of the "%s" with an HTML5 date.', self::class));
15662 }
15663
15664 return $dateFormat;
15665 });
15666 $resolver->setNormalizer('widget', static function (Options $options, $widget) {
15667 if ('single_text' === $widget) {
15668 if (null !== $options['date_widget']) {
15669 throw new LogicException(\sprintf('Cannot use the "date_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class));
15670 }
15671 if (null !== $options['time_widget']) {
15672 throw new LogicException(\sprintf('Cannot use the "time_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class));
15673 }
15674 } elseif (null === $widget && null === $options['date_widget'] && null === $options['time_widget']) {
15675 return 'single_text';
15676 }
15677
15678 return $widget;
15679 });
15680 $resolver->setNormalizer('html5', static function (Options $options, $html5) {
15681 if ($html5 && self::HTML5_FORMAT !== $options['format']) {
15682 throw new LogicException(\sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class));
15683 }
15684
15685 return $html5;
15686 });
15687 }
15688
15689 public function getBlockPrefix(): string
15690 {
15691 return 'datetime';
15692 }
15693}
15694
15695------------------------------------------------------------------------------------------------------------------------
15696 ./Extension/Core/Type/RadioType.php
15697------------------------------------------------------------------------------------------------------------------------
15698<?php
15699
15700/*
15701 * This file is part of the Symfony package.
15702 *
15703 * (c) Fabien Potencier <fabien@symfony.com>
15704 *
15705 * For the full copyright and license information, please view the LICENSE
15706 * file that was distributed with this source code.
15707 */
15708
15709namespace Symfony\Component\Form\Extension\Core\Type;
15710
15711use Symfony\Component\Form\AbstractType;
15712use Symfony\Component\OptionsResolver\OptionsResolver;
15713
15714class RadioType extends AbstractType
15715{
15716 public function configureOptions(OptionsResolver $resolver): void
15717 {
15718 $resolver->setDefaults([
15719 'invalid_message' => 'Please select a valid option.',
15720 ]);
15721 }
15722
15723 public function getParent(): ?string
15724 {
15725 return CheckboxType::class;
15726 }
15727
15728 public function getBlockPrefix(): string
15729 {
15730 return 'radio';
15731 }
15732}
15733
15734------------------------------------------------------------------------------------------------------------------------
15735 ./Extension/Core/Type/TelType.php
15736------------------------------------------------------------------------------------------------------------------------
15737<?php
15738
15739/*
15740 * This file is part of the Symfony package.
15741 *
15742 * (c) Fabien Potencier <fabien@symfony.com>
15743 *
15744 * For the full copyright and license information, please view the LICENSE
15745 * file that was distributed with this source code.
15746 */
15747
15748namespace Symfony\Component\Form\Extension\Core\Type;
15749
15750use Symfony\Component\Form\AbstractType;
15751use Symfony\Component\OptionsResolver\OptionsResolver;
15752
15753class TelType extends AbstractType
15754{
15755 public function configureOptions(OptionsResolver $resolver): void
15756 {
15757 $resolver->setDefaults([
15758 'invalid_message' => 'Please provide a valid phone number.',
15759 ]);
15760 }
15761
15762 public function getParent(): ?string
15763 {
15764 return TextType::class;
15765 }
15766
15767 public function getBlockPrefix(): string
15768 {
15769 return 'tel';
15770 }
15771}
15772
15773------------------------------------------------------------------------------------------------------------------------
15774 ./Extension/Core/Type/HiddenType.php
15775------------------------------------------------------------------------------------------------------------------------
15776<?php
15777
15778/*
15779 * This file is part of the Symfony package.
15780 *
15781 * (c) Fabien Potencier <fabien@symfony.com>
15782 *
15783 * For the full copyright and license information, please view the LICENSE
15784 * file that was distributed with this source code.
15785 */
15786
15787namespace Symfony\Component\Form\Extension\Core\Type;
15788
15789use Symfony\Component\Form\AbstractType;
15790use Symfony\Component\OptionsResolver\OptionsResolver;
15791
15792class HiddenType extends AbstractType
15793{
15794 public function configureOptions(OptionsResolver $resolver): void
15795 {
15796 $resolver->setDefaults([
15797 // hidden fields cannot have a required attribute
15798 'required' => false,
15799 // Pass errors to the parent
15800 'error_bubbling' => true,
15801 'compound' => false,
15802 'invalid_message' => 'The hidden field is invalid.',
15803 ]);
15804 }
15805
15806 public function getBlockPrefix(): string
15807 {
15808 return 'hidden';
15809 }
15810}
15811
15812------------------------------------------------------------------------------------------------------------------------
15813 ./Extension/Core/Type/ColorType.php
15814------------------------------------------------------------------------------------------------------------------------
15815<?php
15816
15817/*
15818 * This file is part of the Symfony package.
15819 *
15820 * (c) Fabien Potencier <fabien@symfony.com>
15821 *
15822 * For the full copyright and license information, please view the LICENSE
15823 * file that was distributed with this source code.
15824 */
15825
15826namespace Symfony\Component\Form\Extension\Core\Type;
15827
15828use Symfony\Component\Form\AbstractType;
15829use Symfony\Component\Form\FormBuilderInterface;
15830use Symfony\Component\Form\FormError;
15831use Symfony\Component\Form\FormEvent;
15832use Symfony\Component\Form\FormEvents;
15833use Symfony\Component\OptionsResolver\OptionsResolver;
15834use Symfony\Contracts\Translation\TranslatorInterface;
15835
15836class ColorType extends AbstractType
15837{
15838 /**
15839 * @see https://www.w3.org/TR/html52/sec-forms.html#color-state-typecolor
15840 */
15841 private const HTML5_PATTERN = '/^#[0-9a-f]{6}$/i';
15842
15843 public function __construct(
15844 private ?TranslatorInterface $translator = null,
15845 ) {
15846 }
15847
15848 public function buildForm(FormBuilderInterface $builder, array $options): void
15849 {
15850 if (!$options['html5']) {
15851 return;
15852 }
15853
15854 $translator = $this->translator;
15855 $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($translator): void {
15856 $value = $event->getData();
15857 if (null === $value || '' === $value) {
15858 return;
15859 }
15860
15861 if (\is_string($value) && preg_match(self::HTML5_PATTERN, $value)) {
15862 return;
15863 }
15864
15865 $messageTemplate = 'This value is not a valid HTML5 color.';
15866 $messageParameters = [
15867 '{{ value }}' => \is_scalar($value) ? (string) $value : \gettype($value),
15868 ];
15869 $message = $translator?->trans($messageTemplate, $messageParameters, 'validators') ?? $messageTemplate;
15870
15871 $event->getForm()->addError(new FormError($message, $messageTemplate, $messageParameters));
15872 });
15873 }
15874
15875 public function configureOptions(OptionsResolver $resolver): void
15876 {
15877 $resolver->setDefaults([
15878 'html5' => false,
15879 'invalid_message' => 'Please select a valid color.',
15880 ]);
15881
15882 $resolver->setAllowedTypes('html5', 'bool');
15883 }
15884
15885 public function getParent(): ?string
15886 {
15887 return TextType::class;
15888 }
15889
15890 public function getBlockPrefix(): string
15891 {
15892 return 'color';
15893 }
15894}
15895
15896------------------------------------------------------------------------------------------------------------------------
15897 ./Extension/Core/Type/TimezoneType.php
15898------------------------------------------------------------------------------------------------------------------------
15899<?php
15900
15901/*
15902 * This file is part of the Symfony package.
15903 *
15904 * (c) Fabien Potencier <fabien@symfony.com>
15905 *
15906 * For the full copyright and license information, please view the LICENSE
15907 * file that was distributed with this source code.
15908 */
15909
15910namespace Symfony\Component\Form\Extension\Core\Type;
15911
15912use Symfony\Component\Form\AbstractType;
15913use Symfony\Component\Form\ChoiceList\ChoiceList;
15914use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
15915use Symfony\Component\Form\Exception\LogicException;
15916use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeZoneToStringTransformer;
15917use Symfony\Component\Form\Extension\Core\DataTransformer\IntlTimeZoneToStringTransformer;
15918use Symfony\Component\Form\FormBuilderInterface;
15919use Symfony\Component\Intl\Intl;
15920use Symfony\Component\Intl\Timezones;
15921use Symfony\Component\OptionsResolver\Options;
15922use Symfony\Component\OptionsResolver\OptionsResolver;
15923
15924class TimezoneType extends AbstractType
15925{
15926 public function buildForm(FormBuilderInterface $builder, array $options): void
15927 {
15928 if ('datetimezone' === $options['input']) {
15929 $builder->addModelTransformer(new DateTimeZoneToStringTransformer($options['multiple']));
15930 } elseif ('intltimezone' === $options['input']) {
15931 $builder->addModelTransformer(new IntlTimeZoneToStringTransformer($options['multiple']));
15932 }
15933 }
15934
15935 public function configureOptions(OptionsResolver $resolver): void
15936 {
15937 $resolver->setDefaults([
15938 'intl' => false,
15939 'choice_loader' => function (Options $options) {
15940 $input = $options['input'];
15941
15942 if ($options['intl']) {
15943 if (!class_exists(Intl::class)) {
15944 throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s" with option "intl=true". Try running "composer require symfony/intl".', static::class));
15945 }
15946
15947 $choiceTranslationLocale = $options['choice_translation_locale'];
15948
15949 return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => self::getIntlTimezones($input, $choiceTranslationLocale)), [$input, $choiceTranslationLocale]);
15950 }
15951
15952 return ChoiceList::lazy($this, static fn () => self::getPhpTimezones($input), $input);
15953 },
15954 'choice_translation_domain' => false,
15955 'choice_translation_locale' => null,
15956 'input' => 'string',
15957 'invalid_message' => 'Please select a valid timezone.',
15958 'regions' => \DateTimeZone::ALL,
15959 ]);
15960
15961 $resolver->setAllowedTypes('intl', ['bool']);
15962
15963 $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
15964 $resolver->setNormalizer('choice_translation_locale', static function (Options $options, $value) {
15965 if (null !== $value && !$options['intl']) {
15966 throw new LogicException('The "choice_translation_locale" option can only be used if the "intl" option is set to true.');
15967 }
15968
15969 return $value;
15970 });
15971
15972 $resolver->setAllowedValues('input', ['string', 'datetimezone', 'intltimezone']);
15973 $resolver->setNormalizer('input', static function (Options $options, $value) {
15974 if ('intltimezone' === $value && !class_exists(\IntlTimeZone::class)) {
15975 throw new LogicException('Cannot use "intltimezone" input because the PHP intl extension is not available.');
15976 }
15977
15978 return $value;
15979 });
15980 }
15981
15982 public function getParent(): ?string
15983 {
15984 return ChoiceType::class;
15985 }
15986
15987 public function getBlockPrefix(): string
15988 {
15989 return 'timezone';
15990 }
15991
15992 private static function getPhpTimezones(string $input): array
15993 {
15994 $timezones = [];
15995
15996 foreach (\DateTimeZone::listIdentifiers(\DateTimeZone::ALL) as $timezone) {
15997 if ('intltimezone' === $input && 'Etc/Unknown' === \IntlTimeZone::createTimeZone($timezone)->getID()) {
15998 continue;
15999 }
16000
16001 $timezones[str_replace(['/', '_'], [' / ', ' '], $timezone)] = $timezone;
16002 }
16003
16004 return $timezones;
16005 }
16006
16007 private static function getIntlTimezones(string $input, ?string $locale = null): array
16008 {
16009 $timezones = array_flip(Timezones::getNames($locale));
16010
16011 if ('intltimezone' === $input) {
16012 foreach ($timezones as $name => $timezone) {
16013 if ('Etc/Unknown' === \IntlTimeZone::createTimeZone($timezone)->getID()) {
16014 unset($timezones[$name]);
16015 }
16016 }
16017 }
16018
16019 return $timezones;
16020 }
16021}
16022
16023------------------------------------------------------------------------------------------------------------------------
16024 ./Extension/Core/Type/ChoiceType.php
16025------------------------------------------------------------------------------------------------------------------------
16026<?php
16027
16028/*
16029 * This file is part of the Symfony package.
16030 *
16031 * (c) Fabien Potencier <fabien@symfony.com>
16032 *
16033 * For the full copyright and license information, please view the LICENSE
16034 * file that was distributed with this source code.
16035 */
16036
16037namespace Symfony\Component\Form\Extension\Core\Type;
16038
16039use Symfony\Component\Form\AbstractType;
16040use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
16041use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
16042use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
16043use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFilter;
16044use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
16045use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
16046use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceTranslationParameters;
16047use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
16048use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy;
16049use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice;
16050use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
16051use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
16052use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
16053use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
16054use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
16055use Symfony\Component\Form\ChoiceList\Loader\LazyChoiceLoader;
16056use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
16057use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
16058use Symfony\Component\Form\ChoiceList\View\ChoiceView;
16059use Symfony\Component\Form\Event\PreSubmitEvent;
16060use Symfony\Component\Form\Exception\LogicException;
16061use Symfony\Component\Form\Exception\TransformationFailedException;
16062use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper;
16063use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper;
16064use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
16065use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
16066use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
16067use Symfony\Component\Form\FormBuilderInterface;
16068use Symfony\Component\Form\FormError;
16069use Symfony\Component\Form\FormEvent;
16070use Symfony\Component\Form\FormEvents;
16071use Symfony\Component\Form\FormInterface;
16072use Symfony\Component\Form\FormView;
16073use Symfony\Component\OptionsResolver\Options;
16074use Symfony\Component\OptionsResolver\OptionsResolver;
16075use Symfony\Component\PropertyAccess\PropertyPath;
16076use Symfony\Contracts\Translation\TranslatorInterface;
16077
16078class ChoiceType extends AbstractType
16079{
16080 private ChoiceListFactoryInterface $choiceListFactory;
16081
16082 public function __construct(
16083 ?ChoiceListFactoryInterface $choiceListFactory = null,
16084 private ?TranslatorInterface $translator = null,
16085 ) {
16086 $this->choiceListFactory = $choiceListFactory ?? new CachingFactoryDecorator(
16087 new PropertyAccessDecorator(
16088 new DefaultChoiceListFactory()
16089 )
16090 );
16091 }
16092
16093 public function buildForm(FormBuilderInterface $builder, array $options): void
16094 {
16095 $unknownValues = [];
16096 $choiceList = $this->createChoiceList($options);
16097 $builder->setAttribute('choice_list', $choiceList);
16098
16099 if ($options['expanded']) {
16100 $builder->setDataMapper($options['multiple'] ? new CheckboxListMapper() : new RadioListMapper());
16101
16102 // Initialize all choices before doing the index check below.
16103 // This helps in cases where index checks are optimized for non
16104 // initialized choice lists. For example, when using an SQL driver,
16105 // the index check would read in one SQL query and the initialization
16106 // requires another SQL query. When the initialization is done first,
16107 // one SQL query is sufficient.
16108
16109 $choiceListView = $this->createChoiceListView($choiceList, $options);
16110 $builder->setAttribute('choice_list_view', $choiceListView);
16111
16112 // Check if the choices already contain the empty value
16113 // Only add the placeholder option if this is not the case
16114 if (null !== $options['placeholder'] && 0 === \count($choiceList->getChoicesForValues(['']))) {
16115 $placeholderView = new ChoiceView(null, '', $options['placeholder'], $options['placeholder_attr']);
16116
16117 // "placeholder" is a reserved name
16118 $this->addSubForm($builder, 'placeholder', $placeholderView, $options);
16119 }
16120
16121 $this->addSubForms($builder, $choiceListView->preferredChoices, $options);
16122 $this->addSubForms($builder, $choiceListView->choices, $options);
16123 }
16124
16125 if ($options['expanded'] || $options['multiple']) {
16126 // Make sure that scalar, submitted values are converted to arrays
16127 // which can be submitted to the checkboxes/radio buttons
16128 $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($choiceList, $options, &$unknownValues) {
16129 /** @var PreSubmitEvent $event */
16130 $form = $event->getForm();
16131 $data = $event->getData();
16132
16133 // Since the type always use mapper an empty array will not be
16134 // considered as empty in Form::submit(), we need to evaluate
16135 // empty data here so its value is submitted to sub forms
16136 if (null === $data) {
16137 $emptyData = $form->getConfig()->getEmptyData();
16138 $data = $emptyData instanceof \Closure ? $emptyData($form, $data) : $emptyData;
16139 }
16140
16141 // Convert the submitted data to a string, if scalar, before
16142 // casting it to an array
16143 if (!\is_array($data)) {
16144 if ($options['multiple']) {
16145 throw new TransformationFailedException('Expected an array.');
16146 }
16147
16148 $data = (array) (string) $data;
16149 }
16150
16151 // A map from submitted values to integers
16152 $valueMap = array_flip($data);
16153
16154 // Make a copy of the value map to determine whether any unknown
16155 // values were submitted
16156 $unknownValues = $valueMap;
16157
16158 // Reconstruct the data as mapping from child names to values
16159 $knownValues = [];
16160
16161 if ($options['expanded']) {
16162 /** @var FormInterface $child */
16163 foreach ($form as $child) {
16164 $value = $child->getConfig()->getOption('value');
16165
16166 // Add the value to $data with the child's name as key
16167 if (isset($valueMap[$value])) {
16168 $knownValues[$child->getName()] = $value;
16169 unset($unknownValues[$value]);
16170 continue;
16171 }
16172
16173 $knownValues[$child->getName()] = null;
16174 }
16175 } else {
16176 foreach ($choiceList->getChoicesForValues($data) as $key => $choice) {
16177 $knownValues[] = $data[$key];
16178 unset($unknownValues[$data[$key]]);
16179 }
16180 }
16181
16182 // The empty value is always known, independent of whether a
16183 // field exists for it or not
16184 unset($unknownValues['']);
16185
16186 // Throw exception if unknown values were submitted (multiple choices will be handled in a different event listener below)
16187 if (\count($unknownValues) > 0 && !$options['multiple']) {
16188 throw new TransformationFailedException(\sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues))));
16189 }
16190
16191 $event->setData($knownValues);
16192 });
16193 }
16194
16195 if ($options['multiple']) {
16196 $messageTemplate = $options['invalid_message'] ?? 'The value {{ value }} is not valid.';
16197 $translator = $this->translator;
16198
16199 $builder->addEventListener(FormEvents::POST_SUBMIT, static function (FormEvent $event) use (&$unknownValues, $messageTemplate, $translator) {
16200 // Throw exception if unknown values were submitted
16201 if (\count($unknownValues) > 0) {
16202 $form = $event->getForm();
16203
16204 $clientDataAsString = \is_scalar($form->getViewData()) ? (string) $form->getViewData() : (\is_array($form->getViewData()) ? implode('", "', array_keys($unknownValues)) : \gettype($form->getViewData()));
16205
16206 if ($translator) {
16207 $message = $translator->trans($messageTemplate, ['{{ value }}' => $clientDataAsString], 'validators');
16208 } else {
16209 $message = strtr($messageTemplate, ['{{ value }}' => $clientDataAsString]);
16210 }
16211
16212 $form->addError(new FormError($message, $messageTemplate, ['{{ value }}' => $clientDataAsString], null, new TransformationFailedException(\sprintf('The choices "%s" do not exist in the choice list.', $clientDataAsString))));
16213 }
16214 });
16215
16216 // <select> tag with "multiple" option or list of checkbox inputs
16217 $builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));
16218 } else {
16219 // <select> tag without "multiple" option or list of radio inputs
16220 $builder->addViewTransformer(new ChoiceToValueTransformer($choiceList));
16221 }
16222
16223 if ($options['multiple'] && $options['by_reference']) {
16224 // Make sure the collection created during the client->norm
16225 // transformation is merged back into the original collection
16226 $builder->addEventSubscriber(new MergeCollectionListener(true, true));
16227 }
16228
16229 // To avoid issues when the submitted choices are arrays (i.e. array to string conversions),
16230 // we have to ensure that all elements of the submitted choice data are NULL, strings or ints.
16231 $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) {
16232 $data = $event->getData();
16233
16234 if (!\is_array($data)) {
16235 return;
16236 }
16237
16238 foreach ($data as $v) {
16239 if (null !== $v && !\is_string($v) && !\is_int($v)) {
16240 throw new TransformationFailedException('All choices submitted must be NULL, strings or ints.');
16241 }
16242 }
16243 }, 256);
16244 }
16245
16246 public function buildView(FormView $view, FormInterface $form, array $options): void
16247 {
16248 $choiceTranslationDomain = $options['choice_translation_domain'];
16249 if ($view->parent && null === $choiceTranslationDomain) {
16250 $choiceTranslationDomain = $view->vars['translation_domain'];
16251 }
16252
16253 /** @var ChoiceListInterface $choiceList */
16254 $choiceList = $form->getConfig()->getAttribute('choice_list');
16255
16256 /** @var ChoiceListView $choiceListView */
16257 $choiceListView = $form->getConfig()->hasAttribute('choice_list_view')
16258 ? $form->getConfig()->getAttribute('choice_list_view')
16259 : $this->createChoiceListView($choiceList, $options);
16260
16261 $view->vars = array_replace($view->vars, [
16262 'multiple' => $options['multiple'],
16263 'expanded' => $options['expanded'],
16264 'preferred_choices' => $choiceListView->preferredChoices,
16265 'choices' => $choiceListView->choices,
16266 'separator' => $options['separator'],
16267 'separator_html' => $options['separator_html'],
16268 'placeholder' => null,
16269 'placeholder_attr' => [],
16270 'choice_translation_domain' => $choiceTranslationDomain,
16271 'choice_translation_parameters' => $options['choice_translation_parameters'],
16272 ]);
16273
16274 // The decision, whether a choice is selected, is potentially done
16275 // thousand of times during the rendering of a template. Provide a
16276 // closure here that is optimized for the value of the form, to
16277 // avoid making the type check inside the closure.
16278 if ($options['multiple']) {
16279 $view->vars['is_selected'] = static fn ($choice, array $values) => \in_array($choice, $values, true);
16280 } else {
16281 $view->vars['is_selected'] = static fn ($choice, $value) => $choice === $value;
16282 }
16283
16284 // Check if the choices already contain the empty value
16285 $view->vars['placeholder_in_choices'] = $choiceListView->hasPlaceholder();
16286
16287 // Only add the empty value option if this is not the case
16288 if (null !== $options['placeholder'] && !$view->vars['placeholder_in_choices']) {
16289 $view->vars['placeholder'] = $options['placeholder'];
16290 $view->vars['placeholder_attr'] = $options['placeholder_attr'];
16291 }
16292
16293 if ($options['multiple'] && !$options['expanded']) {
16294 // Add "[]" to the name in case a select tag with multiple options is
16295 // displayed. Otherwise only one of the selected options is sent in the
16296 // POST request.
16297 $view->vars['full_name'] .= '[]';
16298 }
16299 }
16300
16301 public function finishView(FormView $view, FormInterface $form, array $options): void
16302 {
16303 if ($options['expanded']) {
16304 // Radio buttons should have the same name as the parent
16305 $childName = $view->vars['full_name'];
16306
16307 // Checkboxes should append "[]" to allow multiple selection
16308 if ($options['multiple']) {
16309 $childName .= '[]';
16310 }
16311
16312 foreach ($view as $childView) {
16313 $childView->vars['full_name'] = $childName;
16314 }
16315 }
16316 }
16317
16318 public function configureOptions(OptionsResolver $resolver): void
16319 {
16320 $emptyData = static function (Options $options) {
16321 if ($options['expanded'] && !$options['multiple']) {
16322 return null;
16323 }
16324
16325 if ($options['multiple']) {
16326 return [];
16327 }
16328
16329 return '';
16330 };
16331
16332 $placeholderDefault = static fn (Options $options) => $options['required'] ? null : '';
16333
16334 $placeholderNormalizer = static function (Options $options, $placeholder) {
16335 if ($options['multiple']) {
16336 // never use an empty value for this case
16337 return null;
16338 } elseif ($options['required'] && ($options['expanded'] || isset($options['attr']['size']) && $options['attr']['size'] > 1)) {
16339 // placeholder for required radio buttons or a select with size > 1 does not make sense
16340 return null;
16341 } elseif (false === $placeholder) {
16342 // an empty value should be added but the user decided otherwise
16343 return null;
16344 } elseif ($options['expanded'] && '' === $placeholder) {
16345 // never use an empty label for radio buttons
16346 return 'None';
16347 }
16348
16349 // empty value has been set explicitly
16350 return $placeholder;
16351 };
16352
16353 $compound = static fn (Options $options) => $options['expanded'];
16354
16355 $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) {
16356 if (true === $choiceTranslationDomain) {
16357 return $options['translation_domain'];
16358 }
16359
16360 return $choiceTranslationDomain;
16361 };
16362
16363 $choiceLoaderNormalizer = static function (Options $options, ?ChoiceLoaderInterface $choiceLoader) {
16364 if (!$options['choice_lazy']) {
16365 return $choiceLoader;
16366 }
16367
16368 if (null === $choiceLoader) {
16369 throw new LogicException('The "choice_lazy" option can only be used if the "choice_loader" option is set.');
16370 }
16371
16372 return new LazyChoiceLoader($choiceLoader);
16373 };
16374
16375 $resolver->setDefaults([
16376 'multiple' => false,
16377 'expanded' => false,
16378 'choices' => [],
16379 'choice_filter' => null,
16380 'choice_lazy' => false,
16381 'choice_loader' => null,
16382 'choice_label' => null,
16383 'choice_name' => null,
16384 'choice_value' => null,
16385 'choice_attr' => null,
16386 'choice_translation_parameters' => [],
16387 'preferred_choices' => [],
16388 'separator' => '-------------------',
16389 'separator_html' => false,
16390 'duplicate_preferred_choices' => true,
16391 'group_by' => null,
16392 'empty_data' => $emptyData,
16393 'placeholder' => $placeholderDefault,
16394 'placeholder_attr' => [],
16395 'error_bubbling' => false,
16396 'compound' => $compound,
16397 // The view data is always a string or an array of strings,
16398 // even if the "data" option is manually set to an object.
16399 // See https://github.com/symfony/symfony/pull/5582
16400 'data_class' => null,
16401 'choice_translation_domain' => true,
16402 'trim' => false,
16403 'invalid_message' => 'The selected choice is invalid.',
16404 ]);
16405
16406 $resolver->setNormalizer('placeholder', $placeholderNormalizer);
16407 $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
16408 $resolver->setNormalizer('choice_loader', $choiceLoaderNormalizer);
16409
16410 $resolver->setAllowedTypes('choices', ['null', 'array', \Traversable::class]);
16411 $resolver->setAllowedTypes('choice_translation_domain', ['null', 'bool', 'string']);
16412 $resolver->setAllowedTypes('choice_lazy', 'bool');
16413 $resolver->setAllowedTypes('choice_loader', ['null', ChoiceLoaderInterface::class, ChoiceLoader::class]);
16414 $resolver->setAllowedTypes('choice_filter', ['null', 'callable', 'string', PropertyPath::class, ChoiceFilter::class]);
16415 $resolver->setAllowedTypes('choice_label', ['null', 'bool', 'callable', 'string', PropertyPath::class, ChoiceLabel::class]);
16416 $resolver->setAllowedTypes('choice_name', ['null', 'callable', 'string', PropertyPath::class, ChoiceFieldName::class]);
16417 $resolver->setAllowedTypes('choice_value', ['null', 'callable', 'string', PropertyPath::class, ChoiceValue::class]);
16418 $resolver->setAllowedTypes('choice_attr', ['null', 'array', 'callable', 'string', PropertyPath::class, ChoiceAttr::class]);
16419 $resolver->setAllowedTypes('choice_translation_parameters', ['null', 'array', 'callable', ChoiceTranslationParameters::class]);
16420 $resolver->setAllowedTypes('placeholder_attr', ['array']);
16421 $resolver->setAllowedTypes('preferred_choices', ['array', \Traversable::class, 'callable', 'string', PropertyPath::class, PreferredChoice::class]);
16422 $resolver->setAllowedTypes('separator', ['string']);
16423 $resolver->setAllowedTypes('separator_html', ['bool']);
16424 $resolver->setAllowedTypes('duplicate_preferred_choices', 'bool');
16425 $resolver->setAllowedTypes('group_by', ['null', 'callable', 'string', PropertyPath::class, GroupBy::class]);
16426
16427 $resolver->setInfo('choice_lazy', 'Load choices on demand. When set to true, only the selected choices are loaded and rendered.');
16428 }
16429
16430 public function getBlockPrefix(): string
16431 {
16432 return 'choice';
16433 }
16434
16435 /**
16436 * Adds the sub fields for an expanded choice field.
16437 */
16438 private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options): void
16439 {
16440 foreach ($choiceViews as $name => $choiceView) {
16441 // Flatten groups
16442 if (\is_array($choiceView)) {
16443 $this->addSubForms($builder, $choiceView, $options);
16444 continue;
16445 }
16446
16447 if ($choiceView instanceof ChoiceGroupView) {
16448 $this->addSubForms($builder, $choiceView->choices, $options);
16449 continue;
16450 }
16451
16452 $this->addSubForm($builder, $name, $choiceView, $options);
16453 }
16454 }
16455
16456 private function addSubForm(FormBuilderInterface $builder, string $name, ChoiceView $choiceView, array $options): void
16457 {
16458 $choiceOpts = [
16459 'value' => $choiceView->value,
16460 'label' => $choiceView->label,
16461 'label_html' => $options['label_html'],
16462 'attr' => $choiceView->attr,
16463 'label_translation_parameters' => $choiceView->labelTranslationParameters,
16464 'translation_domain' => $options['choice_translation_domain'],
16465 'block_name' => 'entry',
16466 ];
16467
16468 if ($options['multiple']) {
16469 $choiceType = CheckboxType::class;
16470 // The user can check 0 or more checkboxes. If required
16471 // is true, they are required to check all of them.
16472 $choiceOpts['required'] = false;
16473 } else {
16474 $choiceType = RadioType::class;
16475 }
16476
16477 $builder->add($name, $choiceType, $choiceOpts);
16478 }
16479
16480 private function createChoiceList(array $options): ChoiceListInterface
16481 {
16482 if (null !== $options['choice_loader']) {
16483 return $this->choiceListFactory->createListFromLoader(
16484 $options['choice_loader'],
16485 $options['choice_value'],
16486 $options['choice_filter']
16487 );
16488 }
16489
16490 // Harden against NULL values (like in EntityType and ModelType)
16491 $choices = $options['choices'] ?? [];
16492
16493 return $this->choiceListFactory->createListFromChoices(
16494 $choices,
16495 $options['choice_value'],
16496 $options['choice_filter']
16497 );
16498 }
16499
16500 private function createChoiceListView(ChoiceListInterface $choiceList, array $options): ChoiceListView
16501 {
16502 return $this->choiceListFactory->createView(
16503 $choiceList,
16504 $options['preferred_choices'],
16505 $options['choice_label'],
16506 $options['choice_name'],
16507 $options['group_by'],
16508 $options['choice_attr'],
16509 $options['choice_translation_parameters'],
16510 $options['duplicate_preferred_choices'],
16511 );
16512 }
16513}
16514
16515------------------------------------------------------------------------------------------------------------------------
16516 ./Extension/Core/Type/LanguageType.php
16517------------------------------------------------------------------------------------------------------------------------
16518<?php
16519
16520/*
16521 * This file is part of the Symfony package.
16522 *
16523 * (c) Fabien Potencier <fabien@symfony.com>
16524 *
16525 * For the full copyright and license information, please view the LICENSE
16526 * file that was distributed with this source code.
16527 */
16528
16529namespace Symfony\Component\Form\Extension\Core\Type;
16530
16531use Symfony\Component\Form\AbstractType;
16532use Symfony\Component\Form\ChoiceList\ChoiceList;
16533use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
16534use Symfony\Component\Form\Exception\LogicException;
16535use Symfony\Component\Intl\Exception\MissingResourceException;
16536use Symfony\Component\Intl\Intl;
16537use Symfony\Component\Intl\Languages;
16538use Symfony\Component\OptionsResolver\Options;
16539use Symfony\Component\OptionsResolver\OptionsResolver;
16540
16541class LanguageType extends AbstractType
16542{
16543 public function configureOptions(OptionsResolver $resolver): void
16544 {
16545 $resolver->setDefaults([
16546 'choice_loader' => function (Options $options) {
16547 if (!class_exists(Intl::class)) {
16548 throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
16549 }
16550 $choiceTranslationLocale = $options['choice_translation_locale'];
16551 $useAlpha3Codes = $options['alpha3'];
16552 $choiceSelfTranslation = $options['choice_self_translation'];
16553
16554 return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static function () use ($choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation) {
16555 if (true === $choiceSelfTranslation) {
16556 foreach (Languages::getLanguageCodes() as $alpha2Code) {
16557 try {
16558 $languageCode = $useAlpha3Codes ? Languages::getAlpha3Code($alpha2Code) : $alpha2Code;
16559 $languagesList[$languageCode] = Languages::getName($alpha2Code, $alpha2Code);
16560 } catch (MissingResourceException) {
16561 // ignore errors like "Couldn't read the indices for the locale 'meta'"
16562 }
16563 }
16564 } else {
16565 $languagesList = $useAlpha3Codes ? Languages::getAlpha3Names($choiceTranslationLocale) : Languages::getNames($choiceTranslationLocale);
16566 }
16567
16568 return array_flip($languagesList);
16569 }), [$choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation]);
16570 },
16571 'choice_translation_domain' => false,
16572 'choice_translation_locale' => null,
16573 'alpha3' => false,
16574 'choice_self_translation' => false,
16575 'invalid_message' => 'Please select a valid language.',
16576 ]);
16577
16578 $resolver->setAllowedTypes('choice_self_translation', ['bool']);
16579 $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
16580 $resolver->setAllowedTypes('alpha3', 'bool');
16581
16582 $resolver->setNormalizer('choice_self_translation', static function (Options $options, $value) {
16583 if (true === $value && $options['choice_translation_locale']) {
16584 throw new LogicException('Cannot use the "choice_self_translation" and "choice_translation_locale" options at the same time. Remove one of them.');
16585 }
16586
16587 return $value;
16588 });
16589 }
16590
16591 public function getParent(): ?string
16592 {
16593 return ChoiceType::class;
16594 }
16595
16596 public function getBlockPrefix(): string
16597 {
16598 return 'language';
16599 }
16600}
16601
16602------------------------------------------------------------------------------------------------------------------------
16603 ./Extension/Core/Type/CurrencyType.php
16604------------------------------------------------------------------------------------------------------------------------
16605<?php
16606
16607/*
16608 * This file is part of the Symfony package.
16609 *
16610 * (c) Fabien Potencier <fabien@symfony.com>
16611 *
16612 * For the full copyright and license information, please view the LICENSE
16613 * file that was distributed with this source code.
16614 */
16615
16616namespace Symfony\Component\Form\Extension\Core\Type;
16617
16618use Symfony\Component\Form\AbstractType;
16619use Symfony\Component\Form\ChoiceList\ChoiceList;
16620use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
16621use Symfony\Component\Form\Exception\LogicException;
16622use Symfony\Component\Intl\Currencies;
16623use Symfony\Component\Intl\Intl;
16624use Symfony\Component\OptionsResolver\Options;
16625use Symfony\Component\OptionsResolver\OptionsResolver;
16626
16627class CurrencyType extends AbstractType
16628{
16629 public function configureOptions(OptionsResolver $resolver): void
16630 {
16631 $resolver->setDefaults([
16632 'choice_loader' => function (Options $options) {
16633 if (!class_exists(Intl::class)) {
16634 throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
16635 }
16636
16637 $choiceTranslationLocale = $options['choice_translation_locale'];
16638
16639 return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip(Currencies::getNames($choiceTranslationLocale))), $choiceTranslationLocale);
16640 },
16641 'choice_translation_domain' => false,
16642 'choice_translation_locale' => null,
16643 'invalid_message' => 'Please select a valid currency.',
16644 ]);
16645
16646 $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
16647 }
16648
16649 public function getParent(): ?string
16650 {
16651 return ChoiceType::class;
16652 }
16653
16654 public function getBlockPrefix(): string
16655 {
16656 return 'currency';
16657 }
16658}
16659
16660------------------------------------------------------------------------------------------------------------------------
16661 ./Extension/Core/Type/WeekType.php
16662------------------------------------------------------------------------------------------------------------------------
16663<?php
16664
16665/*
16666 * This file is part of the Symfony package.
16667 *
16668 * (c) Fabien Potencier <fabien@symfony.com>
16669 *
16670 * For the full copyright and license information, please view the LICENSE
16671 * file that was distributed with this source code.
16672 */
16673
16674namespace Symfony\Component\Form\Extension\Core\Type;
16675
16676use Symfony\Component\Form\AbstractType;
16677use Symfony\Component\Form\Exception\LogicException;
16678use Symfony\Component\Form\Extension\Core\DataTransformer\WeekToArrayTransformer;
16679use Symfony\Component\Form\FormBuilderInterface;
16680use Symfony\Component\Form\FormInterface;
16681use Symfony\Component\Form\FormView;
16682use Symfony\Component\Form\ReversedTransformer;
16683use Symfony\Component\OptionsResolver\Options;
16684use Symfony\Component\OptionsResolver\OptionsResolver;
16685
16686class WeekType extends AbstractType
16687{
16688 private const WIDGETS = [
16689 'text' => IntegerType::class,
16690 'choice' => ChoiceType::class,
16691 ];
16692
16693 public function buildForm(FormBuilderInterface $builder, array $options): void
16694 {
16695 if ('string' === $options['input']) {
16696 $builder->addModelTransformer(new WeekToArrayTransformer());
16697 }
16698
16699 if ('single_text' === $options['widget']) {
16700 $builder->addViewTransformer(new ReversedTransformer(new WeekToArrayTransformer()));
16701 } else {
16702 $yearOptions = $weekOptions = [
16703 'error_bubbling' => true,
16704 'empty_data' => '',
16705 ];
16706 // when the form is compound the entries of the array are ignored in favor of children data
16707 // so we need to handle the cascade setting here
16708 $emptyData = $builder->getEmptyData() ?: [];
16709
16710 $yearOptions['empty_data'] = $emptyData['year'] ?? '';
16711 $weekOptions['empty_data'] = $emptyData['week'] ?? '';
16712
16713 if (isset($options['invalid_message'])) {
16714 $yearOptions['invalid_message'] = $options['invalid_message'];
16715 $weekOptions['invalid_message'] = $options['invalid_message'];
16716 }
16717
16718 if (isset($options['invalid_message_parameters'])) {
16719 $yearOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
16720 $weekOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
16721 }
16722
16723 if ('choice' === $options['widget']) {
16724 // Only pass a subset of the options to children
16725 $yearOptions['choices'] = array_combine($options['years'], $options['years']);
16726 $yearOptions['placeholder'] = $options['placeholder']['year'];
16727 $yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year'];
16728
16729 $weekOptions['choices'] = array_combine($options['weeks'], $options['weeks']);
16730 $weekOptions['placeholder'] = $options['placeholder']['week'];
16731 $weekOptions['choice_translation_domain'] = $options['choice_translation_domain']['week'];
16732
16733 // Append generic carry-along options
16734 foreach (['required', 'translation_domain'] as $passOpt) {
16735 $yearOptions[$passOpt] = $options[$passOpt];
16736 $weekOptions[$passOpt] = $options[$passOpt];
16737 }
16738 }
16739
16740 $builder->add('year', self::WIDGETS[$options['widget']], $yearOptions);
16741 $builder->add('week', self::WIDGETS[$options['widget']], $weekOptions);
16742 }
16743 }
16744
16745 public function buildView(FormView $view, FormInterface $form, array $options): void
16746 {
16747 $view->vars['widget'] = $options['widget'];
16748
16749 if ($options['html5']) {
16750 $view->vars['type'] = 'week';
16751 }
16752 }
16753
16754 public function configureOptions(OptionsResolver $resolver): void
16755 {
16756 $compound = static fn (Options $options) => 'single_text' !== $options['widget'];
16757
16758 $placeholderDefault = static fn (Options $options) => $options['required'] ? null : '';
16759
16760 $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) {
16761 if (\is_array($placeholder)) {
16762 $default = $placeholderDefault($options);
16763
16764 return array_merge(
16765 ['year' => $default, 'week' => $default],
16766 $placeholder
16767 );
16768 }
16769
16770 return [
16771 'year' => $placeholder,
16772 'week' => $placeholder,
16773 ];
16774 };
16775
16776 $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) {
16777 if (\is_array($choiceTranslationDomain)) {
16778 return array_replace(
16779 ['year' => false, 'week' => false],
16780 $choiceTranslationDomain
16781 );
16782 }
16783
16784 return [
16785 'year' => $choiceTranslationDomain,
16786 'week' => $choiceTranslationDomain,
16787 ];
16788 };
16789
16790 $resolver->setDefaults([
16791 'years' => range(date('Y') - 10, date('Y') + 10),
16792 'weeks' => array_combine(range(1, 53), range(1, 53)),
16793 'widget' => 'single_text',
16794 'input' => 'array',
16795 'placeholder' => $placeholderDefault,
16796 'html5' => static fn (Options $options) => 'single_text' === $options['widget'],
16797 'error_bubbling' => false,
16798 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '',
16799 'compound' => $compound,
16800 'choice_translation_domain' => false,
16801 'invalid_message' => 'Please enter a valid week.',
16802 ]);
16803
16804 $resolver->setNormalizer('placeholder', $placeholderNormalizer);
16805 $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
16806 $resolver->setNormalizer('html5', static function (Options $options, $html5) {
16807 if ($html5 && 'single_text' !== $options['widget']) {
16808 throw new LogicException(\sprintf('The "widget" option of "%s" must be set to "single_text" when the "html5" option is enabled.', self::class));
16809 }
16810
16811 return $html5;
16812 });
16813
16814 $resolver->setAllowedValues('input', [
16815 'string',
16816 'array',
16817 ]);
16818
16819 $resolver->setAllowedValues('widget', [
16820 'single_text',
16821 'text',
16822 'choice',
16823 ]);
16824
16825 $resolver->setAllowedTypes('years', 'int[]');
16826 $resolver->setAllowedTypes('weeks', 'int[]');
16827 }
16828
16829 public function getBlockPrefix(): string
16830 {
16831 return 'week';
16832 }
16833}
16834
16835------------------------------------------------------------------------------------------------------------------------
16836 ./Extension/Core/Type/MoneyType.php
16837------------------------------------------------------------------------------------------------------------------------
16838<?php
16839
16840/*
16841 * This file is part of the Symfony package.
16842 *
16843 * (c) Fabien Potencier <fabien@symfony.com>
16844 *
16845 * For the full copyright and license information, please view the LICENSE
16846 * file that was distributed with this source code.
16847 */
16848
16849namespace Symfony\Component\Form\Extension\Core\Type;
16850
16851use Symfony\Component\Form\AbstractType;
16852use Symfony\Component\Form\Exception\LogicException;
16853use Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer;
16854use Symfony\Component\Form\FormBuilderInterface;
16855use Symfony\Component\Form\FormInterface;
16856use Symfony\Component\Form\FormView;
16857use Symfony\Component\OptionsResolver\Options;
16858use Symfony\Component\OptionsResolver\OptionsResolver;
16859
16860class MoneyType extends AbstractType
16861{
16862 protected static array $patterns = [];
16863
16864 public function buildForm(FormBuilderInterface $builder, array $options): void
16865 {
16866 // Values used in HTML5 number inputs should be formatted as in "1234.5", ie. 'en' format without grouping,
16867 // according to https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats
16868 $builder
16869 ->addViewTransformer(new MoneyToLocalizedStringTransformer(
16870 $options['scale'],
16871 $options['grouping'],
16872 $options['rounding_mode'],
16873 $options['divisor'],
16874 $options['html5'] ? 'en' : null,
16875 $options['input'],
16876 ))
16877 ;
16878 }
16879
16880 public function buildView(FormView $view, FormInterface $form, array $options): void
16881 {
16882 $view->vars['money_pattern'] = self::getPattern($options['currency']);
16883
16884 if ($options['html5']) {
16885 $view->vars['type'] = 'number';
16886 }
16887 }
16888
16889 public function configureOptions(OptionsResolver $resolver): void
16890 {
16891 $resolver->setDefaults([
16892 'scale' => 2,
16893 'grouping' => false,
16894 'rounding_mode' => \NumberFormatter::ROUND_HALFUP,
16895 'divisor' => 1,
16896 'currency' => 'EUR',
16897 'compound' => false,
16898 'html5' => false,
16899 'invalid_message' => 'Please enter a valid money amount.',
16900 'input' => 'float',
16901 ]);
16902
16903 $resolver->setAllowedValues('rounding_mode', [
16904 \NumberFormatter::ROUND_FLOOR,
16905 \NumberFormatter::ROUND_DOWN,
16906 \NumberFormatter::ROUND_HALFDOWN,
16907 \NumberFormatter::ROUND_HALFEVEN,
16908 \NumberFormatter::ROUND_HALFUP,
16909 \NumberFormatter::ROUND_UP,
16910 \NumberFormatter::ROUND_CEILING,
16911 ]);
16912
16913 $resolver->setAllowedTypes('scale', 'int');
16914
16915 $resolver->setAllowedTypes('html5', 'bool');
16916
16917 $resolver->setAllowedValues('input', ['float', 'integer']);
16918
16919 $resolver->setNormalizer('grouping', static function (Options $options, $value) {
16920 if ($value && $options['html5']) {
16921 throw new LogicException('Cannot use the "grouping" option when the "html5" option is enabled.');
16922 }
16923
16924 return $value;
16925 });
16926 }
16927
16928 public function getBlockPrefix(): string
16929 {
16930 return 'money';
16931 }
16932
16933 /**
16934 * Returns the pattern for this locale in UTF-8.
16935 *
16936 * The pattern contains the placeholder "{{ widget }}" where the HTML tag should
16937 * be inserted
16938 */
16939 protected static function getPattern(?string $currency): string
16940 {
16941 if (!$currency) {
16942 return '{{ widget }}';
16943 }
16944
16945 $locale = \Locale::getDefault();
16946
16947 if (!isset(self::$patterns[$locale])) {
16948 self::$patterns[$locale] = [];
16949 }
16950
16951 if (!isset(self::$patterns[$locale][$currency])) {
16952 $format = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
16953 $pattern = $format->formatCurrency('123', $currency);
16954
16955 // the spacings between currency symbol and number are ignored, because
16956 // a single space leads to better readability in combination with input
16957 // fields
16958
16959 // the regex also considers non-break spaces (0xC2 or 0xA0 in UTF-8)
16960
16961 preg_match('/^([^\s\xc2\xa0]*)[\s\xc2\xa0]*123(?:[,.]0+)?[\s\xc2\xa0]*([^\s\xc2\xa0]*)$/u', $pattern, $matches);
16962
16963 if (!empty($matches[1])) {
16964 self::$patterns[$locale][$currency] = $matches[1].' {{ widget }}';
16965 } elseif (!empty($matches[2])) {
16966 self::$patterns[$locale][$currency] = '{{ widget }} '.$matches[2];
16967 } else {
16968 self::$patterns[$locale][$currency] = '{{ widget }}';
16969 }
16970 }
16971
16972 return self::$patterns[$locale][$currency];
16973 }
16974}
16975
16976------------------------------------------------------------------------------------------------------------------------
16977 ./Extension/Core/Type/EmailType.php
16978------------------------------------------------------------------------------------------------------------------------
16979<?php
16980
16981/*
16982 * This file is part of the Symfony package.
16983 *
16984 * (c) Fabien Potencier <fabien@symfony.com>
16985 *
16986 * For the full copyright and license information, please view the LICENSE
16987 * file that was distributed with this source code.
16988 */
16989
16990namespace Symfony\Component\Form\Extension\Core\Type;
16991
16992use Symfony\Component\Form\AbstractType;
16993use Symfony\Component\OptionsResolver\OptionsResolver;
16994
16995class EmailType extends AbstractType
16996{
16997 public function configureOptions(OptionsResolver $resolver): void
16998 {
16999 $resolver->setDefaults([
17000 'invalid_message' => 'Please enter a valid email address.',
17001 ]);
17002 }
17003
17004 public function getParent(): ?string
17005 {
17006 return TextType::class;
17007 }
17008
17009 public function getBlockPrefix(): string
17010 {
17011 return 'email';
17012 }
17013}
17014
17015------------------------------------------------------------------------------------------------------------------------
17016 ./Extension/Core/Type/TextType.php
17017------------------------------------------------------------------------------------------------------------------------
17018<?php
17019
17020/*
17021 * This file is part of the Symfony package.
17022 *
17023 * (c) Fabien Potencier <fabien@symfony.com>
17024 *
17025 * For the full copyright and license information, please view the LICENSE
17026 * file that was distributed with this source code.
17027 */
17028
17029namespace Symfony\Component\Form\Extension\Core\Type;
17030
17031use Symfony\Component\Form\AbstractType;
17032use Symfony\Component\Form\DataTransformerInterface;
17033use Symfony\Component\Form\FormBuilderInterface;
17034use Symfony\Component\OptionsResolver\OptionsResolver;
17035
17036class TextType extends AbstractType implements DataTransformerInterface
17037{
17038 public function buildForm(FormBuilderInterface $builder, array $options): void
17039 {
17040 // When empty_data is explicitly set to an empty string,
17041 // a string should always be returned when NULL is submitted
17042 // This gives more control and thus helps preventing some issues
17043 // with PHP 7 which allows type hinting strings in functions
17044 // See https://github.com/symfony/symfony/issues/5906#issuecomment-203189375
17045 if ('' === $options['empty_data']) {
17046 $builder->addViewTransformer($this);
17047 }
17048 }
17049
17050 public function configureOptions(OptionsResolver $resolver): void
17051 {
17052 $resolver->setDefaults([
17053 'compound' => false,
17054 ]);
17055 }
17056
17057 public function getBlockPrefix(): string
17058 {
17059 return 'text';
17060 }
17061
17062 public function transform(mixed $data): mixed
17063 {
17064 // Model data should not be transformed
17065 return $data;
17066 }
17067
17068 public function reverseTransform(mixed $data): mixed
17069 {
17070 return $data ?? '';
17071 }
17072}
17073
17074------------------------------------------------------------------------------------------------------------------------
17075 ./Extension/Core/Type/FormType.php
17076------------------------------------------------------------------------------------------------------------------------
17077<?php
17078
17079/*
17080 * This file is part of the Symfony package.
17081 *
17082 * (c) Fabien Potencier <fabien@symfony.com>
17083 *
17084 * For the full copyright and license information, please view the LICENSE
17085 * file that was distributed with this source code.
17086 */
17087
17088namespace Symfony\Component\Form\Extension\Core\Type;
17089
17090use Symfony\Component\Form\Exception\LogicException;
17091use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor;
17092use Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor;
17093use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
17094use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
17095use Symfony\Component\Form\Extension\Core\EventListener\TrimListener;
17096use Symfony\Component\Form\FormBuilderInterface;
17097use Symfony\Component\Form\FormInterface;
17098use Symfony\Component\Form\FormView;
17099use Symfony\Component\OptionsResolver\Options;
17100use Symfony\Component\OptionsResolver\OptionsResolver;
17101use Symfony\Component\PropertyAccess\PropertyAccess;
17102use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
17103use Symfony\Contracts\Translation\TranslatableInterface;
17104
17105class FormType extends BaseType
17106{
17107 private DataMapper $dataMapper;
17108
17109 public function __construct(?PropertyAccessorInterface $propertyAccessor = null)
17110 {
17111 $this->dataMapper = new DataMapper(new ChainAccessor([
17112 new CallbackAccessor(),
17113 new PropertyPathAccessor($propertyAccessor ?? PropertyAccess::createPropertyAccessor()),
17114 ]));
17115 }
17116
17117 public function buildForm(FormBuilderInterface $builder, array $options): void
17118 {
17119 parent::buildForm($builder, $options);
17120
17121 $isDataOptionSet = \array_key_exists('data', $options);
17122
17123 $builder
17124 ->setRequired($options['required'])
17125 ->setErrorBubbling($options['error_bubbling'])
17126 ->setEmptyData($options['empty_data'])
17127 ->setPropertyPath($options['property_path'])
17128 ->setMapped($options['mapped'])
17129 ->setByReference($options['by_reference'])
17130 ->setInheritData($options['inherit_data'])
17131 ->setCompound($options['compound'])
17132 ->setData($isDataOptionSet ? $options['data'] : null)
17133 ->setDataLocked($isDataOptionSet)
17134 ->setDataMapper($options['compound'] ? $this->dataMapper : null)
17135 ->setMethod($options['method'])
17136 ->setAction($options['action']);
17137
17138 if ($options['trim']) {
17139 $builder->addEventSubscriber(new TrimListener());
17140 }
17141
17142 $builder->setIsEmptyCallback($options['is_empty_callback']);
17143 }
17144
17145 public function buildView(FormView $view, FormInterface $form, array $options): void
17146 {
17147 parent::buildView($view, $form, $options);
17148
17149 $name = $form->getName();
17150 $helpTranslationParameters = $options['help_translation_parameters'];
17151
17152 if ($view->parent) {
17153 if ('' === $name) {
17154 throw new LogicException('Form node with empty name can be used only as root form node.');
17155 }
17156
17157 // Complex fields are read-only if they themselves or their parents are.
17158 if (!isset($view->vars['attr']['readonly']) && isset($view->parent->vars['attr']['readonly']) && false !== $view->parent->vars['attr']['readonly']) {
17159 $view->vars['attr']['readonly'] = true;
17160 }
17161
17162 $helpTranslationParameters = array_merge($view->parent->vars['help_translation_parameters'], $helpTranslationParameters);
17163 }
17164
17165 $formConfig = $form->getConfig();
17166 $view->vars = array_replace($view->vars, [
17167 'errors' => $form->getErrors(),
17168 'valid' => $form->isSubmitted() ? $form->isValid() : true,
17169 'value' => $form->getViewData(),
17170 'data' => $form->getNormData(),
17171 'required' => $form->isRequired(),
17172 'label_attr' => $options['label_attr'],
17173 'help' => $options['help'],
17174 'help_attr' => $options['help_attr'],
17175 'help_html' => $options['help_html'],
17176 'help_translation_parameters' => $helpTranslationParameters,
17177 'compound' => $formConfig->getCompound(),
17178 'method' => $formConfig->getMethod(),
17179 'action' => $formConfig->getAction(),
17180 'submitted' => $form->isSubmitted(),
17181 ]);
17182 }
17183
17184 public function finishView(FormView $view, FormInterface $form, array $options): void
17185 {
17186 $multipart = false;
17187
17188 foreach ($view->children as $child) {
17189 if ($child->vars['multipart']) {
17190 $multipart = true;
17191 break;
17192 }
17193 }
17194
17195 $view->vars['multipart'] = $multipart;
17196 }
17197
17198 public function configureOptions(OptionsResolver $resolver): void
17199 {
17200 parent::configureOptions($resolver);
17201
17202 // Derive "data_class" option from passed "data" object
17203 $dataClass = static fn (Options $options) => isset($options['data']) && \is_object($options['data']) ? $options['data']::class : null;
17204
17205 // Derive "empty_data" closure from "data_class" option
17206 $emptyData = static function (Options $options) {
17207 $class = $options['data_class'];
17208
17209 if (null !== $class) {
17210 return static fn (FormInterface $form) => $form->isEmpty() && !$form->isRequired() ? null : new $class();
17211 }
17212
17213 return static fn (FormInterface $form) => $form->getConfig()->getCompound() ? [] : '';
17214 };
17215
17216 // Wrap "post_max_size_message" in a closure to translate it lazily
17217 $uploadMaxSizeMessage = static fn (Options $options) => static fn () => $options['post_max_size_message'];
17218
17219 // For any form that is not represented by a single HTML control,
17220 // errors should bubble up by default
17221 $errorBubbling = static fn (Options $options) => $options['compound'] && !$options['inherit_data'];
17222
17223 // If data is given, the form is locked to that data
17224 // (independent of its value)
17225 $resolver->setDefined([
17226 'data',
17227 ]);
17228
17229 $resolver->setDefaults([
17230 'data_class' => $dataClass,
17231 'empty_data' => $emptyData,
17232 'trim' => true,
17233 'required' => true,
17234 'property_path' => null,
17235 'mapped' => true,
17236 'by_reference' => true,
17237 'error_bubbling' => $errorBubbling,
17238 'label_attr' => [],
17239 'inherit_data' => false,
17240 'compound' => true,
17241 'method' => 'POST',
17242 // According to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt)
17243 // section 4.2., empty URIs are considered same-document references
17244 'action' => '',
17245 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.',
17246 'upload_max_size_message' => $uploadMaxSizeMessage, // internal
17247 'allow_file_upload' => false,
17248 'help' => null,
17249 'help_attr' => [],
17250 'help_html' => false,
17251 'help_translation_parameters' => [],
17252 'invalid_message' => 'This value is not valid.',
17253 'invalid_message_parameters' => [],
17254 'is_empty_callback' => null,
17255 'getter' => null,
17256 'setter' => null,
17257 ]);
17258
17259 $resolver->setAllowedTypes('label_attr', 'array');
17260 $resolver->setAllowedTypes('action', 'string');
17261 $resolver->setAllowedTypes('upload_max_size_message', ['callable']);
17262 $resolver->setAllowedTypes('help', ['string', 'null', TranslatableInterface::class]);
17263 $resolver->setAllowedTypes('help_attr', 'array');
17264 $resolver->setAllowedTypes('help_html', 'bool');
17265 $resolver->setAllowedTypes('is_empty_callback', ['null', 'callable']);
17266 $resolver->setAllowedTypes('getter', ['null', 'callable']);
17267 $resolver->setAllowedTypes('setter', ['null', 'callable']);
17268
17269 $resolver->setInfo('getter', 'A callable that accepts two arguments (the view data and the current form field) and must return a value.');
17270 $resolver->setInfo('setter', 'A callable that accepts three arguments (a reference to the view data, the submitted value and the current form field).');
17271 }
17272
17273 public function getParent(): ?string
17274 {
17275 return null;
17276 }
17277
17278 public function getBlockPrefix(): string
17279 {
17280 return 'form';
17281 }
17282}
17283
17284------------------------------------------------------------------------------------------------------------------------
17285 ./Extension/Core/Type/RangeType.php
17286------------------------------------------------------------------------------------------------------------------------
17287<?php
17288
17289/*
17290 * This file is part of the Symfony package.
17291 *
17292 * (c) Fabien Potencier <fabien@symfony.com>
17293 *
17294 * For the full copyright and license information, please view the LICENSE
17295 * file that was distributed with this source code.
17296 */
17297
17298namespace Symfony\Component\Form\Extension\Core\Type;
17299
17300use Symfony\Component\Form\AbstractType;
17301use Symfony\Component\OptionsResolver\OptionsResolver;
17302
17303class RangeType extends AbstractType
17304{
17305 public function configureOptions(OptionsResolver $resolver): void
17306 {
17307 $resolver->setDefaults([
17308 'invalid_message' => 'Please choose a valid range.',
17309 ]);
17310 }
17311
17312 public function getParent(): ?string
17313 {
17314 return TextType::class;
17315 }
17316
17317 public function getBlockPrefix(): string
17318 {
17319 return 'range';
17320 }
17321}
17322
17323------------------------------------------------------------------------------------------------------------------------
17324 ./Extension/Core/Type/PasswordType.php
17325------------------------------------------------------------------------------------------------------------------------
17326<?php
17327
17328/*
17329 * This file is part of the Symfony package.
17330 *
17331 * (c) Fabien Potencier <fabien@symfony.com>
17332 *
17333 * For the full copyright and license information, please view the LICENSE
17334 * file that was distributed with this source code.
17335 */
17336
17337namespace Symfony\Component\Form\Extension\Core\Type;
17338
17339use Symfony\Component\Form\AbstractType;
17340use Symfony\Component\Form\FormInterface;
17341use Symfony\Component\Form\FormView;
17342use Symfony\Component\OptionsResolver\OptionsResolver;
17343
17344class PasswordType extends AbstractType
17345{
17346 public function buildView(FormView $view, FormInterface $form, array $options): void
17347 {
17348 if ($options['always_empty'] || !$form->isSubmitted()) {
17349 $view->vars['value'] = '';
17350 }
17351 }
17352
17353 public function configureOptions(OptionsResolver $resolver): void
17354 {
17355 $resolver->setDefaults([
17356 'always_empty' => true,
17357 'trim' => false,
17358 'invalid_message' => 'The password is invalid.',
17359 ]);
17360 }
17361
17362 public function getParent(): ?string
17363 {
17364 return TextType::class;
17365 }
17366
17367 public function getBlockPrefix(): string
17368 {
17369 return 'password';
17370 }
17371}
17372
17373------------------------------------------------------------------------------------------------------------------------
17374 ./Extension/Core/Type/FileType.php
17375------------------------------------------------------------------------------------------------------------------------
17376<?php
17377
17378/*
17379 * This file is part of the Symfony package.
17380 *
17381 * (c) Fabien Potencier <fabien@symfony.com>
17382 *
17383 * For the full copyright and license information, please view the LICENSE
17384 * file that was distributed with this source code.
17385 */
17386
17387namespace Symfony\Component\Form\Extension\Core\Type;
17388
17389use Symfony\Component\Form\AbstractType;
17390use Symfony\Component\Form\Event\PreSubmitEvent;
17391use Symfony\Component\Form\FileUploadError;
17392use Symfony\Component\Form\FormBuilderInterface;
17393use Symfony\Component\Form\FormEvent;
17394use Symfony\Component\Form\FormEvents;
17395use Symfony\Component\Form\FormInterface;
17396use Symfony\Component\Form\FormView;
17397use Symfony\Component\HttpFoundation\File\File;
17398use Symfony\Component\OptionsResolver\Options;
17399use Symfony\Component\OptionsResolver\OptionsResolver;
17400use Symfony\Contracts\Translation\TranslatorInterface;
17401
17402class FileType extends AbstractType
17403{
17404 public const KIB_BYTES = 1024;
17405 public const MIB_BYTES = 1048576;
17406
17407 private const SUFFIXES = [
17408 1 => 'bytes',
17409 self::KIB_BYTES => 'KiB',
17410 self::MIB_BYTES => 'MiB',
17411 ];
17412
17413 public function __construct(
17414 private ?TranslatorInterface $translator = null,
17415 ) {
17416 }
17417
17418 public function buildForm(FormBuilderInterface $builder, array $options): void
17419 {
17420 // Ensure that submitted data is always an uploaded file or an array of some
17421 $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
17422 /** @var PreSubmitEvent $event */
17423 $form = $event->getForm();
17424 $requestHandler = $form->getConfig()->getRequestHandler();
17425
17426 if ($options['multiple']) {
17427 $data = [];
17428 $files = $event->getData();
17429
17430 if (!\is_array($files)) {
17431 $files = [];
17432 }
17433
17434 foreach ($files as $file) {
17435 if ($requestHandler->isFileUpload($file)) {
17436 $data[] = $file;
17437
17438 if (method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($file)) {
17439 $form->addError($this->getFileUploadError($errorCode));
17440 }
17441 }
17442 }
17443
17444 // Since the array is never considered empty in the view data format
17445 // on submission, we need to evaluate the configured empty data here
17446 if ([] === $data) {
17447 $emptyData = $form->getConfig()->getEmptyData();
17448 $data = $emptyData instanceof \Closure ? $emptyData($form, $data) : $emptyData;
17449 }
17450
17451 $event->setData($data);
17452 } elseif ($requestHandler->isFileUpload($event->getData()) && method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($event->getData())) {
17453 $form->addError($this->getFileUploadError($errorCode));
17454 } elseif (!$requestHandler->isFileUpload($event->getData())) {
17455 $event->setData(null);
17456 }
17457 });
17458 }
17459
17460 public function buildView(FormView $view, FormInterface $form, array $options): void
17461 {
17462 if ($options['multiple']) {
17463 $view->vars['full_name'] .= '[]';
17464 $view->vars['attr']['multiple'] = 'multiple';
17465 }
17466
17467 $view->vars = array_replace($view->vars, [
17468 'type' => 'file',
17469 'value' => '',
17470 ]);
17471 }
17472
17473 public function finishView(FormView $view, FormInterface $form, array $options): void
17474 {
17475 $view->vars['multipart'] = true;
17476 }
17477
17478 public function configureOptions(OptionsResolver $resolver): void
17479 {
17480 $dataClass = null;
17481 if (class_exists(File::class)) {
17482 $dataClass = static fn (Options $options) => $options['multiple'] ? null : File::class;
17483 }
17484
17485 $emptyData = static fn (Options $options) => $options['multiple'] ? [] : null;
17486
17487 $resolver->setDefaults([
17488 'compound' => false,
17489 'data_class' => $dataClass,
17490 'empty_data' => $emptyData,
17491 'multiple' => false,
17492 'allow_file_upload' => true,
17493 'invalid_message' => 'Please select a valid file.',
17494 ]);
17495 }
17496
17497 public function getBlockPrefix(): string
17498 {
17499 return 'file';
17500 }
17501
17502 private function getFileUploadError(int $errorCode): FileUploadError
17503 {
17504 $messageParameters = [];
17505
17506 if (\UPLOAD_ERR_INI_SIZE === $errorCode) {
17507 [$limitAsString, $suffix] = $this->factorizeSizes(0, self::getMaxFilesize());
17508 $messageTemplate = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.';
17509 $messageParameters = [
17510 '{{ limit }}' => $limitAsString,
17511 '{{ suffix }}' => $suffix,
17512 ];
17513 } elseif (\UPLOAD_ERR_FORM_SIZE === $errorCode) {
17514 $messageTemplate = 'The file is too large.';
17515 } else {
17516 $messageTemplate = 'The file could not be uploaded.';
17517 }
17518
17519 if (null !== $this->translator) {
17520 $message = $this->translator->trans($messageTemplate, $messageParameters, 'validators');
17521 } else {
17522 $message = strtr($messageTemplate, $messageParameters);
17523 }
17524
17525 return new FileUploadError($message, $messageTemplate, $messageParameters);
17526 }
17527
17528 /**
17529 * Returns the maximum size of an uploaded file as configured in php.ini.
17530 *
17531 * This method should be kept in sync with Symfony\Component\HttpFoundation\File\UploadedFile::getMaxFilesize().
17532 */
17533 private static function getMaxFilesize(): int|float
17534 {
17535 $iniMax = strtolower(\ini_get('upload_max_filesize'));
17536
17537 if ('' === $iniMax) {
17538 return \PHP_INT_MAX;
17539 }
17540
17541 $max = ltrim($iniMax, '+');
17542 if (str_starts_with($max, '0x')) {
17543 $max = \intval($max, 16);
17544 } elseif (str_starts_with($max, '0')) {
17545 $max = \intval($max, 8);
17546 } else {
17547 $max = (int) $max;
17548 }
17549
17550 switch (substr($iniMax, -1)) {
17551 case 't': $max *= 1024;
17552 // no break
17553 case 'g': $max *= 1024;
17554 // no break
17555 case 'm': $max *= 1024;
17556 // no break
17557 case 'k': $max *= 1024;
17558 }
17559
17560 return $max;
17561 }
17562
17563 /**
17564 * Converts the limit to the smallest possible number
17565 * (i.e. try "MB", then "kB", then "bytes").
17566 *
17567 * This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::factorizeSizes().
17568 */
17569 private function factorizeSizes(int $size, int|float $limit): array
17570 {
17571 $coef = self::MIB_BYTES;
17572 $coefFactor = self::KIB_BYTES;
17573
17574 $limitAsString = (string) ($limit / $coef);
17575
17576 // Restrict the limit to 2 decimals (without rounding! we
17577 // need the precise value)
17578 while (self::moreDecimalsThan($limitAsString, 2)) {
17579 $coef /= $coefFactor;
17580 $limitAsString = (string) ($limit / $coef);
17581 }
17582
17583 // Convert size to the same measure, but round to 2 decimals
17584 $sizeAsString = (string) round($size / $coef, 2);
17585
17586 // If the size and limit produce the same string output
17587 // (due to rounding), reduce the coefficient
17588 while ($sizeAsString === $limitAsString) {
17589 $coef /= $coefFactor;
17590 $limitAsString = (string) ($limit / $coef);
17591 $sizeAsString = (string) round($size / $coef, 2);
17592 }
17593
17594 return [$limitAsString, self::SUFFIXES[$coef]];
17595 }
17596
17597 /**
17598 * This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::moreDecimalsThan().
17599 */
17600 private static function moreDecimalsThan(string $double, int $numberOfDecimals): bool
17601 {
17602 return \strlen($double) > \strlen(round($double, $numberOfDecimals));
17603 }
17604}
17605
17606------------------------------------------------------------------------------------------------------------------------
17607 ./Extension/Core/Type/CountryType.php
17608------------------------------------------------------------------------------------------------------------------------
17609<?php
17610
17611/*
17612 * This file is part of the Symfony package.
17613 *
17614 * (c) Fabien Potencier <fabien@symfony.com>
17615 *
17616 * For the full copyright and license information, please view the LICENSE
17617 * file that was distributed with this source code.
17618 */
17619
17620namespace Symfony\Component\Form\Extension\Core\Type;
17621
17622use Symfony\Component\Form\AbstractType;
17623use Symfony\Component\Form\ChoiceList\ChoiceList;
17624use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
17625use Symfony\Component\Form\Exception\LogicException;
17626use Symfony\Component\Intl\Countries;
17627use Symfony\Component\Intl\Intl;
17628use Symfony\Component\OptionsResolver\Options;
17629use Symfony\Component\OptionsResolver\OptionsResolver;
17630
17631class CountryType extends AbstractType
17632{
17633 public function configureOptions(OptionsResolver $resolver): void
17634 {
17635 $resolver->setDefaults([
17636 'choice_loader' => function (Options $options) {
17637 if (!class_exists(Intl::class)) {
17638 throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
17639 }
17640
17641 $choiceTranslationLocale = $options['choice_translation_locale'];
17642 $alpha3 = $options['alpha3'];
17643
17644 return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip($alpha3 ? Countries::getAlpha3Names($choiceTranslationLocale) : Countries::getNames($choiceTranslationLocale))), [$choiceTranslationLocale, $alpha3]);
17645 },
17646 'choice_translation_domain' => false,
17647 'choice_translation_locale' => null,
17648 'alpha3' => false,
17649 'invalid_message' => 'Please select a valid country.',
17650 ]);
17651
17652 $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
17653 $resolver->setAllowedTypes('alpha3', 'bool');
17654 }
17655
17656 public function getParent(): ?string
17657 {
17658 return ChoiceType::class;
17659 }
17660
17661 public function getBlockPrefix(): string
17662 {
17663 return 'country';
17664 }
17665}
17666
17667------------------------------------------------------------------------------------------------------------------------
17668 ./Extension/Core/Type/ButtonType.php
17669------------------------------------------------------------------------------------------------------------------------
17670<?php
17671
17672/*
17673 * This file is part of the Symfony package.
17674 *
17675 * (c) Fabien Potencier <fabien@symfony.com>
17676 *
17677 * For the full copyright and license information, please view the LICENSE
17678 * file that was distributed with this source code.
17679 */
17680
17681namespace Symfony\Component\Form\Extension\Core\Type;
17682
17683use Symfony\Component\Form\ButtonTypeInterface;
17684use Symfony\Component\OptionsResolver\OptionsResolver;
17685
17686/**
17687 * A form button.
17688 *
17689 * @author Bernhard Schussek <bschussek@gmail.com>
17690 */
17691class ButtonType extends BaseType implements ButtonTypeInterface
17692{
17693 public function getParent(): ?string
17694 {
17695 return null;
17696 }
17697
17698 public function getBlockPrefix(): string
17699 {
17700 return 'button';
17701 }
17702
17703 public function configureOptions(OptionsResolver $resolver): void
17704 {
17705 parent::configureOptions($resolver);
17706
17707 $resolver->setDefault('auto_initialize', false);
17708 }
17709}
17710
17711------------------------------------------------------------------------------------------------------------------------
17712 ./Extension/Core/Type/PercentType.php
17713------------------------------------------------------------------------------------------------------------------------
17714<?php
17715
17716/*
17717 * This file is part of the Symfony package.
17718 *
17719 * (c) Fabien Potencier <fabien@symfony.com>
17720 *
17721 * For the full copyright and license information, please view the LICENSE
17722 * file that was distributed with this source code.
17723 */
17724
17725namespace Symfony\Component\Form\Extension\Core\Type;
17726
17727use Symfony\Component\Form\AbstractType;
17728use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer;
17729use Symfony\Component\Form\FormBuilderInterface;
17730use Symfony\Component\Form\FormInterface;
17731use Symfony\Component\Form\FormView;
17732use Symfony\Component\OptionsResolver\OptionsResolver;
17733
17734class PercentType extends AbstractType
17735{
17736 public function buildForm(FormBuilderInterface $builder, array $options): void
17737 {
17738 $builder->addViewTransformer(new PercentToLocalizedStringTransformer(
17739 $options['scale'],
17740 $options['type'],
17741 $options['rounding_mode'],
17742 $options['html5']
17743 ));
17744 }
17745
17746 public function buildView(FormView $view, FormInterface $form, array $options): void
17747 {
17748 $view->vars['symbol'] = $options['symbol'];
17749
17750 if ($options['html5']) {
17751 $view->vars['type'] = 'number';
17752 }
17753 }
17754
17755 public function configureOptions(OptionsResolver $resolver): void
17756 {
17757 $resolver->setDefaults([
17758 'scale' => 0,
17759 'rounding_mode' => \NumberFormatter::ROUND_HALFUP,
17760 'symbol' => '%',
17761 'type' => 'fractional',
17762 'compound' => false,
17763 'html5' => false,
17764 'invalid_message' => 'Please enter a percentage value.',
17765 ]);
17766
17767 $resolver->setAllowedValues('type', [
17768 'fractional',
17769 'integer',
17770 ]);
17771 $resolver->setAllowedValues('rounding_mode', [
17772 \NumberFormatter::ROUND_FLOOR,
17773 \NumberFormatter::ROUND_DOWN,
17774 \NumberFormatter::ROUND_HALFDOWN,
17775 \NumberFormatter::ROUND_HALFEVEN,
17776 \NumberFormatter::ROUND_HALFUP,
17777 \NumberFormatter::ROUND_UP,
17778 \NumberFormatter::ROUND_CEILING,
17779 ]);
17780 $resolver->setAllowedTypes('scale', 'int');
17781 $resolver->setAllowedTypes('symbol', ['bool', 'string']);
17782 $resolver->setAllowedTypes('html5', 'bool');
17783 }
17784
17785 public function getBlockPrefix(): string
17786 {
17787 return 'percent';
17788 }
17789}
17790
17791------------------------------------------------------------------------------------------------------------------------
17792 ./Extension/Core/Type/BirthdayType.php
17793------------------------------------------------------------------------------------------------------------------------
17794<?php
17795
17796/*
17797 * This file is part of the Symfony package.
17798 *
17799 * (c) Fabien Potencier <fabien@symfony.com>
17800 *
17801 * For the full copyright and license information, please view the LICENSE
17802 * file that was distributed with this source code.
17803 */
17804
17805namespace Symfony\Component\Form\Extension\Core\Type;
17806
17807use Symfony\Component\Form\AbstractType;
17808use Symfony\Component\OptionsResolver\OptionsResolver;
17809
17810class BirthdayType extends AbstractType
17811{
17812 public function configureOptions(OptionsResolver $resolver): void
17813 {
17814 $resolver->setDefaults([
17815 'years' => range((int) date('Y') - 120, date('Y')),
17816 'invalid_message' => 'Please enter a valid birthdate.',
17817 ]);
17818
17819 $resolver->setAllowedTypes('years', 'array');
17820 }
17821
17822 public function getParent(): ?string
17823 {
17824 return DateType::class;
17825 }
17826
17827 public function getBlockPrefix(): string
17828 {
17829 return 'birthday';
17830 }
17831}
17832
17833------------------------------------------------------------------------------------------------------------------------
17834 ./Extension/Core/Type/DateType.php
17835------------------------------------------------------------------------------------------------------------------------
17836<?php
17837
17838/*
17839 * This file is part of the Symfony package.
17840 *
17841 * (c) Fabien Potencier <fabien@symfony.com>
17842 *
17843 * For the full copyright and license information, please view the LICENSE
17844 * file that was distributed with this source code.
17845 */
17846
17847namespace Symfony\Component\Form\Extension\Core\Type;
17848
17849use Symfony\Component\Form\AbstractType;
17850use Symfony\Component\Form\Exception\LogicException;
17851use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
17852use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
17853use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
17854use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
17855use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
17856use Symfony\Component\Form\FormBuilderInterface;
17857use Symfony\Component\Form\FormEvent;
17858use Symfony\Component\Form\FormEvents;
17859use Symfony\Component\Form\FormInterface;
17860use Symfony\Component\Form\FormView;
17861use Symfony\Component\Form\ReversedTransformer;
17862use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
17863use Symfony\Component\OptionsResolver\Options;
17864use Symfony\Component\OptionsResolver\OptionsResolver;
17865
17866class DateType extends AbstractType
17867{
17868 public const DEFAULT_FORMAT = \IntlDateFormatter::MEDIUM;
17869 public const HTML5_FORMAT = 'yyyy-MM-dd';
17870
17871 private const ACCEPTED_FORMATS = [
17872 \IntlDateFormatter::FULL,
17873 \IntlDateFormatter::LONG,
17874 \IntlDateFormatter::MEDIUM,
17875 \IntlDateFormatter::SHORT,
17876 ];
17877
17878 private const WIDGETS = [
17879 'text' => TextType::class,
17880 'choice' => ChoiceType::class,
17881 ];
17882
17883 public function buildForm(FormBuilderInterface $builder, array $options): void
17884 {
17885 $dateFormat = \is_int($options['format']) ? $options['format'] : self::DEFAULT_FORMAT;
17886 $timeFormat = \IntlDateFormatter::NONE;
17887 $calendar = $options['calendar'] ?? \IntlDateFormatter::GREGORIAN;
17888 $pattern = \is_string($options['format']) ? $options['format'] : '';
17889
17890 if (!\in_array($dateFormat, self::ACCEPTED_FORMATS, true)) {
17891 throw new InvalidOptionsException('The "format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
17892 }
17893
17894 if ('single_text' === $options['widget']) {
17895 if ('' !== $pattern && !str_contains($pattern, 'y') && !str_contains($pattern, 'M') && !str_contains($pattern, 'd')) {
17896 throw new InvalidOptionsException(\sprintf('The "format" option should contain the letters "y", "M" or "d". Its current value is "%s".', $pattern));
17897 }
17898
17899 $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
17900 $options['model_timezone'],
17901 $options['view_timezone'],
17902 $dateFormat,
17903 $timeFormat,
17904 $calendar,
17905 $pattern
17906 ));
17907 } else {
17908 if ('' !== $pattern && (!str_contains($pattern, 'y') || !str_contains($pattern, 'M') || !str_contains($pattern, 'd'))) {
17909 throw new InvalidOptionsException(\sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".', $pattern));
17910 }
17911
17912 $yearOptions = $monthOptions = $dayOptions = [
17913 'error_bubbling' => true,
17914 'empty_data' => '',
17915 ];
17916 // when the form is compound the entries of the array are ignored in favor of children data
17917 // so we need to handle the cascade setting here
17918 $emptyData = $builder->getEmptyData() ?: [];
17919
17920 if ($emptyData instanceof \Closure) {
17921 $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) {
17922 $emptyData = $emptyData($form->getParent());
17923
17924 return $emptyData[$option] ?? '';
17925 };
17926
17927 $yearOptions['empty_data'] = $lazyEmptyData('year');
17928 $monthOptions['empty_data'] = $lazyEmptyData('month');
17929 $dayOptions['empty_data'] = $lazyEmptyData('day');
17930 } else {
17931 if (isset($emptyData['year'])) {
17932 $yearOptions['empty_data'] = $emptyData['year'];
17933 }
17934 if (isset($emptyData['month'])) {
17935 $monthOptions['empty_data'] = $emptyData['month'];
17936 }
17937 if (isset($emptyData['day'])) {
17938 $dayOptions['empty_data'] = $emptyData['day'];
17939 }
17940 }
17941
17942 if (isset($options['invalid_message'])) {
17943 $dayOptions['invalid_message'] = $options['invalid_message'];
17944 $monthOptions['invalid_message'] = $options['invalid_message'];
17945 $yearOptions['invalid_message'] = $options['invalid_message'];
17946 }
17947
17948 if (isset($options['invalid_message_parameters'])) {
17949 $dayOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
17950 $monthOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
17951 $yearOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
17952 }
17953
17954 $formatter = new \IntlDateFormatter(
17955 \Locale::getDefault(),
17956 $dateFormat,
17957 $timeFormat,
17958 null,
17959 $calendar,
17960 $pattern
17961 );
17962
17963 $formatter->setLenient(false);
17964
17965 if ('choice' === $options['widget']) {
17966 // Only pass a subset of the options to children
17967 $yearOptions['choices'] = $this->formatTimestamps($formatter, '/y+/', $this->listYears($options['years']));
17968 $yearOptions['placeholder'] = $options['placeholder']['year'];
17969 $yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year'];
17970 $monthOptions['choices'] = $this->formatTimestamps($formatter, '/[M|L]+/', $this->listMonths($options['months']));
17971 $monthOptions['placeholder'] = $options['placeholder']['month'];
17972 $monthOptions['choice_translation_domain'] = $options['choice_translation_domain']['month'];
17973 $dayOptions['choices'] = $this->formatTimestamps($formatter, '/d+/', $this->listDays($options['days']));
17974 $dayOptions['placeholder'] = $options['placeholder']['day'];
17975 $dayOptions['choice_translation_domain'] = $options['choice_translation_domain']['day'];
17976 }
17977
17978 // Append generic carry-along options
17979 foreach (['required', 'translation_domain'] as $passOpt) {
17980 $yearOptions[$passOpt] = $monthOptions[$passOpt] = $dayOptions[$passOpt] = $options[$passOpt];
17981 }
17982
17983 $builder
17984 ->add('year', self::WIDGETS[$options['widget']], $yearOptions)
17985 ->add('month', self::WIDGETS[$options['widget']], $monthOptions)
17986 ->add('day', self::WIDGETS[$options['widget']], $dayOptions)
17987 ->addViewTransformer(new DateTimeToArrayTransformer(
17988 $options['model_timezone'], $options['view_timezone'], ['year', 'month', 'day']
17989 ))
17990 ->setAttribute('formatter', $formatter)
17991 ;
17992 }
17993
17994 if ('datetime_immutable' === $options['input']) {
17995 $builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
17996 } elseif ('string' === $options['input']) {
17997 $builder->addModelTransformer(new ReversedTransformer(
17998 new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format'])
17999 ));
18000 } elseif ('timestamp' === $options['input']) {
18001 $builder->addModelTransformer(new ReversedTransformer(
18002 new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
18003 ));
18004 } elseif ('array' === $options['input']) {
18005 $builder->addModelTransformer(new ReversedTransformer(
18006 new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], ['year', 'month', 'day'])
18007 ));
18008 }
18009
18010 if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) {
18011 $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
18012 $date = $event->getData();
18013
18014 if (!$date instanceof \DateTimeInterface) {
18015 return;
18016 }
18017
18018 if ($date->getTimezone()->getName() !== $options['model_timezone']) {
18019 throw new LogicException(\sprintf('Using a "%s" instance with a timezone ("%s") not matching the configured model timezone "%s" is not supported.', get_debug_type($date), $date->getTimezone()->getName(), $options['model_timezone']));
18020 }
18021 });
18022 }
18023 }
18024
18025 public function finishView(FormView $view, FormInterface $form, array $options): void
18026 {
18027 $view->vars['widget'] = $options['widget'];
18028
18029 // Change the input to an HTML5 date input if
18030 // * the widget is set to "single_text"
18031 // * the format matches the one expected by HTML5
18032 // * the html5 is set to true
18033 if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
18034 $view->vars['type'] = 'date';
18035 }
18036
18037 if ($form->getConfig()->hasAttribute('formatter')) {
18038 $pattern = $form->getConfig()->getAttribute('formatter')->getPattern();
18039
18040 // remove special characters unless the format was explicitly specified
18041 if (!\is_string($options['format'])) {
18042 // remove quoted strings first
18043 $pattern = preg_replace('/\'[^\']+\'/', '', $pattern);
18044
18045 // remove remaining special chars
18046 $pattern = preg_replace('/[^yMd]+/', '', $pattern);
18047 }
18048
18049 // set right order with respect to locale (e.g.: de_DE=dd.MM.yy; en_US=M/d/yy)
18050 // lookup various formats at http://userguide.icu-project.org/formatparse/datetime
18051 if (preg_match('/^([yMd]+)[^yMd]*([yMd]+)[^yMd]*([yMd]+)$/', $pattern)) {
18052 $pattern = preg_replace(['/y+/', '/M+/', '/d+/'], ['{{ year }}', '{{ month }}', '{{ day }}'], $pattern);
18053 } else {
18054 // default fallback
18055 $pattern = '{{ year }}{{ month }}{{ day }}';
18056 }
18057
18058 $view->vars['date_pattern'] = $pattern;
18059 }
18060 }
18061
18062 public function configureOptions(OptionsResolver $resolver): void
18063 {
18064 $compound = static fn (Options $options) => 'single_text' !== $options['widget'];
18065
18066 $placeholderDefault = static fn (Options $options) => $options['required'] ? null : '';
18067
18068 $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) {
18069 if (\is_array($placeholder)) {
18070 $default = $placeholderDefault($options);
18071
18072 return array_merge(
18073 ['year' => $default, 'month' => $default, 'day' => $default],
18074 $placeholder
18075 );
18076 }
18077
18078 return [
18079 'year' => $placeholder,
18080 'month' => $placeholder,
18081 'day' => $placeholder,
18082 ];
18083 };
18084
18085 $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) {
18086 if (\is_array($choiceTranslationDomain)) {
18087 return array_replace(
18088 ['year' => false, 'month' => false, 'day' => false],
18089 $choiceTranslationDomain
18090 );
18091 }
18092
18093 return [
18094 'year' => $choiceTranslationDomain,
18095 'month' => $choiceTranslationDomain,
18096 'day' => $choiceTranslationDomain,
18097 ];
18098 };
18099
18100 $format = static fn (Options $options) => 'single_text' === $options['widget'] ? self::HTML5_FORMAT : self::DEFAULT_FORMAT;
18101
18102 $resolver->setDefaults([
18103 'years' => range((int) date('Y') - 5, (int) date('Y') + 5),
18104 'months' => range(1, 12),
18105 'days' => range(1, 31),
18106 'widget' => 'single_text',
18107 'input' => 'datetime',
18108 'format' => $format,
18109 'model_timezone' => null,
18110 'view_timezone' => null,
18111 'calendar' => null,
18112 'placeholder' => $placeholderDefault,
18113 'html5' => true,
18114 // Don't modify \DateTime classes by reference, we treat
18115 // them like immutable value objects
18116 'by_reference' => false,
18117 'error_bubbling' => false,
18118 // If initialized with a \DateTime object, FormType initializes
18119 // this option to "\DateTime". Since the internal, normalized
18120 // representation is not \DateTime, but an array, we need to unset
18121 // this option.
18122 'data_class' => null,
18123 'compound' => $compound,
18124 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '',
18125 'choice_translation_domain' => false,
18126 'input_format' => 'Y-m-d',
18127 'invalid_message' => 'Please enter a valid date.',
18128 ]);
18129
18130 $resolver->setNormalizer('placeholder', $placeholderNormalizer);
18131 $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
18132
18133 $resolver->setAllowedValues('input', [
18134 'datetime',
18135 'datetime_immutable',
18136 'string',
18137 'timestamp',
18138 'array',
18139 ]);
18140 $resolver->setAllowedValues('widget', [
18141 'single_text',
18142 'text',
18143 'choice',
18144 ]);
18145
18146 $resolver->setAllowedTypes('format', ['int', 'string']);
18147 $resolver->setAllowedTypes('years', 'array');
18148 $resolver->setAllowedTypes('months', 'array');
18149 $resolver->setAllowedTypes('days', 'array');
18150 $resolver->setAllowedTypes('input_format', 'string');
18151 $resolver->setAllowedTypes('calendar', ['null', 'int', \IntlCalendar::class]);
18152
18153 $resolver->setInfo('calendar', 'The calendar to use for formatting and parsing the date. The value should be an instance of \IntlCalendar. By default, the Gregorian calendar with the default locale is used.');
18154
18155 $resolver->setNormalizer('html5', static function (Options $options, $html5) {
18156 if ($html5 && 'single_text' === $options['widget'] && self::HTML5_FORMAT !== $options['format']) {
18157 throw new LogicException(\sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class));
18158 }
18159
18160 return $html5;
18161 });
18162 }
18163
18164 public function getBlockPrefix(): string
18165 {
18166 return 'date';
18167 }
18168
18169 private function formatTimestamps(\IntlDateFormatter $formatter, string $regex, array $timestamps): array
18170 {
18171 $pattern = $formatter->getPattern();
18172 $timezone = $formatter->getTimeZoneId();
18173 $formattedTimestamps = [];
18174
18175 $formatter->setTimeZone('UTC');
18176
18177 if (preg_match($regex, $pattern, $matches)) {
18178 $formatter->setPattern($matches[0]);
18179
18180 foreach ($timestamps as $timestamp => $choice) {
18181 $formattedTimestamps[$formatter->format($timestamp)] = $choice;
18182 }
18183
18184 // I'd like to clone the formatter above, but then we get a
18185 // segmentation fault, so let's restore the old state instead
18186 $formatter->setPattern($pattern);
18187 }
18188
18189 $formatter->setTimeZone($timezone);
18190
18191 return $formattedTimestamps;
18192 }
18193
18194 private function listYears(array $years): array
18195 {
18196 $result = [];
18197
18198 foreach ($years as $year) {
18199 $result[\PHP_INT_SIZE === 4 ? \DateTimeImmutable::createFromFormat('Y e', $year.' UTC')->format('U') : gmmktime(0, 0, 0, 6, 15, $year)] = $year;
18200 }
18201
18202 return $result;
18203 }
18204
18205 private function listMonths(array $months): array
18206 {
18207 $result = [];
18208
18209 foreach ($months as $month) {
18210 $result[gmmktime(0, 0, 0, $month, 15)] = $month;
18211 }
18212
18213 return $result;
18214 }
18215
18216 private function listDays(array $days): array
18217 {
18218 $result = [];
18219
18220 foreach ($days as $day) {
18221 $result[gmmktime(0, 0, 0, 5, $day)] = $day;
18222 }
18223
18224 return $result;
18225 }
18226}
18227
18228------------------------------------------------------------------------------------------------------------------------
18229 ./Extension/Core/Type/LocaleType.php
18230------------------------------------------------------------------------------------------------------------------------
18231<?php
18232
18233/*
18234 * This file is part of the Symfony package.
18235 *
18236 * (c) Fabien Potencier <fabien@symfony.com>
18237 *
18238 * For the full copyright and license information, please view the LICENSE
18239 * file that was distributed with this source code.
18240 */
18241
18242namespace Symfony\Component\Form\Extension\Core\Type;
18243
18244use Symfony\Component\Form\AbstractType;
18245use Symfony\Component\Form\ChoiceList\ChoiceList;
18246use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
18247use Symfony\Component\Form\Exception\LogicException;
18248use Symfony\Component\Intl\Intl;
18249use Symfony\Component\Intl\Locales;
18250use Symfony\Component\OptionsResolver\Options;
18251use Symfony\Component\OptionsResolver\OptionsResolver;
18252
18253class LocaleType extends AbstractType
18254{
18255 public function configureOptions(OptionsResolver $resolver): void
18256 {
18257 $resolver->setDefaults([
18258 'choice_loader' => function (Options $options) {
18259 if (!class_exists(Intl::class)) {
18260 throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
18261 }
18262
18263 $choiceTranslationLocale = $options['choice_translation_locale'];
18264
18265 return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip(Locales::getNames($choiceTranslationLocale))), $choiceTranslationLocale);
18266 },
18267 'choice_translation_domain' => false,
18268 'choice_translation_locale' => null,
18269 'invalid_message' => 'Please select a valid locale.',
18270 ]);
18271
18272 $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
18273 }
18274
18275 public function getParent(): ?string
18276 {
18277 return ChoiceType::class;
18278 }
18279
18280 public function getBlockPrefix(): string
18281 {
18282 return 'locale';
18283 }
18284}
18285
18286------------------------------------------------------------------------------------------------------------------------
18287 ./Extension/Core/Type/CheckboxType.php
18288------------------------------------------------------------------------------------------------------------------------
18289<?php
18290
18291/*
18292 * This file is part of the Symfony package.
18293 *
18294 * (c) Fabien Potencier <fabien@symfony.com>
18295 *
18296 * For the full copyright and license information, please view the LICENSE
18297 * file that was distributed with this source code.
18298 */
18299
18300namespace Symfony\Component\Form\Extension\Core\Type;
18301
18302use Symfony\Component\Form\AbstractType;
18303use Symfony\Component\Form\Extension\Core\DataTransformer\BooleanToStringTransformer;
18304use Symfony\Component\Form\FormBuilderInterface;
18305use Symfony\Component\Form\FormInterface;
18306use Symfony\Component\Form\FormView;
18307use Symfony\Component\OptionsResolver\OptionsResolver;
18308
18309class CheckboxType extends AbstractType
18310{
18311 public function buildForm(FormBuilderInterface $builder, array $options): void
18312 {
18313 // Unlike in other types, where the data is NULL by default, it
18314 // needs to be a Boolean here. setData(null) is not acceptable
18315 // for checkboxes and radio buttons (unless a custom model
18316 // transformer handles this case).
18317 // We cannot solve this case via overriding the "data" option, because
18318 // doing so also calls setDataLocked(true).
18319 $builder->setData($options['data'] ?? false);
18320 $builder->addViewTransformer(new BooleanToStringTransformer($options['value'], $options['false_values']));
18321 }
18322
18323 public function buildView(FormView $view, FormInterface $form, array $options): void
18324 {
18325 $view->vars = array_replace($view->vars, [
18326 'value' => $options['value'],
18327 'checked' => null !== $form->getViewData(),
18328 ]);
18329 }
18330
18331 public function configureOptions(OptionsResolver $resolver): void
18332 {
18333 $emptyData = static fn (FormInterface $form, $viewData) => $viewData;
18334
18335 $resolver->setDefaults([
18336 'value' => '1',
18337 'empty_data' => $emptyData,
18338 'compound' => false,
18339 'false_values' => [null],
18340 'invalid_message' => 'The checkbox has an invalid value.',
18341 'is_empty_callback' => static fn ($modelData): bool => false === $modelData,
18342 ]);
18343
18344 $resolver->setAllowedTypes('false_values', 'array');
18345 }
18346
18347 public function getBlockPrefix(): string
18348 {
18349 return 'checkbox';
18350 }
18351}
18352
18353------------------------------------------------------------------------------------------------------------------------
18354 ./Extension/Core/Type/TextareaType.php
18355------------------------------------------------------------------------------------------------------------------------
18356<?php
18357
18358/*
18359 * This file is part of the Symfony package.
18360 *
18361 * (c) Fabien Potencier <fabien@symfony.com>
18362 *
18363 * For the full copyright and license information, please view the LICENSE
18364 * file that was distributed with this source code.
18365 */
18366
18367namespace Symfony\Component\Form\Extension\Core\Type;
18368
18369use Symfony\Component\Form\AbstractType;
18370use Symfony\Component\Form\FormInterface;
18371use Symfony\Component\Form\FormView;
18372
18373class TextareaType extends AbstractType
18374{
18375 public function buildView(FormView $view, FormInterface $form, array $options): void
18376 {
18377 $view->vars['pattern'] = null;
18378 unset($view->vars['attr']['pattern']);
18379 }
18380
18381 public function getParent(): ?string
18382 {
18383 return TextType::class;
18384 }
18385
18386 public function getBlockPrefix(): string
18387 {
18388 return 'textarea';
18389 }
18390}
18391
18392------------------------------------------------------------------------------------------------------------------------
18393 ./Extension/Core/Type/CollectionType.php
18394------------------------------------------------------------------------------------------------------------------------
18395<?php
18396
18397/*
18398 * This file is part of the Symfony package.
18399 *
18400 * (c) Fabien Potencier <fabien@symfony.com>
18401 *
18402 * For the full copyright and license information, please view the LICENSE
18403 * file that was distributed with this source code.
18404 */
18405
18406namespace Symfony\Component\Form\Extension\Core\Type;
18407
18408use Symfony\Component\Form\AbstractType;
18409use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
18410use Symfony\Component\Form\FormBuilderInterface;
18411use Symfony\Component\Form\FormInterface;
18412use Symfony\Component\Form\FormView;
18413use Symfony\Component\OptionsResolver\Options;
18414use Symfony\Component\OptionsResolver\OptionsResolver;
18415
18416class CollectionType extends AbstractType
18417{
18418 public function buildForm(FormBuilderInterface $builder, array $options): void
18419 {
18420 $resizePrototypeOptions = null;
18421 if ($options['allow_add'] && $options['prototype']) {
18422 $resizePrototypeOptions = array_replace($options['entry_options'], $options['prototype_options']);
18423 $prototypeOptions = array_replace([
18424 'required' => $options['required'],
18425 'label' => $options['prototype_name'].'label__',
18426 ], $resizePrototypeOptions);
18427
18428 if (null !== $options['prototype_data']) {
18429 $prototypeOptions['data'] = $options['prototype_data'];
18430 }
18431
18432 $prototype = $builder->create($options['prototype_name'], $options['entry_type'], $prototypeOptions);
18433 $builder->setAttribute('prototype', $prototype->getForm());
18434 }
18435
18436 $resizeListener = new ResizeFormListener(
18437 $options['entry_type'],
18438 $options['entry_options'],
18439 $options['allow_add'],
18440 $options['allow_delete'],
18441 $options['delete_empty'],
18442 $resizePrototypeOptions,
18443 $options['keep_as_list']
18444 );
18445
18446 $builder->addEventSubscriber($resizeListener);
18447 }
18448
18449 public function buildView(FormView $view, FormInterface $form, array $options): void
18450 {
18451 $view->vars = array_replace($view->vars, [
18452 'allow_add' => $options['allow_add'],
18453 'allow_delete' => $options['allow_delete'],
18454 ]);
18455
18456 if ($form->getConfig()->hasAttribute('prototype')) {
18457 $prototype = $form->getConfig()->getAttribute('prototype');
18458 $view->vars['prototype'] = $prototype->setParent($form)->createView($view);
18459 }
18460 }
18461
18462 public function finishView(FormView $view, FormInterface $form, array $options): void
18463 {
18464 $prefixOffset = -2;
18465 // check if the entry type also defines a block prefix
18466 /** @var FormInterface $entry */
18467 foreach ($form as $entry) {
18468 if ($entry->getConfig()->getOption('block_prefix')) {
18469 --$prefixOffset;
18470 }
18471
18472 break;
18473 }
18474
18475 foreach ($view as $entryView) {
18476 array_splice($entryView->vars['block_prefixes'], $prefixOffset, 0, 'collection_entry');
18477 }
18478
18479 /** @var FormInterface $prototype */
18480 if ($prototype = $form->getConfig()->getAttribute('prototype')) {
18481 if ($view->vars['prototype']->vars['multipart']) {
18482 $view->vars['multipart'] = true;
18483 }
18484
18485 if ($prefixOffset > -3 && $prototype->getConfig()->getOption('block_prefix')) {
18486 --$prefixOffset;
18487 }
18488
18489 array_splice($view->vars['prototype']->vars['block_prefixes'], $prefixOffset, 0, 'collection_entry');
18490 }
18491 }
18492
18493 public function configureOptions(OptionsResolver $resolver): void
18494 {
18495 $entryOptionsNormalizer = static function (Options $options, $value) {
18496 $value['block_name'] = 'entry';
18497
18498 return $value;
18499 };
18500
18501 $resolver->setDefaults([
18502 'allow_add' => false,
18503 'allow_delete' => false,
18504 'prototype' => true,
18505 'prototype_data' => null,
18506 'prototype_name' => '__name__',
18507 'entry_type' => TextType::class,
18508 'entry_options' => [],
18509 'prototype_options' => [],
18510 'delete_empty' => false,
18511 'invalid_message' => 'The collection is invalid.',
18512 'keep_as_list' => false,
18513 ]);
18514
18515 $resolver->setNormalizer('entry_options', $entryOptionsNormalizer);
18516
18517 $resolver->setAllowedTypes('delete_empty', ['bool', 'callable']);
18518 $resolver->setAllowedTypes('prototype_options', 'array');
18519 $resolver->setAllowedTypes('keep_as_list', ['bool']);
18520 }
18521
18522 public function getBlockPrefix(): string
18523 {
18524 return 'collection';
18525 }
18526}
18527
18528------------------------------------------------------------------------------------------------------------------------
18529 ./Extension/Core/Type/UuidType.php
18530------------------------------------------------------------------------------------------------------------------------
18531<?php
18532
18533/*
18534 * This file is part of the Symfony package.
18535 *
18536 * (c) Fabien Potencier <fabien@symfony.com>
18537 *
18538 * For the full copyright and license information, please view the LICENSE
18539 * file that was distributed with this source code.
18540 */
18541
18542namespace Symfony\Component\Form\Extension\Core\Type;
18543
18544use Symfony\Component\Form\AbstractType;
18545use Symfony\Component\Form\Extension\Core\DataTransformer\UuidToStringTransformer;
18546use Symfony\Component\Form\FormBuilderInterface;
18547use Symfony\Component\OptionsResolver\OptionsResolver;
18548
18549/**
18550 * @author Pavel Dyakonov <wapinet@mail.ru>
18551 */
18552class UuidType extends AbstractType
18553{
18554 public function buildForm(FormBuilderInterface $builder, array $options): void
18555 {
18556 $builder
18557 ->addViewTransformer(new UuidToStringTransformer())
18558 ;
18559 }
18560
18561 public function configureOptions(OptionsResolver $resolver): void
18562 {
18563 $resolver->setDefaults([
18564 'compound' => false,
18565 'invalid_message' => 'Please enter a valid UUID.',
18566 ]);
18567 }
18568}
18569
18570------------------------------------------------------------------------------------------------------------------------
18571 ./Extension/Core/DataMapper/RadioListMapper.php
18572------------------------------------------------------------------------------------------------------------------------
18573<?php
18574
18575/*
18576 * This file is part of the Symfony package.
18577 *
18578 * (c) Fabien Potencier <fabien@symfony.com>
18579 *
18580 * For the full copyright and license information, please view the LICENSE
18581 * file that was distributed with this source code.
18582 */
18583
18584namespace Symfony\Component\Form\Extension\Core\DataMapper;
18585
18586use Symfony\Component\Form\DataMapperInterface;
18587use Symfony\Component\Form\Exception\UnexpectedTypeException;
18588
18589/**
18590 * Maps choices to/from radio forms.
18591 *
18592 * A {@link ChoiceListInterface} implementation is used to find the
18593 * corresponding string values for the choices. The radio form whose "value"
18594 * option corresponds to the selected value is marked as selected.
18595 *
18596 * @author Bernhard Schussek <bschussek@gmail.com>
18597 */
18598class RadioListMapper implements DataMapperInterface
18599{
18600 public function mapDataToForms(mixed $choice, \Traversable $radios): void
18601 {
18602 if (!\is_string($choice)) {
18603 throw new UnexpectedTypeException($choice, 'string');
18604 }
18605
18606 foreach ($radios as $radio) {
18607 $value = $radio->getConfig()->getOption('value');
18608 $radio->setData($choice === $value);
18609 }
18610 }
18611
18612 public function mapFormsToData(\Traversable $radios, mixed &$choice): void
18613 {
18614 if (null !== $choice && !\is_string($choice)) {
18615 throw new UnexpectedTypeException($choice, 'null or string');
18616 }
18617
18618 $choice = null;
18619
18620 foreach ($radios as $radio) {
18621 if ($radio->getData()) {
18622 if ('placeholder' === $radio->getName()) {
18623 return;
18624 }
18625
18626 $choice = $radio->getConfig()->getOption('value');
18627
18628 return;
18629 }
18630 }
18631 }
18632}
18633
18634------------------------------------------------------------------------------------------------------------------------
18635 ./Extension/Core/DataMapper/DataMapper.php
18636------------------------------------------------------------------------------------------------------------------------
18637<?php
18638
18639/*
18640 * This file is part of the Symfony package.
18641 *
18642 * (c) Fabien Potencier <fabien@symfony.com>
18643 *
18644 * For the full copyright and license information, please view the LICENSE
18645 * file that was distributed with this source code.
18646 */
18647
18648namespace Symfony\Component\Form\Extension\Core\DataMapper;
18649
18650use Symfony\Component\Form\DataAccessorInterface;
18651use Symfony\Component\Form\DataMapperInterface;
18652use Symfony\Component\Form\Exception\UnexpectedTypeException;
18653use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor;
18654use Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor;
18655use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
18656
18657/**
18658 * Maps arrays/objects to/from forms using data accessors.
18659 *
18660 * @author Bernhard Schussek <bschussek@gmail.com>
18661 */
18662class DataMapper implements DataMapperInterface
18663{
18664 private DataAccessorInterface $dataAccessor;
18665
18666 public function __construct(?DataAccessorInterface $dataAccessor = null)
18667 {
18668 $this->dataAccessor = $dataAccessor ?? new ChainAccessor([
18669 new CallbackAccessor(),
18670 new PropertyPathAccessor(),
18671 ]);
18672 }
18673
18674 public function mapDataToForms(mixed $data, \Traversable $forms): void
18675 {
18676 $empty = null === $data || [] === $data;
18677
18678 if (!$empty && !\is_array($data) && !\is_object($data)) {
18679 throw new UnexpectedTypeException($data, 'object, array or empty');
18680 }
18681
18682 foreach ($forms as $form) {
18683 $config = $form->getConfig();
18684
18685 if (!$empty && $config->getMapped() && $this->dataAccessor->isReadable($data, $form)) {
18686 $form->setData($this->dataAccessor->getValue($data, $form));
18687 } else {
18688 $form->setData($config->getData());
18689 }
18690 }
18691 }
18692
18693 public function mapFormsToData(\Traversable $forms, mixed &$data): void
18694 {
18695 if (null === $data) {
18696 return;
18697 }
18698
18699 if (!\is_array($data) && !\is_object($data)) {
18700 throw new UnexpectedTypeException($data, 'object, array or empty');
18701 }
18702
18703 foreach ($forms as $form) {
18704 $config = $form->getConfig();
18705
18706 // Write-back is disabled if the form is not synchronized (transformation failed),
18707 // if the form was not submitted and if the form is disabled (modification not allowed)
18708 if ($config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled() && $this->dataAccessor->isWritable($data, $form)) {
18709 $this->dataAccessor->setValue($data, $form->getData(), $form);
18710 }
18711 }
18712 }
18713
18714 /**
18715 * @internal
18716 */
18717 public function getDataAccessor(): DataAccessorInterface
18718 {
18719 return $this->dataAccessor;
18720 }
18721}
18722
18723------------------------------------------------------------------------------------------------------------------------
18724 ./Extension/Core/DataMapper/CheckboxListMapper.php
18725------------------------------------------------------------------------------------------------------------------------
18726<?php
18727
18728/*
18729 * This file is part of the Symfony package.
18730 *
18731 * (c) Fabien Potencier <fabien@symfony.com>
18732 *
18733 * For the full copyright and license information, please view the LICENSE
18734 * file that was distributed with this source code.
18735 */
18736
18737namespace Symfony\Component\Form\Extension\Core\DataMapper;
18738
18739use Symfony\Component\Form\DataMapperInterface;
18740use Symfony\Component\Form\Exception\UnexpectedTypeException;
18741
18742/**
18743 * Maps choices to/from checkbox forms.
18744 *
18745 * A {@link ChoiceListInterface} implementation is used to find the
18746 * corresponding string values for the choices. Each checkbox form whose "value"
18747 * option corresponds to any of the selected values is marked as selected.
18748 *
18749 * @author Bernhard Schussek <bschussek@gmail.com>
18750 */
18751class CheckboxListMapper implements DataMapperInterface
18752{
18753 public function mapDataToForms(mixed $choices, \Traversable $checkboxes): void
18754 {
18755 if (!\is_array($choices ??= [])) {
18756 throw new UnexpectedTypeException($choices, 'array');
18757 }
18758
18759 foreach ($checkboxes as $checkbox) {
18760 $value = $checkbox->getConfig()->getOption('value');
18761 $checkbox->setData(\in_array($value, $choices, true));
18762 }
18763 }
18764
18765 public function mapFormsToData(\Traversable $checkboxes, mixed &$choices): void
18766 {
18767 if (!\is_array($choices)) {
18768 throw new UnexpectedTypeException($choices, 'array');
18769 }
18770
18771 $values = [];
18772
18773 foreach ($checkboxes as $checkbox) {
18774 if ($checkbox->getData()) {
18775 // construct an array of choice values
18776 $values[] = $checkbox->getConfig()->getOption('value');
18777 }
18778 }
18779
18780 $choices = $values;
18781 }
18782}
18783
18784------------------------------------------------------------------------------------------------------------------------
18785 ./Extension/Core/DataAccessor/CallbackAccessor.php
18786------------------------------------------------------------------------------------------------------------------------
18787<?php
18788
18789/*
18790 * This file is part of the Symfony package.
18791 *
18792 * (c) Fabien Potencier <fabien@symfony.com>
18793 *
18794 * For the full copyright and license information, please view the LICENSE
18795 * file that was distributed with this source code.
18796 */
18797
18798namespace Symfony\Component\Form\Extension\Core\DataAccessor;
18799
18800use Symfony\Component\Form\DataAccessorInterface;
18801use Symfony\Component\Form\Exception\AccessException;
18802use Symfony\Component\Form\FormInterface;
18803
18804/**
18805 * Writes and reads values to/from an object or array using callback functions.
18806 *
18807 * @author Yonel Ceruto <yonelceruto@gmail.com>
18808 */
18809class CallbackAccessor implements DataAccessorInterface
18810{
18811 public function getValue(object|array $data, FormInterface $form): mixed
18812 {
18813 if (null === $getter = $form->getConfig()->getOption('getter')) {
18814 throw new AccessException('Unable to read from the given form data as no getter is defined.');
18815 }
18816
18817 return ($getter)($data, $form);
18818 }
18819
18820 public function setValue(object|array &$data, mixed $value, FormInterface $form): void
18821 {
18822 if (null === $setter = $form->getConfig()->getOption('setter')) {
18823 throw new AccessException('Unable to write the given value as no setter is defined.');
18824 }
18825
18826 ($setter)($data, $form->getData(), $form);
18827 }
18828
18829 public function isReadable(object|array $data, FormInterface $form): bool
18830 {
18831 return null !== $form->getConfig()->getOption('getter');
18832 }
18833
18834 public function isWritable(object|array $data, FormInterface $form): bool
18835 {
18836 return null !== $form->getConfig()->getOption('setter');
18837 }
18838}
18839
18840------------------------------------------------------------------------------------------------------------------------
18841 ./Extension/Core/DataAccessor/PropertyPathAccessor.php
18842------------------------------------------------------------------------------------------------------------------------
18843<?php
18844
18845/*
18846 * This file is part of the Symfony package.
18847 *
18848 * (c) Fabien Potencier <fabien@symfony.com>
18849 *
18850 * For the full copyright and license information, please view the LICENSE
18851 * file that was distributed with this source code.
18852 */
18853
18854namespace Symfony\Component\Form\Extension\Core\DataAccessor;
18855
18856use Symfony\Component\Form\DataAccessorInterface;
18857use Symfony\Component\Form\DataMapperInterface;
18858use Symfony\Component\Form\Exception\AccessException;
18859use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
18860use Symfony\Component\Form\FormInterface;
18861use Symfony\Component\PropertyAccess\Exception\AccessException as PropertyAccessException;
18862use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
18863use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
18864use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
18865use Symfony\Component\PropertyAccess\PropertyAccess;
18866use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
18867use Symfony\Component\PropertyAccess\PropertyPathInterface;
18868
18869/**
18870 * Writes and reads values to/from an object or array using property path.
18871 *
18872 * @author Yonel Ceruto <yonelceruto@gmail.com>
18873 * @author Bernhard Schussek <bschussek@gmail.com>
18874 */
18875class PropertyPathAccessor implements DataAccessorInterface
18876{
18877 private PropertyAccessorInterface $propertyAccessor;
18878
18879 public function __construct(?PropertyAccessorInterface $propertyAccessor = null)
18880 {
18881 $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
18882 }
18883
18884 public function getValue(object|array $data, FormInterface $form): mixed
18885 {
18886 if (null === $propertyPath = $form->getPropertyPath()) {
18887 throw new AccessException('Unable to read from the given form data as no property path is defined.');
18888 }
18889
18890 return $this->getPropertyValue($data, $propertyPath);
18891 }
18892
18893 public function setValue(object|array &$data, mixed $value, FormInterface $form): void
18894 {
18895 if (null === $propertyPath = $form->getPropertyPath()) {
18896 throw new AccessException('Unable to write the given value as no property path is defined.');
18897 }
18898
18899 $getValue = function () use ($data, $form, $propertyPath) {
18900 $dataMapper = $this->getDataMapper($form);
18901
18902 if ($dataMapper instanceof DataMapper && null !== $dataAccessor = $dataMapper->getDataAccessor()) {
18903 return $dataAccessor->getValue($data, $form);
18904 }
18905
18906 return $this->getPropertyValue($data, $propertyPath);
18907 };
18908
18909 // If the field is of type DateTimeInterface and the data is the same skip the update to
18910 // keep the original object hash
18911 if ($value instanceof \DateTimeInterface && $value == $getValue()) {
18912 return;
18913 }
18914
18915 // If the data is identical to the value in $data, we are
18916 // dealing with a reference
18917 if (!\is_object($data) || !$form->getConfig()->getByReference() || $value !== $getValue()) {
18918 try {
18919 $this->propertyAccessor->setValue($data, $propertyPath, $value);
18920 } catch (NoSuchPropertyException $e) {
18921 throw new NoSuchPropertyException($e->getMessage().' Make the property public, add a setter, or set the "mapped" field option in the form type to be false.', 0, $e);
18922 }
18923 }
18924 }
18925
18926 public function isReadable(object|array $data, FormInterface $form): bool
18927 {
18928 return null !== $form->getPropertyPath();
18929 }
18930
18931 public function isWritable(object|array $data, FormInterface $form): bool
18932 {
18933 return null !== $form->getPropertyPath();
18934 }
18935
18936 private function getPropertyValue(object|array $data, PropertyPathInterface $propertyPath): mixed
18937 {
18938 try {
18939 return $this->propertyAccessor->getValue($data, $propertyPath);
18940 } catch (PropertyAccessException $e) {
18941 if (\is_array($data) && $e instanceof NoSuchIndexException) {
18942 return null;
18943 }
18944
18945 if (!$e instanceof UninitializedPropertyException
18946 // For versions without UninitializedPropertyException check the exception message
18947 && (class_exists(UninitializedPropertyException::class) || !str_contains($e->getMessage(), 'You should initialize it'))
18948 ) {
18949 throw $e;
18950 }
18951
18952 return null;
18953 }
18954 }
18955
18956 private function getDataMapper(FormInterface $form): ?DataMapperInterface
18957 {
18958 do {
18959 $dataMapper = $form->getConfig()->getDataMapper();
18960 } while (null === $dataMapper && null !== $form = $form->getParent());
18961
18962 return $dataMapper;
18963 }
18964}
18965
18966------------------------------------------------------------------------------------------------------------------------
18967 ./Extension/Core/DataAccessor/ChainAccessor.php
18968------------------------------------------------------------------------------------------------------------------------
18969<?php
18970
18971/*
18972 * This file is part of the Symfony package.
18973 *
18974 * (c) Fabien Potencier <fabien@symfony.com>
18975 *
18976 * For the full copyright and license information, please view the LICENSE
18977 * file that was distributed with this source code.
18978 */
18979
18980namespace Symfony\Component\Form\Extension\Core\DataAccessor;
18981
18982use Symfony\Component\Form\DataAccessorInterface;
18983use Symfony\Component\Form\Exception\AccessException;
18984use Symfony\Component\Form\FormInterface;
18985
18986/**
18987 * @author Yonel Ceruto <yonelceruto@gmail.com>
18988 */
18989class ChainAccessor implements DataAccessorInterface
18990{
18991 /**
18992 * @param DataAccessorInterface[]|iterable $accessors
18993 */
18994 public function __construct(
18995 private iterable $accessors,
18996 ) {
18997 }
18998
18999 public function getValue(object|array $data, FormInterface $form): mixed
19000 {
19001 foreach ($this->accessors as $accessor) {
19002 if ($accessor->isReadable($data, $form)) {
19003 return $accessor->getValue($data, $form);
19004 }
19005 }
19006
19007 throw new AccessException('Unable to read from the given form data as no accessor in the chain is able to read the data.');
19008 }
19009
19010 public function setValue(object|array &$data, mixed $value, FormInterface $form): void
19011 {
19012 foreach ($this->accessors as $accessor) {
19013 if ($accessor->isWritable($data, $form)) {
19014 $accessor->setValue($data, $value, $form);
19015
19016 return;
19017 }
19018 }
19019
19020 throw new AccessException('Unable to write the given value as no accessor in the chain is able to set the data.');
19021 }
19022
19023 public function isReadable(object|array $data, FormInterface $form): bool
19024 {
19025 foreach ($this->accessors as $accessor) {
19026 if ($accessor->isReadable($data, $form)) {
19027 return true;
19028 }
19029 }
19030
19031 return false;
19032 }
19033
19034 public function isWritable(object|array $data, FormInterface $form): bool
19035 {
19036 foreach ($this->accessors as $accessor) {
19037 if ($accessor->isWritable($data, $form)) {
19038 return true;
19039 }
19040 }
19041
19042 return false;
19043 }
19044}
19045
19046------------------------------------------------------------------------------------------------------------------------
19047 ./Extension/Core/DataTransformer/WeekToArrayTransformer.php
19048------------------------------------------------------------------------------------------------------------------------
19049<?php
19050
19051/*
19052 * This file is part of the Symfony package.
19053 *
19054 * (c) Fabien Potencier <fabien@symfony.com>
19055 *
19056 * For the full copyright and license information, please view the LICENSE
19057 * file that was distributed with this source code.
19058 */
19059
19060namespace Symfony\Component\Form\Extension\Core\DataTransformer;
19061
19062use Symfony\Component\Form\DataTransformerInterface;
19063use Symfony\Component\Form\Exception\TransformationFailedException;
19064
19065/**
19066 * Transforms between an ISO 8601 week date string and an array.
19067 *
19068 * @author Damien Fayet <damienf1521@gmail.com>
19069 *
19070 * @implements DataTransformerInterface<string, array{year: int|null, week: int|null}>
19071 */
19072class WeekToArrayTransformer implements DataTransformerInterface
19073{
19074 /**
19075 * Transforms a string containing an ISO 8601 week date into an array.
19076 *
19077 * @param string|null $value A week date string
19078 *
19079 * @return array{year: int|null, week: int|null}
19080 *
19081 * @throws TransformationFailedException If the given value is not a string,
19082 * or if the given value does not follow the right format
19083 */
19084 public function transform(mixed $value): array
19085 {
19086 if (null === $value) {
19087 return ['year' => null, 'week' => null];
19088 }
19089
19090 if (!\is_string($value)) {
19091 throw new TransformationFailedException(\sprintf('Value is expected to be a string but was "%s".', get_debug_type($value)));
19092 }
19093
19094 if (0 === preg_match('/^(?P<year>\d{4})-W(?P<week>\d{2})$/', $value, $matches)) {
19095 throw new TransformationFailedException('Given data does not follow the date format "Y-\WW".');
19096 }
19097
19098 return [
19099 'year' => (int) $matches['year'],
19100 'week' => (int) $matches['week'],
19101 ];
19102 }
19103
19104 /**
19105 * Transforms an array into a week date string.
19106 *
19107 * @param array{year: int|null, week: int|null} $value
19108 *
19109 * @return string|null A week date string following the format Y-\WW
19110 *
19111 * @throws TransformationFailedException If the given value cannot be merged in a valid week date string,
19112 * or if the obtained week date does not exists
19113 */
19114 public function reverseTransform(mixed $value): ?string
19115 {
19116 if (null === $value || [] === $value) {
19117 return null;
19118 }
19119
19120 if (!\is_array($value)) {
19121 throw new TransformationFailedException(\sprintf('Value is expected to be an array, but was "%s".', get_debug_type($value)));
19122 }
19123
19124 if (!\array_key_exists('year', $value)) {
19125 throw new TransformationFailedException('Key "year" is missing.');
19126 }
19127
19128 if (!\array_key_exists('week', $value)) {
19129 throw new TransformationFailedException('Key "week" is missing.');
19130 }
19131
19132 if ($additionalKeys = array_diff(array_keys($value), ['year', 'week'])) {
19133 throw new TransformationFailedException(\sprintf('Expected only keys "year" and "week" to be present, but also got ["%s"].', implode('", "', $additionalKeys)));
19134 }
19135
19136 if (null === $value['year'] && null === $value['week']) {
19137 return null;
19138 }
19139
19140 if (!\is_int($value['year'])) {
19141 throw new TransformationFailedException(\sprintf('Year is expected to be an integer, but was "%s".', get_debug_type($value['year'])));
19142 }
19143
19144 if (!\is_int($value['week'])) {
19145 throw new TransformationFailedException(\sprintf('Week is expected to be an integer, but was "%s".', get_debug_type($value['week'])));
19146 }
19147
19148 // The 28th December is always in the last week of the year
19149 if (date('W', strtotime('28th December '.$value['year'])) < $value['week']) {
19150 throw new TransformationFailedException(\sprintf('Week "%d" does not exist for year "%d".', $value['week'], $value['year']));
19151 }
19152
19153 return \sprintf('%d-W%02d', $value['year'], $value['week']);
19154 }
19155}
19156
19157------------------------------------------------------------------------------------------------------------------------
19158 ./Extension/Core/DataTransformer/DataTransformerChain.php
19159------------------------------------------------------------------------------------------------------------------------
19160<?php
19161
19162/*
19163 * This file is part of the Symfony package.
19164 *
19165 * (c) Fabien Potencier <fabien@symfony.com>
19166 *
19167 * For the full copyright and license information, please view the LICENSE
19168 * file that was distributed with this source code.
19169 */
19170
19171namespace Symfony\Component\Form\Extension\Core\DataTransformer;
19172
19173use Symfony\Component\Form\DataTransformerInterface;
19174use Symfony\Component\Form\Exception\TransformationFailedException;
19175
19176/**
19177 * Passes a value through multiple value transformers.
19178 *
19179 * @author Bernhard Schussek <bschussek@gmail.com>
19180 */
19181class DataTransformerChain implements DataTransformerInterface
19182{
19183 /**
19184 * Uses the given value transformers to transform values.
19185 *
19186 * @param DataTransformerInterface[] $transformers
19187 */
19188 public function __construct(
19189 protected array $transformers,
19190 ) {
19191 }
19192
19193 /**
19194 * Passes the value through the transform() method of all nested transformers.
19195 *
19196 * The transformers receive the value in the same order as they were passed
19197 * to the constructor. Each transformer receives the result of the previous
19198 * transformer as input. The output of the last transformer is returned
19199 * by this method.
19200 *
19201 * @param mixed $value The original value
19202 *
19203 * @throws TransformationFailedException
19204 */
19205 public function transform(mixed $value): mixed
19206 {
19207 foreach ($this->transformers as $transformer) {
19208 $value = $transformer->transform($value);
19209 }
19210
19211 return $value;
19212 }
19213
19214 /**
19215 * Passes the value through the reverseTransform() method of all nested
19216 * transformers.
19217 *
19218 * The transformers receive the value in the reverse order as they were passed
19219 * to the constructor. Each transformer receives the result of the previous
19220 * transformer as input. The output of the last transformer is returned
19221 * by this method.
19222 *
19223 * @param mixed $value The transformed value
19224 *
19225 * @throws TransformationFailedException
19226 */
19227 public function reverseTransform(mixed $value): mixed
19228 {
19229 for ($i = \count($this->transformers) - 1; $i >= 0; --$i) {
19230 $value = $this->transformers[$i]->reverseTransform($value);
19231 }
19232
19233 return $value;
19234 }
19235
19236 /**
19237 * @return DataTransformerInterface[]
19238 */
19239 public function getTransformers(): array
19240 {
19241 return $this->transformers;
19242 }
19243}
19244
19245------------------------------------------------------------------------------------------------------------------------
19246 ./Extension/Core/DataTransformer/BaseDateTimeTransformer.php
19247------------------------------------------------------------------------------------------------------------------------
19248<?php
19249
19250/*
19251 * This file is part of the Symfony package.
19252 *
19253 * (c) Fabien Potencier <fabien@symfony.com>
19254 *
19255 * For the full copyright and license information, please view the LICENSE
19256 * file that was distributed with this source code.
19257 */
19258
19259namespace Symfony\Component\Form\Extension\Core\DataTransformer;
19260
19261use Symfony\Component\Form\DataTransformerInterface;
19262use Symfony\Component\Form\Exception\InvalidArgumentException;
19263
19264/**
19265 * @template TTransformedValue
19266 *
19267 * @implements DataTransformerInterface<\DateTimeInterface, TTransformedValue>
19268 */
19269abstract class BaseDateTimeTransformer implements DataTransformerInterface
19270{
19271 protected static array $formats = [
19272 \IntlDateFormatter::NONE,
19273 \IntlDateFormatter::FULL,
19274 \IntlDateFormatter::LONG,
19275 \IntlDateFormatter::MEDIUM,
19276 \IntlDateFormatter::SHORT,
19277 ];
19278
19279 protected string $inputTimezone;
19280 protected string $outputTimezone;
19281
19282 /**
19283 * @param string|null $inputTimezone The name of the input timezone
19284 * @param string|null $outputTimezone The name of the output timezone
19285 *
19286 * @throws InvalidArgumentException if a timezone is not valid
19287 */
19288 public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null)
19289 {
19290 $this->inputTimezone = $inputTimezone ?: date_default_timezone_get();
19291 $this->outputTimezone = $outputTimezone ?: date_default_timezone_get();
19292
19293 // Check if input and output timezones are valid
19294 try {
19295 new \DateTimeZone($this->inputTimezone);
19296 } catch (\Exception $e) {
19297 throw new InvalidArgumentException(\sprintf('Input timezone is invalid: "%s".', $this->inputTimezone), $e->getCode(), $e);
19298 }
19299
19300 try {
19301 new \DateTimeZone($this->outputTimezone);
19302 } catch (\Exception $e) {
19303 throw new InvalidArgumentException(\sprintf('Output timezone is invalid: "%s".', $this->outputTimezone), $e->getCode(), $e);
19304 }
19305 }
19306}
19307
19308------------------------------------------------------------------------------------------------------------------------
19309 ./Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php
19310------------------------------------------------------------------------------------------------------------------------
19311<?php
19312
19313/*
19314 * This file is part of the Symfony package.
19315 *
19316 * (c) Fabien Potencier <fabien@symfony.com>
19317 *
19318 * For the full copyright and license information, please view the LICENSE
19319 * file that was distributed with this source code.
19320 */
19321
19322namespace Symfony\Component\Form\Extension\Core\DataTransformer;
19323
19324use Symfony\Component\Form\Exception\TransformationFailedException;
19325
19326/**
19327 * @author Bernhard Schussek <bschussek@gmail.com>
19328 *
19329 * @extends BaseDateTimeTransformer<string>
19330 */
19331class DateTimeToRfc3339Transformer extends BaseDateTimeTransformer
19332{
19333 /**
19334 * Transforms a normalized date into a localized date.
19335 *
19336 * @param \DateTimeInterface $dateTime A DateTimeInterface object
19337 *
19338 * @throws TransformationFailedException If the given value is not a \DateTimeInterface
19339 */
19340 public function transform(mixed $dateTime): string
19341 {
19342 if (null === $dateTime) {
19343 return '';
19344 }
19345
19346 if (!$dateTime instanceof \DateTimeInterface) {
19347 throw new TransformationFailedException('Expected a \DateTimeInterface.');
19348 }
19349
19350 if ($this->inputTimezone !== $this->outputTimezone) {
19351 $dateTime = \DateTimeImmutable::createFromInterface($dateTime);
19352 $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
19353 }
19354
19355 return preg_replace('/\+00:00$/', 'Z', $dateTime->format('c'));
19356 }
19357
19358 /**
19359 * Transforms a formatted string following RFC 3339 into a normalized date.
19360 *
19361 * @param string $rfc3339 Formatted string
19362 *
19363 * @throws TransformationFailedException If the given value is not a string,
19364 * if the value could not be transformed
19365 */
19366 public function reverseTransform(mixed $rfc3339): ?\DateTime
19367 {
19368 if (!\is_string($rfc3339)) {
19369 throw new TransformationFailedException('Expected a string.');
19370 }
19371
19372 if ('' === $rfc3339) {
19373 return null;
19374 }
19375
19376 if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|(?:(?:\+|-)\d{2}:\d{2}))$/', $rfc3339, $matches)) {
19377 throw new TransformationFailedException(\sprintf('The date "%s" is not a valid date.', $rfc3339));
19378 }
19379
19380 try {
19381 $dateTime = new \DateTime($rfc3339);
19382 } catch (\Exception $e) {
19383 throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
19384 }
19385
19386 if ($this->inputTimezone !== $dateTime->getTimezone()->getName()) {
19387 $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
19388 }
19389
19390 if (!checkdate($matches[2], $matches[3], $matches[1])) {
19391 throw new TransformationFailedException(\sprintf('The date "%s-%s-%s" is not a valid date.', $matches[1], $matches[2], $matches[3]));
19392 }
19393
19394 return $dateTime;
19395 }
19396}
19397
19398------------------------------------------------------------------------------------------------------------------------
19399 ./Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php
19400------------------------------------------------------------------------------------------------------------------------
19401<?php
19402
19403/*
19404 * This file is part of the Symfony package.
19405 *
19406 * (c) Fabien Potencier <fabien@symfony.com>
19407 *
19408 * For the full copyright and license information, please view the LICENSE
19409 * file that was distributed with this source code.
19410 */
19411
19412namespace Symfony\Component\Form\Extension\Core\DataTransformer;
19413
19414use Symfony\Component\Form\DataTransformerInterface;
19415use Symfony\Component\Form\Exception\TransformationFailedException;
19416use Symfony\Component\Form\Exception\UnexpectedTypeException;
19417
19418/**
19419 * Transforms between a normalized date interval and an interval string/array.
19420 *
19421 * @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
19422 *
19423 * @implements DataTransformerInterface<\DateInterval, array>
19424 */
19425class DateIntervalToArrayTransformer implements DataTransformerInterface
19426{
19427 public const YEARS = 'years';
19428 public const MONTHS = 'months';
19429 public const DAYS = 'days';
19430 public const HOURS = 'hours';
19431 public const MINUTES = 'minutes';
19432 public const SECONDS = 'seconds';
19433 public const INVERT = 'invert';
19434
19435 private const AVAILABLE_FIELDS = [
19436 self::YEARS => 'y',
19437 self::MONTHS => 'm',
19438 self::DAYS => 'd',
19439 self::HOURS => 'h',
19440 self::MINUTES => 'i',
19441 self::SECONDS => 's',
19442 self::INVERT => 'r',
19443 ];
19444 private array $fields;
19445
19446 /**
19447 * @param string[]|null $fields The date fields
19448 * @param bool $pad Whether to use padding
19449 */
19450 public function __construct(
19451 ?array $fields = null,
19452 private bool $pad = false,
19453 ) {
19454 $this->fields = $fields ?? ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'invert'];
19455 }
19456
19457 /**
19458 * Transforms a normalized date interval into an interval array.
19459 *
19460 * @param \DateInterval $dateInterval Normalized date interval
19461 *
19462 * @throws UnexpectedTypeException if the given value is not a \DateInterval instance
19463 */
19464 public function transform(mixed $dateInterval): array
19465 {
19466 if (null === $dateInterval) {
19467 return array_intersect_key(
19468 [
19469 'years' => '',
19470 'months' => '',
19471 'weeks' => '',
19472 'days' => '',
19473 'hours' => '',
19474 'minutes' => '',
19475 'seconds' => '',
19476 'invert' => false,
19477 ],
19478 array_flip($this->fields)
19479 );
19480 }
19481 if (!$dateInterval instanceof \DateInterval) {
19482 throw new UnexpectedTypeException($dateInterval, \DateInterval::class);
19483 }
19484 $result = [];
19485 foreach (self::AVAILABLE_FIELDS as $field => $char) {
19486 $result[$field] = $dateInterval->format('%'.($this->pad ? strtoupper($char) : $char));
19487 }
19488 if (\in_array('weeks', $this->fields, true)) {
19489 $result['weeks'] = '0';
19490 if (isset($result['days']) && (int) $result['days'] >= 7) {
19491 $result['weeks'] = (string) floor($result['days'] / 7);
19492 $result['days'] = (string) ($result['days'] % 7);
19493 }
19494 }
19495 $result['invert'] = '-' === $result['invert'];
19496
19497 return array_intersect_key($result, array_flip($this->fields));
19498 }
19499
19500 /**
19501 * Transforms an interval array into a normalized date interval.
19502 *
19503 * @param array $value Interval array
19504 *
19505 * @throws UnexpectedTypeException if the given value is not an array
19506 * @throws TransformationFailedException if the value could not be transformed
19507 */
19508 public function reverseTransform(mixed $value): ?\DateInterval
19509 {
19510 if (null === $value) {
19511 return null;
19512 }
19513 if (!\is_array($value)) {
19514 throw new UnexpectedTypeException($value, 'array');
19515 }
19516 if ('' === implode('', $value)) {
19517 return null;
19518 }
19519 $emptyFields = [];
19520 foreach ($this->fields as $field) {
19521 if (!isset($value[$field])) {
19522 $emptyFields[] = $field;
19523 }
19524 }
19525 if (\count($emptyFields) > 0) {
19526 throw new TransformationFailedException(\sprintf('The fields "%s" should not be empty.', implode('", "', $emptyFields)));
19527 }
19528 if (isset($value['invert']) && !\is_bool($value['invert'])) {
19529 throw new TransformationFailedException('The value of "invert" must be boolean.');
19530 }
19531 foreach (self::AVAILABLE_FIELDS as $field => $char) {
19532 if ('invert' !== $field && isset($value[$field]) && !ctype_digit((string) $value[$field])) {
19533 throw new TransformationFailedException(\sprintf('This amount of "%s" is invalid.', $field));
19534 }
19535 }
19536 try {
19537 if (!empty($value['weeks'])) {
19538 $interval = \sprintf(
19539 'P%sY%sM%sWT%sH%sM%sS',
19540 empty($value['years']) ? '0' : $value['years'],
19541 empty($value['months']) ? '0' : $value['months'],
19542 $value['weeks'],
19543 empty($value['hours']) ? '0' : $value['hours'],
19544 empty($value['minutes']) ? '0' : $value['minutes'],
19545 empty($value['seconds']) ? '0' : $value['seconds']
19546 );
19547 } else {
19548 $interval = \sprintf(
19549 'P%sY%sM%sDT%sH%sM%sS',
19550 empty($value['years']) ? '0' : $value['years'],
19551 empty($value['months']) ? '0' : $value['months'],
19552 empty($value['days']) ? '0' : $value['days'],
19553 empty($value['hours']) ? '0' : $value['hours'],
19554 empty($value['minutes']) ? '0' : $value['minutes'],
19555 empty($value['seconds']) ? '0' : $value['seconds']
19556 );
19557 }
19558 $dateInterval = new \DateInterval($interval);
19559 if (isset($value['invert'])) {
19560 $dateInterval->invert = $value['invert'] ? 1 : 0;
19561 }
19562 } catch (\Exception $e) {
19563 throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
19564 }
19565
19566 return $dateInterval;
19567 }
19568}
19569
19570------------------------------------------------------------------------------------------------------------------------
19571 ./Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php
19572------------------------------------------------------------------------------------------------------------------------
19573<?php
19574
19575/*
19576 * This file is part of the Symfony package.
19577 *
19578 * (c) Fabien Potencier <fabien@symfony.com>
19579 *
19580 * For the full copyright and license information, please view the LICENSE
19581 * file that was distributed with this source code.
19582 */
19583
19584namespace Symfony\Component\Form\Extension\Core\DataTransformer;
19585
19586use Symfony\Component\Form\Exception\TransformationFailedException;
19587
19588/**
19589 * Transforms between a timestamp and a DateTime object.
19590 *
19591 * @author Bernhard Schussek <bschussek@gmail.com>
19592 * @author Florian Eckerstorfer <florian@eckerstorfer.org>
19593 *
19594 * @extends BaseDateTimeTransformer<int|numeric-string>
19595 */
19596class DateTimeToTimestampTransformer extends BaseDateTimeTransformer
19597{
19598 /**
19599 * Transforms a DateTime object into a timestamp in the configured timezone.
19600 *
19601 * @param \DateTimeInterface $dateTime A DateTimeInterface object
19602 *
19603 * @throws TransformationFailedException If the given value is not a \DateTimeInterface
19604 */
19605 public function transform(mixed $dateTime): ?int
19606 {
19607 if (null === $dateTime) {
19608 return null;
19609 }
19610
19611 if (!$dateTime instanceof \DateTimeInterface) {
19612 throw new TransformationFailedException('Expected a \DateTimeInterface.');
19613 }
19614
19615 return $dateTime->getTimestamp();
19616 }
19617
19618 /**
19619 * Transforms a timestamp in the configured timezone into a DateTime object.
19620 *
19621 * @param string $value A timestamp
19622 *
19623 * @throws TransformationFailedException If the given value is not a timestamp
19624 * or if the given timestamp is invalid
19625 */
19626 public function reverseTransform(mixed $value): ?\DateTime
19627 {
19628 if (null === $value) {
19629 return null;
19630 }
19631
19632 if (!is_numeric($value)) {
19633 throw new TransformationFailedException('Expected a numeric.');
19634 }
19635
19636 try {
19637 $dateTime = new \DateTime();
19638 $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
19639 $dateTime->setTimestamp($value);
19640
19641 if ($this->inputTimezone !== $this->outputTimezone) {
19642 $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
19643 }
19644 } catch (\Exception $e) {
19645 throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
19646 }
19647
19648 return $dateTime;
19649 }
19650}
19651
19652------------------------------------------------------------------------------------------------------------------------
19653 ./Extension/Core/DataTransformer/DateTimeToArrayTransformer.php
19654------------------------------------------------------------------------------------------------------------------------
19655<?php
19656
19657/*
19658 * This file is part of the Symfony package.
19659 *
19660 * (c) Fabien Potencier <fabien@symfony.com>
19661 *
19662 * For the full copyright and license information, please view the LICENSE
19663 * file that was distributed with this source code.
19664 */
19665
19666namespace Symfony\Component\Form\Extension\Core\DataTransformer;
19667
19668use Symfony\Component\Form\Exception\TransformationFailedException;
19669
19670/**
19671 * Transforms between a normalized time and a localized time string/array.
19672 *
19673 * @author Bernhard Schussek <bschussek@gmail.com>
19674 * @author Florian Eckerstorfer <florian@eckerstorfer.org>
19675 *
19676 * @extends BaseDateTimeTransformer<array>
19677 */
19678class DateTimeToArrayTransformer extends BaseDateTimeTransformer
19679{
19680 private array $fields;
19681 private \DateTimeInterface $referenceDate;
19682
19683 /**
19684 * @param string|null $inputTimezone The input timezone
19685 * @param string|null $outputTimezone The output timezone
19686 * @param string[]|null $fields The date fields
19687 * @param bool $pad Whether to use padding
19688 */
19689 public function __construct(
19690 ?string $inputTimezone = null,
19691 ?string $outputTimezone = null,
19692 ?array $fields = null,
19693 private bool $pad = false,
19694 ?\DateTimeInterface $referenceDate = null,
19695 ) {
19696 parent::__construct($inputTimezone, $outputTimezone);
19697
19698 $this->fields = $fields ?? ['year', 'month', 'day', 'hour', 'minute', 'second'];
19699 $this->referenceDate = $referenceDate ?? new \DateTimeImmutable('1970-01-01 00:00:00');
19700 }
19701
19702 /**
19703 * Transforms a normalized date into a localized date.
19704 *
19705 * @param \DateTimeInterface $dateTime A DateTimeInterface object
19706 *
19707 * @throws TransformationFailedException If the given value is not a \DateTimeInterface
19708 */
19709 public function transform(mixed $dateTime): array
19710 {
19711 if (null === $dateTime) {
19712 return array_intersect_key([
19713 'year' => '',
19714 'month' => '',
19715 'day' => '',
19716 'hour' => '',
19717 'minute' => '',
19718 'second' => '',
19719 ], array_flip($this->fields));
19720 }
19721
19722 if (!$dateTime instanceof \DateTimeInterface) {
19723 throw new TransformationFailedException('Expected a \DateTimeInterface.');
19724 }
19725
19726 if ($this->inputTimezone !== $this->outputTimezone) {
19727 $dateTime = \DateTimeImmutable::createFromInterface($dateTime);
19728 $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
19729 }
19730
19731 $result = array_intersect_key([
19732 'year' => $dateTime->format('Y'),
19733 'month' => $dateTime->format('m'),
19734 'day' => $dateTime->format('d'),
19735 'hour' => $dateTime->format('H'),
19736 'minute' => $dateTime->format('i'),
19737 'second' => $dateTime->format('s'),
19738 ], array_flip($this->fields));
19739
19740 if (!$this->pad) {
19741 foreach ($result as &$entry) {
19742 // remove leading zeros
19743 $entry = (string) (int) $entry;
19744 }
19745 // unset reference to keep scope clear
19746 unset($entry);
19747 }
19748
19749 return $result;
19750 }
19751
19752 /**
19753 * Transforms a localized date into a normalized date.
19754 *
19755 * @param array $value Localized date
19756 *
19757 * @throws TransformationFailedException If the given value is not an array,
19758 * if the value could not be transformed
19759 */
19760 public function reverseTransform(mixed $value): ?\DateTime
19761 {
19762 if (null === $value) {
19763 return null;
19764 }
19765
19766 if (!\is_array($value)) {
19767 throw new TransformationFailedException('Expected an array.');
19768 }
19769
19770 if ('' === implode('', $value)) {
19771 return null;
19772 }
19773
19774 $emptyFields = [];
19775
19776 foreach ($this->fields as $field) {
19777 if (!isset($value[$field])) {
19778 $emptyFields[] = $field;
19779 }
19780 }
19781
19782 if (\count($emptyFields) > 0) {
19783 throw new TransformationFailedException(\sprintf('The fields "%s" should not be empty.', implode('", "', $emptyFields)));
19784 }
19785
19786 if (isset($value['month']) && !ctype_digit((string) $value['month'])) {
19787 throw new TransformationFailedException('This month is invalid.');
19788 }
19789
19790 if (isset($value['day']) && !ctype_digit((string) $value['day'])) {
19791 throw new TransformationFailedException('This day is invalid.');
19792 }
19793
19794 if (isset($value['year']) && !ctype_digit((string) $value['year'])) {
19795 throw new TransformationFailedException('This year is invalid.');
19796 }
19797
19798 if (!empty($value['month']) && !empty($value['day']) && !empty($value['year']) && false === checkdate($value['month'], $value['day'], $value['year'])) {
19799 throw new TransformationFailedException('This is an invalid date.');
19800 }
19801
19802 if (isset($value['hour']) && !ctype_digit((string) $value['hour'])) {
19803 throw new TransformationFailedException('This hour is invalid.');
19804 }
19805
19806 if (isset($value['minute']) && !ctype_digit((string) $value['minute'])) {
19807 throw new TransformationFailedException('This minute is invalid.');
19808 }
19809
19810 if (isset($value['second']) && !ctype_digit((string) $value['second'])) {
19811 throw new TransformationFailedException('This second is invalid.');
19812 }
19813
19814 try {
19815 $dateTime = new \DateTime(\sprintf(
19816 '%s-%s-%s %s:%s:%s',
19817 empty($value['year']) ? $this->referenceDate->format('Y') : $value['year'],
19818 empty($value['month']) ? $this->referenceDate->format('m') : $value['month'],
19819 empty($value['day']) ? $this->referenceDate->format('d') : $value['day'],
19820 $value['hour'] ?? $this->referenceDate->format('H'),
19821 $value['minute'] ?? $this->referenceDate->format('i'),
19822 $value['second'] ?? $this->referenceDate->format('s')
19823 ),
19824 new \DateTimeZone($this->outputTimezone)
19825 );
19826
19827 if ($this->inputTimezone !== $this->outputTimezone) {
19828 $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
19829 }
19830 } catch (\Exception $e) {
19831 throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
19832 }
19833
19834 return $dateTime;
19835 }
19836}
19837
19838------------------------------------------------------------------------------------------------------------------------
19839 ./Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php
19840------------------------------------------------------------------------------------------------------------------------
19841<?php
19842
19843/*
19844 * This file is part of the Symfony package.
19845 *
19846 * (c) Fabien Potencier <fabien@symfony.com>
19847 *
19848 * For the full copyright and license information, please view the LICENSE
19849 * file that was distributed with this source code.
19850 */
19851
19852namespace Symfony\Component\Form\Extension\Core\DataTransformer;
19853
19854use Symfony\Component\Form\DataTransformerInterface;
19855use Symfony\Component\Form\Exception\TransformationFailedException;
19856
19857/**
19858 * @author Bernhard Schussek <bschussek@gmail.com>
19859 *
19860 * @implements DataTransformerInterface<mixed, array>
19861 */
19862class ValueToDuplicatesTransformer implements DataTransformerInterface
19863{
19864 public function __construct(
19865 private array $keys,
19866 ) {
19867 }
19868
19869 /**
19870 * Duplicates the given value through the array.
19871 */
19872 public function transform(mixed $value): array
19873 {
19874 $result = [];
19875
19876 foreach ($this->keys as $key) {
19877 $result[$key] = $value;
19878 }
19879
19880 return $result;
19881 }
19882
19883 /**
19884 * Extracts the duplicated value from an array.
19885 *
19886 * @throws TransformationFailedException if the given value is not an array or
19887 * if the given array cannot be transformed
19888 */
19889 public function reverseTransform(mixed $array): mixed
19890 {
19891 if (!\is_array($array)) {
19892 throw new TransformationFailedException('Expected an array.');
19893 }
19894
19895 $result = current($array);
19896 $emptyKeys = [];
19897
19898 foreach ($this->keys as $key) {
19899 if (isset($array[$key]) && false !== $array[$key] && [] !== $array[$key]) {
19900 if ($array[$key] !== $result) {
19901 throw new TransformationFailedException('All values in the array should be the same.');
19902 }
19903 } else {
19904 $emptyKeys[] = $key;
19905 }
19906 }
19907
19908 if (\count($emptyKeys) > 0) {
19909 if (\count($emptyKeys) == \count($this->keys)) {
19910 // All keys empty
19911 return null;
19912 }
19913
19914 throw new TransformationFailedException(\sprintf('The keys "%s" should not be empty.', implode('", "', $emptyKeys)));
19915 }
19916
19917 return $result;
19918 }
19919}
19920
19921------------------------------------------------------------------------------------------------------------------------
19922 ./Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php
19923------------------------------------------------------------------------------------------------------------------------
19924<?php
19925
19926/*
19927 * This file is part of the Symfony package.
19928 *
19929 * (c) Fabien Potencier <fabien@symfony.com>
19930 *
19931 * For the full copyright and license information, please view the LICENSE
19932 * file that was distributed with this source code.
19933 */
19934
19935namespace Symfony\Component\Form\Extension\Core\DataTransformer;
19936
19937use Symfony\Component\Form\Exception\TransformationFailedException;
19938
19939/**
19940 * Transforms between an integer and a localized number with grouping
19941 * (each thousand) and comma separators.
19942 *
19943 * @author Bernhard Schussek <bschussek@gmail.com>
19944 */
19945class IntegerToLocalizedStringTransformer extends NumberToLocalizedStringTransformer
19946{
19947 /**
19948 * Constructs a transformer.
19949 *
19950 * @param bool $grouping Whether thousands should be grouped
19951 * @param int|null $roundingMode One of the ROUND_ constants in this class
19952 * @param string|null $locale locale used for transforming
19953 */
19954 public function __construct(?bool $grouping = false, ?int $roundingMode = \NumberFormatter::ROUND_DOWN, ?string $locale = null)
19955 {
19956 parent::__construct(0, $grouping, $roundingMode, $locale);
19957 }
19958
19959 public function reverseTransform(mixed $value): int|float|null
19960 {
19961 $decimalSeparator = $this->getNumberFormatter()->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
19962
19963 if (\is_string($value) && str_contains($value, $decimalSeparator)) {
19964 throw new TransformationFailedException(\sprintf('The value "%s" is not a valid integer.', $value));
19965 }
19966
19967 $result = parent::reverseTransform($value);
19968
19969 return null !== $result ? (int) $result : null;
19970 }
19971
19972 /**
19973 * @internal
19974 */
19975 protected function castParsedValue(int|float $value): int|float
19976 {
19977 return $value;
19978 }
19979}
19980
19981------------------------------------------------------------------------------------------------------------------------
19982 ./Extension/Core/DataTransformer/BooleanToStringTransformer.php
19983------------------------------------------------------------------------------------------------------------------------
19984<?php
19985
19986/*
19987 * This file is part of the Symfony package.
19988 *
19989 * (c) Fabien Potencier <fabien@symfony.com>
19990 *
19991 * For the full copyright and license information, please view the LICENSE
19992 * file that was distributed with this source code.
19993 */
19994
19995namespace Symfony\Component\Form\Extension\Core\DataTransformer;
19996
19997use Symfony\Component\Form\DataTransformerInterface;
19998use Symfony\Component\Form\Exception\InvalidArgumentException;
19999use Symfony\Component\Form\Exception\TransformationFailedException;
20000
20001/**
20002 * Transforms between a Boolean and a string.
20003 *
20004 * @author Bernhard Schussek <bschussek@gmail.com>
20005 * @author Florian Eckerstorfer <florian@eckerstorfer.org>
20006 *
20007 * @implements DataTransformerInterface<bool, string>
20008 */
20009class BooleanToStringTransformer implements DataTransformerInterface
20010{
20011 /**
20012 * @param string $trueValue The value emitted upon transform if the input is true
20013 */
20014 public function __construct(
20015 private string $trueValue,
20016 private array $falseValues = [null],
20017 ) {
20018 if (\in_array($this->trueValue, $this->falseValues, true)) {
20019 throw new InvalidArgumentException('The specified "true" value is contained in the false-values.');
20020 }
20021 }
20022
20023 /**
20024 * Transforms a Boolean into a string.
20025 *
20026 * @param bool $value Boolean value
20027 *
20028 * @throws TransformationFailedException if the given value is not a Boolean
20029 */
20030 public function transform(mixed $value): ?string
20031 {
20032 if (null === $value) {
20033 return null;
20034 }
20035
20036 if (!\is_bool($value)) {
20037 throw new TransformationFailedException('Expected a Boolean.');
20038 }
20039
20040 return $value ? $this->trueValue : null;
20041 }
20042
20043 /**
20044 * Transforms a string into a Boolean.
20045 *
20046 * @param string $value String value
20047 *
20048 * @throws TransformationFailedException if the given value is not a string
20049 */
20050 public function reverseTransform(mixed $value): bool
20051 {
20052 if (\in_array($value, $this->falseValues, true)) {
20053 return false;
20054 }
20055
20056 if (!\is_string($value)) {
20057 throw new TransformationFailedException('Expected a string.');
20058 }
20059
20060 return true;
20061 }
20062}
20063
20064------------------------------------------------------------------------------------------------------------------------
20065 ./Extension/Core/DataTransformer/ChoicesToValuesTransformer.php
20066------------------------------------------------------------------------------------------------------------------------
20067<?php
20068
20069/*
20070 * This file is part of the Symfony package.
20071 *
20072 * (c) Fabien Potencier <fabien@symfony.com>
20073 *
20074 * For the full copyright and license information, please view the LICENSE
20075 * file that was distributed with this source code.
20076 */
20077
20078namespace Symfony\Component\Form\Extension\Core\DataTransformer;
20079
20080use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
20081use Symfony\Component\Form\DataTransformerInterface;
20082use Symfony\Component\Form\Exception\TransformationFailedException;
20083
20084/**
20085 * @author Bernhard Schussek <bschussek@gmail.com>
20086 *
20087 * @implements DataTransformerInterface<array, array>
20088 */
20089class ChoicesToValuesTransformer implements DataTransformerInterface
20090{
20091 public function __construct(
20092 private ChoiceListInterface $choiceList,
20093 ) {
20094 }
20095
20096 /**
20097 * @throws TransformationFailedException if the given value is not an array
20098 */
20099 public function transform(mixed $array): array
20100 {
20101 if (null === $array) {
20102 return [];
20103 }
20104
20105 if (!\is_array($array)) {
20106 throw new TransformationFailedException('Expected an array.');
20107 }
20108
20109 return $this->choiceList->getValuesForChoices($array);
20110 }
20111
20112 /**
20113 * @throws TransformationFailedException if the given value is not an array
20114 * or if no matching choice could be
20115 * found for some given value
20116 */
20117 public function reverseTransform(mixed $array): array
20118 {
20119 if (null === $array) {
20120 return [];
20121 }
20122
20123 if (!\is_array($array)) {
20124 throw new TransformationFailedException('Expected an array.');
20125 }
20126
20127 $choices = $this->choiceList->getChoicesForValues($array);
20128
20129 if (\count($choices) !== \count($array)) {
20130 throw new TransformationFailedException('Could not find all matching choices for the given values.');
20131 }
20132
20133 return $choices;
20134 }
20135}
20136
20137------------------------------------------------------------------------------------------------------------------------
20138 ./Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php
20139------------------------------------------------------------------------------------------------------------------------
20140<?php
20141
20142/*
20143 * This file is part of the Symfony package.
20144 *
20145 * (c) Fabien Potencier <fabien@symfony.com>
20146 *
20147 * For the full copyright and license information, please view the LICENSE
20148 * file that was distributed with this source code.
20149 */
20150
20151namespace Symfony\Component\Form\Extension\Core\DataTransformer;
20152
20153use Symfony\Component\Form\Exception\TransformationFailedException;
20154
20155/**
20156 * @author Franz Wilding <franz.wilding@me.com>
20157 * @author Bernhard Schussek <bschussek@gmail.com>
20158 * @author Fred Cox <mcfedr@gmail.com>
20159 *
20160 * @extends BaseDateTimeTransformer<string>
20161 */
20162class DateTimeToHtml5LocalDateTimeTransformer extends BaseDateTimeTransformer
20163{
20164 public const HTML5_FORMAT = 'Y-m-d\\TH:i:s';
20165 public const HTML5_FORMAT_NO_SECONDS = 'Y-m-d\\TH:i';
20166
20167 public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, private bool $withSeconds = false)
20168 {
20169 parent::__construct($inputTimezone, $outputTimezone);
20170 }
20171
20172 /**
20173 * Transforms a \DateTime into a local date and time string.
20174 *
20175 * According to the HTML standard, the input string of a datetime-local
20176 * input is an RFC3339 date followed by 'T', followed by an RFC3339 time.
20177 * https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-local-date-and-time-string
20178 *
20179 * @param \DateTimeInterface $dateTime
20180 *
20181 * @throws TransformationFailedException If the given value is not an
20182 * instance of \DateTime or \DateTimeInterface
20183 */
20184 public function transform(mixed $dateTime): string
20185 {
20186 if (null === $dateTime) {
20187 return '';
20188 }
20189
20190 if (!$dateTime instanceof \DateTimeInterface) {
20191 throw new TransformationFailedException('Expected a \DateTimeInterface.');
20192 }
20193
20194 if ($this->inputTimezone !== $this->outputTimezone) {
20195 $dateTime = \DateTimeImmutable::createFromInterface($dateTime);
20196 $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
20197 }
20198
20199 return $dateTime->format($this->withSeconds ? self::HTML5_FORMAT : self::HTML5_FORMAT_NO_SECONDS);
20200 }
20201
20202 /**
20203 * Transforms a local date and time string into a \DateTime.
20204 *
20205 * When transforming back to DateTime the regex is slightly laxer, taking into
20206 * account rules for parsing a local date and time string
20207 * https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-local-date-and-time-string
20208 *
20209 * @param string $dateTimeLocal Formatted string
20210 *
20211 * @throws TransformationFailedException If the given value is not a string,
20212 * if the value could not be transformed
20213 */
20214 public function reverseTransform(mixed $dateTimeLocal): ?\DateTime
20215 {
20216 if (!\is_string($dateTimeLocal)) {
20217 throw new TransformationFailedException('Expected a string.');
20218 }
20219
20220 if ('' === $dateTimeLocal) {
20221 return null;
20222 }
20223
20224 // to maintain backwards compatibility we do not strictly validate the submitted date
20225 // see https://github.com/symfony/symfony/issues/28699
20226 if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})[T ]\d{2}:\d{2}(?::\d{2})?/', $dateTimeLocal, $matches)) {
20227 throw new TransformationFailedException(\sprintf('The date "%s" is not a valid date.', $dateTimeLocal));
20228 }
20229
20230 try {
20231 $dateTime = new \DateTime($dateTimeLocal, new \DateTimeZone($this->outputTimezone));
20232 } catch (\Exception $e) {
20233 throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
20234 }
20235
20236 if ($this->inputTimezone !== $dateTime->getTimezone()->getName()) {
20237 $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
20238 }
20239
20240 if (!checkdate($matches[2], $matches[3], $matches[1])) {
20241 throw new TransformationFailedException(\sprintf('The date "%s-%s-%s" is not a valid date.', $matches[1], $matches[2], $matches[3]));
20242 }
20243
20244 return $dateTime;
20245 }
20246}
20247
20248------------------------------------------------------------------------------------------------------------------------
20249 ./Extension/Core/DataTransformer/UlidToStringTransformer.php
20250------------------------------------------------------------------------------------------------------------------------
20251<?php
20252
20253/*
20254 * This file is part of the Symfony package.
20255 *
20256 * (c) Fabien Potencier <fabien@symfony.com>
20257 *
20258 * For the full copyright and license information, please view the LICENSE
20259 * file that was distributed with this source code.
20260 */
20261
20262namespace Symfony\Component\Form\Extension\Core\DataTransformer;
20263
20264use Symfony\Component\Form\DataTransformerInterface;
20265use Symfony\Component\Form\Exception\TransformationFailedException;
20266use Symfony\Component\Uid\Ulid;
20267
20268/**
20269 * Transforms between a ULID string and a Ulid object.
20270 *
20271 * @author Pavel Dyakonov <wapinet@mail.ru>
20272 *
20273 * @implements DataTransformerInterface<Ulid, string>
20274 */
20275class UlidToStringTransformer implements DataTransformerInterface
20276{
20277 /**
20278 * Transforms a Ulid object into a string.
20279 *
20280 * @param Ulid $value A Ulid object
20281 *
20282 * @throws TransformationFailedException If the given value is not a Ulid object
20283 */
20284 public function transform(mixed $value): ?string
20285 {
20286 if (null === $value) {
20287 return null;
20288 }
20289
20290 if (!$value instanceof Ulid) {
20291 throw new TransformationFailedException('Expected a Ulid.');
20292 }
20293
20294 return (string) $value;
20295 }
20296
20297 /**
20298 * Transforms a ULID string into a Ulid object.
20299 *
20300 * @param string $value A ULID string
20301 *
20302 * @throws TransformationFailedException If the given value is not a string,
20303 * or could not be transformed
20304 */
20305 public function reverseTransform(mixed $value): ?Ulid
20306 {
20307 if (null === $value || '' === $value) {
20308 return null;
20309 }
20310
20311 if (!\is_string($value)) {
20312 throw new TransformationFailedException('Expected a string.');
20313 }
20314
20315 try {
20316 $ulid = new Ulid($value);
20317 } catch (\InvalidArgumentException $e) {
20318 throw new TransformationFailedException(\sprintf('The value "%s" is not a valid ULID.', $value), $e->getCode(), $e);
20319 }
20320
20321 return $ulid;
20322 }
20323}
20324
20325------------------------------------------------------------------------------------------------------------------------
20326 ./Extension/Core/DataTransformer/ChoiceToValueTransformer.php
20327------------------------------------------------------------------------------------------------------------------------
20328<?php
20329
20330/*
20331 * This file is part of the Symfony package.
20332 *
20333 * (c) Fabien Potencier <fabien@symfony.com>
20334 *
20335 * For the full copyright and license information, please view the LICENSE
20336 * file that was distributed with this source code.
20337 */
20338
20339namespace Symfony\Component\Form\Extension\Core\DataTransformer;
20340
20341use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
20342use Symfony\Component\Form\DataTransformerInterface;
20343use Symfony\Component\Form\Exception\TransformationFailedException;
20344
20345/**
20346 * @author Bernhard Schussek <bschussek@gmail.com>
20347 *
20348 * @implements DataTransformerInterface<mixed, string>
20349 */
20350class ChoiceToValueTransformer implements DataTransformerInterface
20351{
20352 public function __construct(
20353 private ChoiceListInterface $choiceList,
20354 ) {
20355 }
20356
20357 public function transform(mixed $choice): mixed
20358 {
20359 return (string) current($this->choiceList->getValuesForChoices([$choice]));
20360 }
20361
20362 public function reverseTransform(mixed $value): mixed
20363 {
20364 if (null !== $value && !\is_string($value)) {
20365 throw new TransformationFailedException('Expected a string or null.');
20366 }
20367
20368 $choices = $this->choiceList->getChoicesForValues([(string) $value]);
20369
20370 if (1 !== \count($choices)) {
20371 if (null === $value || '' === $value) {
20372 return null;
20373 }
20374
20375 throw new TransformationFailedException(\sprintf('The choice "%s" does not exist or is not unique.', $value));
20376 }
20377
20378 return current($choices);
20379 }
20380}
20381
20382------------------------------------------------------------------------------------------------------------------------
20383 ./Extension/Core/DataTransformer/DateTimeToStringTransformer.php
20384------------------------------------------------------------------------------------------------------------------------
20385<?php
20386
20387/*
20388 * This file is part of the Symfony package.
20389 *
20390 * (c) Fabien Potencier <fabien@symfony.com>
20391 *
20392 * For the full copyright and license information, please view the LICENSE
20393 * file that was distributed with this source code.
20394 */
20395
20396namespace Symfony\Component\Form\Extension\Core\DataTransformer;
20397
20398use Symfony\Component\Form\Exception\TransformationFailedException;
20399
20400/**
20401 * Transforms between a date string and a DateTime object.
20402 *
20403 * @author Bernhard Schussek <bschussek@gmail.com>
20404 * @author Florian Eckerstorfer <florian@eckerstorfer.org>
20405 *
20406 * @extends BaseDateTimeTransformer<string>
20407 */
20408class DateTimeToStringTransformer extends BaseDateTimeTransformer
20409{
20410 /**
20411 * Format used for generating strings.
20412 */
20413 private string $generateFormat;
20414
20415 /**
20416 * Format used for parsing strings.
20417 *
20418 * Different than the {@link $generateFormat} because formats for parsing
20419 * support additional characters in PHP that are not supported for
20420 * generating strings.
20421 */
20422 private string $parseFormat;
20423
20424 /**
20425 * Transforms a \DateTime instance to a string.
20426 *
20427 * @see \DateTime::format() for supported formats
20428 *
20429 * @param string|null $inputTimezone The name of the input timezone
20430 * @param string|null $outputTimezone The name of the output timezone
20431 * @param string $format The date format
20432 * @param string|null $parseFormat The parse format when different from $format
20433 */
20434 public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, string $format = 'Y-m-d H:i:s', ?string $parseFormat = null)
20435 {
20436 parent::__construct($inputTimezone, $outputTimezone);
20437
20438 $this->generateFormat = $format;
20439 $this->parseFormat = $parseFormat ?? $format;
20440
20441 // See https://php.net/datetime.createfromformat
20442 // The character "|" in the format makes sure that the parts of a date
20443 // that are *not* specified in the format are reset to the corresponding
20444 // values from 1970-01-01 00:00:00 instead of the current time.
20445 // Without "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 12:32:47",
20446 // where the time corresponds to the current server time.
20447 // With "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 00:00:00",
20448 // which is at least deterministic and thus used here.
20449 if (!str_contains($this->parseFormat, '|')) {
20450 $this->parseFormat .= '|';
20451 }
20452 }
20453
20454 /**
20455 * Transforms a DateTime object into a date string with the configured format
20456 * and timezone.
20457 *
20458 * @param \DateTimeInterface $dateTime A DateTimeInterface object
20459 *
20460 * @throws TransformationFailedException If the given value is not a \DateTimeInterface
20461 */
20462 public function transform(mixed $dateTime): string
20463 {
20464 if (null === $dateTime) {
20465 return '';
20466 }
20467
20468 if (!$dateTime instanceof \DateTimeInterface) {
20469 throw new TransformationFailedException('Expected a \DateTimeInterface.');
20470 }
20471
20472 $dateTime = \DateTimeImmutable::createFromInterface($dateTime);
20473 $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
20474
20475 return $dateTime->format($this->generateFormat);
20476 }
20477
20478 /**
20479 * Transforms a date string in the configured timezone into a DateTime object.
20480 *
20481 * @param string $value A value as produced by PHP's date() function
20482 *
20483 * @throws TransformationFailedException If the given value is not a string,
20484 * or could not be transformed
20485 */
20486 public function reverseTransform(mixed $value): ?\DateTime
20487 {
20488 if (!$value) {
20489 return null;
20490 }
20491
20492 if (!\is_string($value)) {
20493 throw new TransformationFailedException('Expected a string.');
20494 }
20495
20496 if (str_contains($value, "\0")) {
20497 throw new TransformationFailedException('Null bytes not allowed');
20498 }
20499
20500 $outputTz = new \DateTimeZone($this->outputTimezone);
20501 $dateTime = \DateTime::createFromFormat($this->parseFormat, $value, $outputTz);
20502
20503 $lastErrors = \DateTime::getLastErrors() ?: ['error_count' => 0, 'warning_count' => 0];
20504
20505 if (0 < $lastErrors['warning_count'] || 0 < $lastErrors['error_count']) {
20506 throw new TransformationFailedException(implode(', ', array_merge(array_values($lastErrors['warnings']), array_values($lastErrors['errors']))));
20507 }
20508
20509 try {
20510 if ($this->inputTimezone !== $this->outputTimezone) {
20511 $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
20512 }
20513 } catch (\Exception $e) {
20514 throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
20515 }
20516
20517 return $dateTime;
20518 }
20519}
20520
20521------------------------------------------------------------------------------------------------------------------------
20522 ./Extension/Core/DataTransformer/StringToFloatTransformer.php
20523------------------------------------------------------------------------------------------------------------------------
20524<?php
20525
20526/*
20527 * This file is part of the Symfony package.
20528 *
20529 * (c) Fabien Potencier <fabien@symfony.com>
20530 *
20531 * For the full copyright and license information, please view the LICENSE
20532 * file that was distributed with this source code.
20533 */
20534
20535namespace Symfony\Component\Form\Extension\Core\DataTransformer;
20536
20537use Symfony\Component\Form\DataTransformerInterface;
20538use Symfony\Component\Form\Exception\TransformationFailedException;
20539
20540/**
20541 * @implements DataTransformerInterface<numeric-string, float>
20542 */
20543class StringToFloatTransformer implements DataTransformerInterface
20544{
20545 public function __construct(
20546 private ?int $scale = null,
20547 ) {
20548 }
20549
20550 public function transform(mixed $value): ?float
20551 {
20552 if (null === $value) {
20553 return null;
20554 }
20555
20556 if (!\is_string($value) || !is_numeric($value)) {
20557 throw new TransformationFailedException('Expected a numeric string.');
20558 }
20559
20560 return (float) $value;
20561 }
20562
20563 public function reverseTransform(mixed $value): ?string
20564 {
20565 if (null === $value) {
20566 return null;
20567 }
20568
20569 if (!\is_int($value) && !\is_float($value)) {
20570 throw new TransformationFailedException('Expected a numeric.');
20571 }
20572
20573 if ($this->scale > 0) {
20574 return number_format((float) $value, $this->scale, '.', '');
20575 }
20576
20577 return (string) $value;
20578 }
20579}
20580
20581------------------------------------------------------------------------------------------------------------------------
20582 ./Extension/Core/DataTransformer/ArrayToPartsTransformer.php
20583------------------------------------------------------------------------------------------------------------------------
20584<?php
20585
20586/*
20587 * This file is part of the Symfony package.
20588 *
20589 * (c) Fabien Potencier <fabien@symfony.com>
20590 *
20591 * For the full copyright and license information, please view the LICENSE
20592 * file that was distributed with this source code.
20593 */
20594
20595namespace Symfony\Component\Form\Extension\Core\DataTransformer;
20596
20597use Symfony\Component\Form\DataTransformerInterface;
20598use Symfony\Component\Form\Exception\TransformationFailedException;
20599
20600/**
20601 * @author Bernhard Schussek <bschussek@gmail.com>
20602 *
20603 * @implements DataTransformerInterface<array, array>
20604 */
20605class ArrayToPartsTransformer implements DataTransformerInterface
20606{
20607 public function __construct(
20608 private array $partMapping,
20609 ) {
20610 }
20611
20612 public function transform(mixed $array): mixed
20613 {
20614 if (!\is_array($array ??= [])) {
20615 throw new TransformationFailedException('Expected an array.');
20616 }
20617
20618 $result = [];
20619
20620 foreach ($this->partMapping as $partKey => $originalKeys) {
20621 if (!$array) {
20622 $result[$partKey] = null;
20623 } else {
20624 $result[$partKey] = array_intersect_key($array, array_flip($originalKeys));
20625 }
20626 }
20627
20628 return $result;
20629 }
20630
20631 public function reverseTransform(mixed $array): mixed
20632 {
20633 if (!\is_array($array)) {
20634 throw new TransformationFailedException('Expected an array.');
20635 }
20636
20637 $result = [];
20638 $emptyKeys = [];
20639
20640 foreach ($this->partMapping as $partKey => $originalKeys) {
20641 if (!empty($array[$partKey])) {
20642 foreach ($originalKeys as $originalKey) {
20643 if (isset($array[$partKey][$originalKey])) {
20644 $result[$originalKey] = $array[$partKey][$originalKey];
20645 }
20646 }
20647 } else {
20648 $emptyKeys[] = $partKey;
20649 }
20650 }
20651
20652 if (\count($emptyKeys) > 0) {
20653 if (\count($emptyKeys) === \count($this->partMapping)) {
20654 // All parts empty
20655 return null;
20656 }
20657
20658 throw new TransformationFailedException(\sprintf('The keys "%s" should not be empty.', implode('", "', $emptyKeys)));
20659 }
20660
20661 return $result;
20662 }
20663}
20664
20665------------------------------------------------------------------------------------------------------------------------
20666 ./Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php
20667------------------------------------------------------------------------------------------------------------------------
20668<?php
20669
20670/*
20671 * This file is part of the Symfony package.
20672 *
20673 * (c) Fabien Potencier <fabien@symfony.com>
20674 *
20675 * For the full copyright and license information, please view the LICENSE
20676 * file that was distributed with this source code.
20677 */
20678
20679namespace Symfony\Component\Form\Extension\Core\DataTransformer;
20680
20681use Symfony\Component\Form\Exception\InvalidArgumentException;
20682use Symfony\Component\Form\Exception\TransformationFailedException;
20683use Symfony\Component\Form\Exception\UnexpectedTypeException;
20684
20685/**
20686 * Transforms between a normalized time and a localized time string.
20687 *
20688 * @author Bernhard Schussek <bschussek@gmail.com>
20689 * @author Florian Eckerstorfer <florian@eckerstorfer.org>
20690 *
20691 * @extends BaseDateTimeTransformer<string>
20692 */
20693class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
20694{
20695 private int $dateFormat;
20696 private int $timeFormat;
20697
20698 /**
20699 * @see BaseDateTimeTransformer::formats for available format options
20700 *
20701 * @param string|null $inputTimezone The name of the input timezone
20702 * @param string|null $outputTimezone The name of the output timezone
20703 * @param int|null $dateFormat The date format
20704 * @param int|null $timeFormat The time format
20705 * @param int|\IntlCalendar $calendar One of the \IntlDateFormatter calendar constants or an \IntlCalendar instance
20706 * @param string|null $pattern A pattern to pass to \IntlDateFormatter
20707 *
20708 * @throws UnexpectedTypeException If a format is not supported or if a timezone is not a string
20709 */
20710 public function __construct(
20711 ?string $inputTimezone = null,
20712 ?string $outputTimezone = null,
20713 ?int $dateFormat = null,
20714 ?int $timeFormat = null,
20715 private int|\IntlCalendar $calendar = \IntlDateFormatter::GREGORIAN,
20716 private ?string $pattern = null,
20717 ) {
20718 parent::__construct($inputTimezone, $outputTimezone);
20719
20720 $dateFormat ??= \IntlDateFormatter::MEDIUM;
20721 $timeFormat ??= \IntlDateFormatter::SHORT;
20722
20723 if (!\in_array($dateFormat, self::$formats, true)) {
20724 throw new UnexpectedTypeException($dateFormat, implode('", "', self::$formats));
20725 }
20726
20727 if (!\in_array($timeFormat, self::$formats, true)) {
20728 throw new UnexpectedTypeException($timeFormat, implode('", "', self::$formats));
20729 }
20730
20731 if (\is_int($calendar) && !\in_array($calendar, [\IntlDateFormatter::GREGORIAN, \IntlDateFormatter::TRADITIONAL], true)) {
20732 throw new InvalidArgumentException('The "calendar" option should be either an \IntlDateFormatter constant or an \IntlCalendar instance.');
20733 }
20734
20735 $this->dateFormat = $dateFormat;
20736 $this->timeFormat = $timeFormat;
20737 }
20738
20739 /**
20740 * Transforms a normalized date into a localized date string/array.
20741 *
20742 * @param \DateTimeInterface $dateTime A DateTimeInterface object
20743 *
20744 * @throws TransformationFailedException if the given value is not a \DateTimeInterface
20745 * or if the date could not be transformed
20746 */
20747 public function transform(mixed $dateTime): string
20748 {
20749 if (null === $dateTime) {
20750 return '';
20751 }
20752
20753 if (!$dateTime instanceof \DateTimeInterface) {
20754 throw new TransformationFailedException('Expected a \DateTimeInterface.');
20755 }
20756
20757 $value = $this->getIntlDateFormatter()->format($dateTime->getTimestamp());
20758
20759 if (0 != intl_get_error_code()) {
20760 throw new TransformationFailedException(intl_get_error_message());
20761 }
20762
20763 return $value;
20764 }
20765
20766 /**
20767 * Transforms a localized date string/array into a normalized date.
20768 *
20769 * @param string $value Localized date string
20770 *
20771 * @throws TransformationFailedException if the given value is not a string,
20772 * if the date could not be parsed
20773 */
20774 public function reverseTransform(mixed $value): ?\DateTime
20775 {
20776 if (!\is_string($value)) {
20777 throw new TransformationFailedException('Expected a string.');
20778 }
20779
20780 if ('' === $value) {
20781 return null;
20782 }
20783
20784 // date-only patterns require parsing to be done in UTC, as midnight might not exist in the local timezone due
20785 // to DST changes
20786 $dateOnly = $this->isPatternDateOnly();
20787 $dateFormatter = $this->getIntlDateFormatter($dateOnly);
20788
20789 try {
20790 $timestamp = @$dateFormatter->parse($value);
20791 } catch (\IntlException $e) {
20792 throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
20793 }
20794
20795 if (0 != intl_get_error_code()) {
20796 throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code());
20797 } elseif ($timestamp > 253402214400) {
20798 // This timestamp represents UTC midnight of 9999-12-31 to prevent 5+ digit years
20799 throw new TransformationFailedException('Years beyond 9999 are not supported.');
20800 } elseif (false === $timestamp) {
20801 // the value couldn't be parsed but the Intl extension didn't report an error code, this
20802 // could be the case when the Intl polyfill is used which always returns 0 as the error code
20803 throw new TransformationFailedException(\sprintf('"%s" could not be parsed as a date.', $value));
20804 }
20805
20806 try {
20807 if ($dateOnly) {
20808 // we only care about year-month-date, which has been delivered as a timestamp pointing to UTC midnight
20809 $dateTime = new \DateTime(gmdate('Y-m-d', $timestamp), new \DateTimeZone($this->outputTimezone));
20810 } else {
20811 // read timestamp into DateTime object - the formatter delivers a timestamp
20812 $dateTime = new \DateTime(\sprintf('@%s', $timestamp));
20813 }
20814 // set timezone separately, as it would be ignored if set via the constructor,
20815 // see https://php.net/datetime.construct
20816 $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
20817 } catch (\Exception $e) {
20818 throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
20819 }
20820
20821 if ($this->outputTimezone !== $this->inputTimezone) {
20822 $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
20823 }
20824
20825 return $dateTime;
20826 }
20827
20828 /**
20829 * Returns a preconfigured IntlDateFormatter instance.
20830 *
20831 * @param bool $ignoreTimezone Use UTC regardless of the configured timezone
20832 */
20833 protected function getIntlDateFormatter(bool $ignoreTimezone = false): \IntlDateFormatter
20834 {
20835 $dateFormat = $this->dateFormat;
20836 $timeFormat = $this->timeFormat;
20837 $timezone = new \DateTimeZone($ignoreTimezone ? 'UTC' : $this->outputTimezone);
20838
20839 $calendar = $this->calendar;
20840 $pattern = $this->pattern;
20841
20842 $intlDateFormatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormat, $timeFormat, $timezone, $calendar, $pattern ?? '');
20843 $intlDateFormatter->setLenient(false);
20844
20845 return $intlDateFormatter;
20846 }
20847
20848 /**
20849 * Checks if the pattern contains only a date.
20850 */
20851 protected function isPatternDateOnly(): bool
20852 {
20853 if (null === $this->pattern) {
20854 return false;
20855 }
20856
20857 // strip escaped text
20858 $pattern = preg_replace("#'(.*?)'#", '', $this->pattern);
20859
20860 // check for the absence of time-related placeholders
20861 return 0 === preg_match('#[ahHkKmsSAzZOvVxX]#', $pattern);
20862 }
20863}
20864
20865------------------------------------------------------------------------------------------------------------------------
20866 ./Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php
20867------------------------------------------------------------------------------------------------------------------------
20868<?php
20869
20870/*
20871 * This file is part of the Symfony package.
20872 *
20873 * (c) Fabien Potencier <fabien@symfony.com>
20874 *
20875 * For the full copyright and license information, please view the LICENSE
20876 * file that was distributed with this source code.
20877 */
20878
20879namespace Symfony\Component\Form\Extension\Core\DataTransformer;
20880
20881use Symfony\Component\Form\Exception\TransformationFailedException;
20882
20883/**
20884 * Transforms between a normalized format and a localized money string.
20885 *
20886 * @author Bernhard Schussek <bschussek@gmail.com>
20887 * @author Florian Eckerstorfer <florian@eckerstorfer.org>
20888 */
20889class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransformer
20890{
20891 private int $divisor;
20892
20893 public function __construct(
20894 ?int $scale = 2,
20895 ?bool $grouping = true,
20896 ?int $roundingMode = \NumberFormatter::ROUND_HALFUP,
20897 ?int $divisor = 1,
20898 ?string $locale = null,
20899 private readonly string $input = 'float',
20900 ) {
20901 parent::__construct($scale ?? 2, $grouping ?? true, $roundingMode, $locale);
20902
20903 $this->divisor = $divisor ?? 1;
20904 }
20905
20906 /**
20907 * Transforms a normalized format into a localized money string.
20908 *
20909 * @param int|float|null $value Normalized number
20910 *
20911 * @throws TransformationFailedException if the given value is not numeric or
20912 * if the value cannot be transformed
20913 */
20914 public function transform(mixed $value): string
20915 {
20916 if (null !== $value && 1 !== $this->divisor) {
20917 if (!is_numeric($value)) {
20918 throw new TransformationFailedException('Expected a numeric.');
20919 }
20920 $value /= $this->divisor;
20921 }
20922
20923 return parent::transform($value);
20924 }
20925
20926 /**
20927 * Transforms a localized money string into a normalized format.
20928 *
20929 * @param string $value Localized money string
20930 *
20931 * @throws TransformationFailedException if the given value is not a string
20932 * or if the value cannot be transformed
20933 */
20934 public function reverseTransform(mixed $value): int|float|null
20935 {
20936 $value = parent::reverseTransform($value);
20937 if (null !== $value) {
20938 $value = (string) ($value * $this->divisor);
20939
20940 if ('float' === $this->input) {
20941 return (float) $value;
20942 }
20943
20944 if ($value > \PHP_INT_MAX || $value < \PHP_INT_MIN) {
20945 throw new TransformationFailedException(\sprintf('Cannot cast "%s" to an integer. Try setting the input to "float" instead.', $value));
20946 }
20947
20948 $value = (int) $value;
20949 }
20950
20951 return $value;
20952 }
20953}
20954
20955------------------------------------------------------------------------------------------------------------------------
20956 ./Extension/Core/DataTransformer/DateIntervalToStringTransformer.php
20957------------------------------------------------------------------------------------------------------------------------
20958<?php
20959
20960/*
20961 * This file is part of the Symfony package.
20962 *
20963 * (c) Fabien Potencier <fabien@symfony.com>
20964 *
20965 * For the full copyright and license information, please view the LICENSE
20966 * file that was distributed with this source code.
20967 */
20968
20969namespace Symfony\Component\Form\Extension\Core\DataTransformer;
20970
20971use Symfony\Component\Form\DataTransformerInterface;
20972use Symfony\Component\Form\Exception\TransformationFailedException;
20973use Symfony\Component\Form\Exception\UnexpectedTypeException;
20974
20975/**
20976 * Transforms between a date string and a DateInterval object.
20977 *
20978 * @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
20979 *
20980 * @implements DataTransformerInterface<\DateInterval, string>
20981 */
20982class DateIntervalToStringTransformer implements DataTransformerInterface
20983{
20984 /**
20985 * Transforms a \DateInterval instance to a string.
20986 *
20987 * @see \DateInterval::format() for supported formats
20988 *
20989 * @param string $format The date format
20990 */
20991 public function __construct(
20992 private string $format = 'P%yY%mM%dDT%hH%iM%sS',
20993 ) {
20994 }
20995
20996 /**
20997 * Transforms a DateInterval object into a date string with the configured format.
20998 *
20999 * @param \DateInterval|null $value A DateInterval object
21000 *
21001 * @throws UnexpectedTypeException if the given value is not a \DateInterval instance
21002 */
21003 public function transform(mixed $value): string
21004 {
21005 if (null === $value) {
21006 return '';
21007 }
21008 if (!$value instanceof \DateInterval) {
21009 throw new UnexpectedTypeException($value, \DateInterval::class);
21010 }
21011
21012 return $value->format($this->format);
21013 }
21014
21015 /**
21016 * Transforms a date string in the configured format into a DateInterval object.
21017 *
21018 * @param string $value An ISO 8601 or date string like date interval presentation
21019 *
21020 * @throws UnexpectedTypeException if the given value is not a string
21021 * @throws TransformationFailedException if the date interval could not be parsed
21022 */
21023 public function reverseTransform(mixed $value): ?\DateInterval
21024 {
21025 if (null === $value) {
21026 return null;
21027 }
21028 if (!\is_string($value)) {
21029 throw new UnexpectedTypeException($value, 'string');
21030 }
21031 if ('' === $value) {
21032 return null;
21033 }
21034 if (!$this->isISO8601($value)) {
21035 throw new TransformationFailedException('Non ISO 8601 date strings are not supported yet.');
21036 }
21037 $valuePattern = '/^'.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?P<$1>\d+)$2', $this->format).'$/';
21038 if (!preg_match($valuePattern, $value)) {
21039 throw new TransformationFailedException(\sprintf('Value "%s" contains intervals not accepted by format "%s".', $value, $this->format));
21040 }
21041 try {
21042 $dateInterval = new \DateInterval($value);
21043 } catch (\Exception $e) {
21044 throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
21045 }
21046
21047 return $dateInterval;
21048 }
21049
21050 private function isISO8601(string $string): bool
21051 {
21052 return preg_match('/^P(?=\w*(?:\d|%\w))(?:\d+Y|%[yY]Y)?(?:\d+M|%[mM]M)?(?:(?:\d+D|%[dD]D)|(?:\d+W|%[wW]W))?(?:T(?:\d+H|[hH]H)?(?:\d+M|[iI]M)?(?:\d+S|[sS]S)?)?$/', $string);
21053 }
21054}
21055
21056------------------------------------------------------------------------------------------------------------------------
21057 ./Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php
21058------------------------------------------------------------------------------------------------------------------------
21059<?php
21060
21061/*
21062 * This file is part of the Symfony package.
21063 *
21064 * (c) Fabien Potencier <fabien@symfony.com>
21065 *
21066 * For the full copyright and license information, please view the LICENSE
21067 * file that was distributed with this source code.
21068 */
21069
21070namespace Symfony\Component\Form\Extension\Core\DataTransformer;
21071
21072use Symfony\Component\Form\DataTransformerInterface;
21073use Symfony\Component\Form\Exception\TransformationFailedException;
21074use Symfony\Component\Form\Exception\UnexpectedTypeException;
21075
21076/**
21077 * Transforms between a normalized format (integer or float) and a percentage value.
21078 *
21079 * @author Bernhard Schussek <bschussek@gmail.com>
21080 * @author Florian Eckerstorfer <florian@eckerstorfer.org>
21081 *
21082 * @implements DataTransformerInterface<int|float, string>
21083 */
21084class PercentToLocalizedStringTransformer implements DataTransformerInterface
21085{
21086 public const FRACTIONAL = 'fractional';
21087 public const INTEGER = 'integer';
21088
21089 protected static array $types = [
21090 self::FRACTIONAL,
21091 self::INTEGER,
21092 ];
21093
21094 private string $type;
21095 private int $scale;
21096
21097 /**
21098 * @see self::$types for a list of supported types
21099 *
21100 * @param int $roundingMode A value from \NumberFormatter, such as \NumberFormatter::ROUND_HALFUP
21101 * @param bool $html5Format Use an HTML5 specific format, see https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats
21102 *
21103 * @throws UnexpectedTypeException if the given value of type is unknown
21104 */
21105 public function __construct(
21106 ?int $scale = null,
21107 ?string $type = null,
21108 private int $roundingMode = \NumberFormatter::ROUND_HALFUP,
21109 private bool $html5Format = false,
21110 ) {
21111 $type ??= self::FRACTIONAL;
21112
21113 if (!\in_array($type, self::$types, true)) {
21114 throw new UnexpectedTypeException($type, implode('", "', self::$types));
21115 }
21116
21117 $this->type = $type;
21118 $this->scale = $scale ?? 0;
21119 }
21120
21121 /**
21122 * Transforms between a normalized format (integer or float) into a percentage value.
21123 *
21124 * @param int|float $value Normalized value
21125 *
21126 * @throws TransformationFailedException if the given value is not numeric or
21127 * if the value could not be transformed
21128 */
21129 public function transform(mixed $value): string
21130 {
21131 if (null === $value) {
21132 return '';
21133 }
21134
21135 if (!is_numeric($value)) {
21136 throw new TransformationFailedException('Expected a numeric.');
21137 }
21138
21139 if (self::FRACTIONAL == $this->type) {
21140 $value *= 100;
21141 }
21142
21143 $formatter = $this->getNumberFormatter();
21144 $value = $formatter->format($value);
21145
21146 if (intl_is_failure($formatter->getErrorCode())) {
21147 throw new TransformationFailedException($formatter->getErrorMessage());
21148 }
21149
21150 // replace the UTF-8 non break spaces
21151 return $value;
21152 }
21153
21154 /**
21155 * Transforms between a percentage value into a normalized format (integer or float).
21156 *
21157 * @param string $value Percentage value
21158 *
21159 * @throws TransformationFailedException if the given value is not a string or
21160 * if the value could not be transformed
21161 */
21162 public function reverseTransform(mixed $value): int|float|null
21163 {
21164 if (!\is_string($value)) {
21165 throw new TransformationFailedException('Expected a string.');
21166 }
21167
21168 if ('' === $value) {
21169 return null;
21170 }
21171
21172 $position = 0;
21173 $formatter = $this->getNumberFormatter();
21174 $groupSep = $formatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL);
21175 $decSep = $formatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
21176 $grouping = $formatter->getAttribute(\NumberFormatter::GROUPING_USED);
21177
21178 if ('.' !== $decSep && (!$grouping || '.' !== $groupSep)) {
21179 $value = str_replace('.', $decSep, $value);
21180 }
21181
21182 if (',' !== $decSep && (!$grouping || ',' !== $groupSep)) {
21183 $value = str_replace(',', $decSep, $value);
21184 }
21185
21186 if (str_contains($value, $decSep)) {
21187 $type = \NumberFormatter::TYPE_DOUBLE;
21188 } else {
21189 $type = \PHP_INT_SIZE === 8 ? \NumberFormatter::TYPE_INT64 : \NumberFormatter::TYPE_INT32;
21190 }
21191
21192 try {
21193 // replace normal spaces so that the formatter can read them
21194 $result = @$formatter->parse(str_replace(' ', "\xc2\xa0", $value), $type, $position);
21195 } catch (\IntlException $e) {
21196 throw new TransformationFailedException($e->getMessage(), 0, $e);
21197 }
21198
21199 if (intl_is_failure($formatter->getErrorCode())) {
21200 throw new TransformationFailedException($formatter->getErrorMessage(), $formatter->getErrorCode());
21201 }
21202
21203 if (self::FRACTIONAL == $this->type) {
21204 $result /= 100;
21205 }
21206
21207 if (\function_exists('mb_detect_encoding') && false !== $encoding = mb_detect_encoding($value, null, true)) {
21208 $length = mb_strlen($value, $encoding);
21209 $remainder = mb_substr($value, $position, $length, $encoding);
21210 } else {
21211 $length = \strlen($value);
21212 $remainder = substr($value, $position, $length);
21213 }
21214
21215 // After parsing, position holds the index of the character where the
21216 // parsing stopped
21217 if ($position < $length) {
21218 // Check if there are unrecognized characters at the end of the
21219 // number (excluding whitespace characters)
21220 $remainder = trim($remainder, " \t\n\r\0\x0b\xc2\xa0");
21221
21222 if ('' !== $remainder) {
21223 throw new TransformationFailedException(\sprintf('The number contains unrecognized characters: "%s".', $remainder));
21224 }
21225 }
21226
21227 return $this->round($result);
21228 }
21229
21230 /**
21231 * Returns a preconfigured \NumberFormatter instance.
21232 */
21233 protected function getNumberFormatter(): \NumberFormatter
21234 {
21235 // Values used in HTML5 number inputs should be formatted as in "1234.5", ie. 'en' format without grouping,
21236 // according to https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats
21237 $formatter = new \NumberFormatter($this->html5Format ? 'en' : \Locale::getDefault(), \NumberFormatter::DECIMAL);
21238
21239 if ($this->html5Format) {
21240 $formatter->setAttribute(\NumberFormatter::GROUPING_USED, 0);
21241 }
21242
21243 $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
21244
21245 $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
21246
21247 return $formatter;
21248 }
21249
21250 /**
21251 * Rounds a number according to the configured scale and rounding mode.
21252 */
21253 private function round(int|float $number): int|float
21254 {
21255 // shift number to maintain the correct scale during rounding
21256 $roundingCoef = 10 ** $this->scale;
21257
21258 if (self::FRACTIONAL === $this->type) {
21259 $roundingCoef *= 100;
21260 }
21261
21262 // string representation to avoid rounding errors, similar to bcmul()
21263 $number = (string) ($number * $roundingCoef);
21264
21265 $number = match ($this->roundingMode) {
21266 \NumberFormatter::ROUND_CEILING => ceil($number),
21267 \NumberFormatter::ROUND_FLOOR => floor($number),
21268 \NumberFormatter::ROUND_UP => $number > 0 ? ceil($number) : floor($number),
21269 \NumberFormatter::ROUND_DOWN => $number > 0 ? floor($number) : ceil($number),
21270 \NumberFormatter::ROUND_HALFEVEN => round($number, 0, \PHP_ROUND_HALF_EVEN),
21271 \NumberFormatter::ROUND_HALFUP => round($number, 0, \PHP_ROUND_HALF_UP),
21272 \NumberFormatter::ROUND_HALFDOWN => round($number, 0, \PHP_ROUND_HALF_DOWN),
21273 };
21274
21275 return 1 === $roundingCoef ? (int) $number : $number / $roundingCoef;
21276 }
21277}
21278
21279------------------------------------------------------------------------------------------------------------------------
21280 ./Extension/Core/DataTransformer/DateTimeImmutableToDateTimeTransformer.php
21281------------------------------------------------------------------------------------------------------------------------
21282<?php
21283
21284/*
21285 * This file is part of the Symfony package.
21286 *
21287 * (c) Fabien Potencier <fabien@symfony.com>
21288 *
21289 * For the full copyright and license information, please view the LICENSE
21290 * file that was distributed with this source code.
21291 */
21292
21293namespace Symfony\Component\Form\Extension\Core\DataTransformer;
21294
21295use Symfony\Component\Form\DataTransformerInterface;
21296use Symfony\Component\Form\Exception\TransformationFailedException;
21297
21298/**
21299 * Transforms between a DateTimeImmutable object and a DateTime object.
21300 *
21301 * @author Valentin Udaltsov <udaltsov.valentin@gmail.com>
21302 *
21303 * @implements DataTransformerInterface<\DateTimeImmutable, \DateTime>
21304 */
21305final class DateTimeImmutableToDateTimeTransformer implements DataTransformerInterface
21306{
21307 /**
21308 * Transforms a DateTimeImmutable into a DateTime object.
21309 *
21310 * @param \DateTimeImmutable|null $value A DateTimeImmutable object
21311 *
21312 * @throws TransformationFailedException If the given value is not a \DateTimeImmutable
21313 */
21314 public function transform(mixed $value): ?\DateTime
21315 {
21316 if (null === $value) {
21317 return null;
21318 }
21319
21320 if (!$value instanceof \DateTimeImmutable) {
21321 throw new TransformationFailedException('Expected a \DateTimeImmutable.');
21322 }
21323
21324 return \DateTime::createFromImmutable($value);
21325 }
21326
21327 /**
21328 * Transforms a DateTime object into a DateTimeImmutable object.
21329 *
21330 * @param \DateTime|null $value A DateTime object
21331 *
21332 * @throws TransformationFailedException If the given value is not a \DateTime
21333 */
21334 public function reverseTransform(mixed $value): ?\DateTimeImmutable
21335 {
21336 if (null === $value) {
21337 return null;
21338 }
21339
21340 if (!$value instanceof \DateTime) {
21341 throw new TransformationFailedException('Expected a \DateTime.');
21342 }
21343
21344 return \DateTimeImmutable::createFromMutable($value);
21345 }
21346}
21347
21348------------------------------------------------------------------------------------------------------------------------
21349 ./Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php
21350------------------------------------------------------------------------------------------------------------------------
21351<?php
21352
21353/*
21354 * This file is part of the Symfony package.
21355 *
21356 * (c) Fabien Potencier <fabien@symfony.com>
21357 *
21358 * For the full copyright and license information, please view the LICENSE
21359 * file that was distributed with this source code.
21360 */
21361
21362namespace Symfony\Component\Form\Extension\Core\DataTransformer;
21363
21364use Symfony\Component\Form\DataTransformerInterface;
21365use Symfony\Component\Form\Exception\TransformationFailedException;
21366
21367/**
21368 * Transforms between a timezone identifier string and a DateTimeZone object.
21369 *
21370 * @author Roland Franssen <franssen.roland@gmail.com>
21371 *
21372 * @implements DataTransformerInterface<\DateTimeZone|array<\DateTimeZone>, string|array<string>>
21373 */
21374class DateTimeZoneToStringTransformer implements DataTransformerInterface
21375{
21376 public function __construct(
21377 private bool $multiple = false,
21378 ) {
21379 }
21380
21381 public function transform(mixed $dateTimeZone): mixed
21382 {
21383 if (null === $dateTimeZone) {
21384 return null;
21385 }
21386
21387 if ($this->multiple) {
21388 if (!\is_array($dateTimeZone)) {
21389 throw new TransformationFailedException('Expected an array of \DateTimeZone objects.');
21390 }
21391
21392 return array_map([new self(), 'transform'], $dateTimeZone);
21393 }
21394
21395 if (!$dateTimeZone instanceof \DateTimeZone) {
21396 throw new TransformationFailedException('Expected a \DateTimeZone object.');
21397 }
21398
21399 return $dateTimeZone->getName();
21400 }
21401
21402 public function reverseTransform(mixed $value): mixed
21403 {
21404 if (null === $value) {
21405 return null;
21406 }
21407
21408 if ($this->multiple) {
21409 if (!\is_array($value)) {
21410 throw new TransformationFailedException('Expected an array of timezone identifier strings.');
21411 }
21412
21413 return array_map([new self(), 'reverseTransform'], $value);
21414 }
21415
21416 if (!\is_string($value)) {
21417 throw new TransformationFailedException('Expected a timezone identifier string.');
21418 }
21419
21420 try {
21421 return new \DateTimeZone($value);
21422 } catch (\Exception $e) {
21423 throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
21424 }
21425 }
21426}
21427
21428------------------------------------------------------------------------------------------------------------------------
21429 ./Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
21430------------------------------------------------------------------------------------------------------------------------
21431<?php
21432
21433/*
21434 * This file is part of the Symfony package.
21435 *
21436 * (c) Fabien Potencier <fabien@symfony.com>
21437 *
21438 * For the full copyright and license information, please view the LICENSE
21439 * file that was distributed with this source code.
21440 */
21441
21442namespace Symfony\Component\Form\Extension\Core\DataTransformer;
21443
21444use Symfony\Component\Form\DataTransformerInterface;
21445use Symfony\Component\Form\Exception\TransformationFailedException;
21446
21447/**
21448 * Transforms between a number type and a localized number with grouping
21449 * (each thousand) and comma separators.
21450 *
21451 * @author Bernhard Schussek <bschussek@gmail.com>
21452 * @author Florian Eckerstorfer <florian@eckerstorfer.org>
21453 *
21454 * @implements DataTransformerInterface<int|float, string>
21455 */
21456class NumberToLocalizedStringTransformer implements DataTransformerInterface
21457{
21458 protected bool $grouping;
21459 protected int $roundingMode;
21460
21461 public function __construct(
21462 private ?int $scale = null,
21463 ?bool $grouping = false,
21464 ?int $roundingMode = \NumberFormatter::ROUND_HALFUP,
21465 private ?string $locale = null,
21466 ) {
21467 $this->grouping = $grouping ?? false;
21468 $this->roundingMode = $roundingMode ?? \NumberFormatter::ROUND_HALFUP;
21469 }
21470
21471 /**
21472 * Transforms a number type into localized number.
21473 *
21474 * @param int|float|null $value Number value
21475 *
21476 * @throws TransformationFailedException if the given value is not numeric
21477 * or if the value cannot be transformed
21478 */
21479 public function transform(mixed $value): string
21480 {
21481 if (null === $value) {
21482 return '';
21483 }
21484
21485 if (!is_numeric($value)) {
21486 throw new TransformationFailedException('Expected a numeric.');
21487 }
21488
21489 $formatter = $this->getNumberFormatter();
21490 $value = $formatter->format($value);
21491
21492 if (intl_is_failure($formatter->getErrorCode())) {
21493 throw new TransformationFailedException($formatter->getErrorMessage());
21494 }
21495
21496 // Convert non-breaking and narrow non-breaking spaces to normal ones
21497 return str_replace(["\xc2\xa0", "\xe2\x80\xaf"], ' ', $value);
21498 }
21499
21500 /**
21501 * Transforms a localized number into an integer or float.
21502 *
21503 * @param string $value The localized value
21504 *
21505 * @throws TransformationFailedException if the given value is not a string
21506 * or if the value cannot be transformed
21507 */
21508 public function reverseTransform(mixed $value): int|float|null
21509 {
21510 if (null !== $value && !\is_string($value)) {
21511 throw new TransformationFailedException('Expected a string.');
21512 }
21513
21514 if (null === $value || '' === $value) {
21515 return null;
21516 }
21517
21518 if (\in_array($value, ['NaN', 'NAN', 'nan'], true)) {
21519 throw new TransformationFailedException('"NaN" is not a valid number.');
21520 }
21521
21522 $position = 0;
21523 $formatter = $this->getNumberFormatter();
21524 $groupSep = $formatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL);
21525 $decSep = $formatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
21526
21527 if ('.' !== $decSep && (!$this->grouping || '.' !== $groupSep)) {
21528 $value = str_replace('.', $decSep, $value);
21529 }
21530
21531 if (',' !== $decSep && (!$this->grouping || ',' !== $groupSep)) {
21532 $value = str_replace(',', $decSep, $value);
21533 }
21534
21535 // If the value is in exponential notation with a negative exponent, we end up with a float value too
21536 if (str_contains($value, $decSep) || false !== stripos($value, 'e-')) {
21537 $type = \NumberFormatter::TYPE_DOUBLE;
21538 } else {
21539 $type = \PHP_INT_SIZE === 8
21540 ? \NumberFormatter::TYPE_INT64
21541 : \NumberFormatter::TYPE_INT32;
21542 }
21543
21544 try {
21545 $result = @$formatter->parse($value, $type, $position);
21546 } catch (\IntlException $e) {
21547 throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
21548 }
21549
21550 if (intl_is_failure($formatter->getErrorCode())) {
21551 throw new TransformationFailedException($formatter->getErrorMessage(), $formatter->getErrorCode());
21552 }
21553
21554 if ($result >= \PHP_INT_MAX || $result <= -\PHP_INT_MAX) {
21555 throw new TransformationFailedException('I don\'t have a clear idea what infinity looks like.');
21556 }
21557
21558 $result = $this->castParsedValue($result);
21559
21560 if (false !== $encoding = mb_detect_encoding($value, null, true)) {
21561 $length = mb_strlen($value, $encoding);
21562 $remainder = mb_substr($value, $position, $length, $encoding);
21563 } else {
21564 $length = \strlen($value);
21565 $remainder = substr($value, $position, $length);
21566 }
21567
21568 // After parsing, position holds the index of the character where the
21569 // parsing stopped
21570 if ($position < $length) {
21571 // Check if there are unrecognized characters at the end of the
21572 // number (excluding whitespace characters)
21573 $remainder = trim($remainder, " \t\n\r\0\x0b\xc2\xa0");
21574
21575 if ('' !== $remainder) {
21576 throw new TransformationFailedException(\sprintf('The number contains unrecognized characters: "%s".', $remainder));
21577 }
21578 }
21579
21580 // NumberFormatter::parse() does not round
21581 return $this->round($result);
21582 }
21583
21584 /**
21585 * Returns a preconfigured \NumberFormatter instance.
21586 */
21587 protected function getNumberFormatter(): \NumberFormatter
21588 {
21589 $formatter = new \NumberFormatter($this->locale ?? \Locale::getDefault(), \NumberFormatter::DECIMAL);
21590
21591 if (null !== $this->scale) {
21592 $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
21593 $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
21594 }
21595
21596 $formatter->setAttribute(\NumberFormatter::GROUPING_USED, $this->grouping);
21597
21598 return $formatter;
21599 }
21600
21601 /**
21602 * @internal
21603 */
21604 protected function castParsedValue(int|float $value): int|float
21605 {
21606 if (\is_int($value) && $value === (int) $float = (float) $value) {
21607 return $float;
21608 }
21609
21610 return $value;
21611 }
21612
21613 /**
21614 * Rounds a number according to the configured scale and rounding mode.
21615 */
21616 private function round(int|float $number): int|float
21617 {
21618 if (null !== $this->scale) {
21619 // shift number to maintain the correct scale during rounding
21620 $roundingCoef = 10 ** $this->scale;
21621 // string representation to avoid rounding errors, similar to bcmul()
21622 $number = (string) ($number * $roundingCoef);
21623
21624 $number = match ($this->roundingMode) {
21625 \NumberFormatter::ROUND_CEILING => ceil($number),
21626 \NumberFormatter::ROUND_FLOOR => floor($number),
21627 \NumberFormatter::ROUND_UP => $number > 0 ? ceil($number) : floor($number),
21628 \NumberFormatter::ROUND_DOWN => $number > 0 ? floor($number) : ceil($number),
21629 \NumberFormatter::ROUND_HALFEVEN => round($number, 0, \PHP_ROUND_HALF_EVEN),
21630 \NumberFormatter::ROUND_HALFUP => round($number, 0, \PHP_ROUND_HALF_UP),
21631 \NumberFormatter::ROUND_HALFDOWN => round($number, 0, \PHP_ROUND_HALF_DOWN),
21632 };
21633
21634 $number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef;
21635 }
21636
21637 return $number;
21638 }
21639}
21640
21641------------------------------------------------------------------------------------------------------------------------
21642 ./Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php
21643------------------------------------------------------------------------------------------------------------------------
21644<?php
21645
21646/*
21647 * This file is part of the Symfony package.
21648 *
21649 * (c) Fabien Potencier <fabien@symfony.com>
21650 *
21651 * For the full copyright and license information, please view the LICENSE
21652 * file that was distributed with this source code.
21653 */
21654
21655namespace Symfony\Component\Form\Extension\Core\DataTransformer;
21656
21657use Symfony\Component\Form\DataTransformerInterface;
21658use Symfony\Component\Form\Exception\TransformationFailedException;
21659
21660/**
21661 * Transforms between a timezone identifier string and a IntlTimeZone object.
21662 *
21663 * @author Roland Franssen <franssen.roland@gmail.com>
21664 *
21665 * @implements DataTransformerInterface<\IntlTimeZone|array<\IntlTimeZone>, string|array<string>>
21666 */
21667class IntlTimeZoneToStringTransformer implements DataTransformerInterface
21668{
21669 public function __construct(
21670 private bool $multiple = false,
21671 ) {
21672 }
21673
21674 public function transform(mixed $intlTimeZone): mixed
21675 {
21676 if (null === $intlTimeZone) {
21677 return null;
21678 }
21679
21680 if ($this->multiple) {
21681 if (!\is_array($intlTimeZone)) {
21682 throw new TransformationFailedException('Expected an array of \IntlTimeZone objects.');
21683 }
21684
21685 return array_map([new self(), 'transform'], $intlTimeZone);
21686 }
21687
21688 if (!$intlTimeZone instanceof \IntlTimeZone) {
21689 throw new TransformationFailedException('Expected a \IntlTimeZone object.');
21690 }
21691
21692 return $intlTimeZone->getID();
21693 }
21694
21695 public function reverseTransform(mixed $value): mixed
21696 {
21697 if (null === $value) {
21698 return null;
21699 }
21700
21701 if ($this->multiple) {
21702 if (!\is_array($value)) {
21703 throw new TransformationFailedException('Expected an array of timezone identifier strings.');
21704 }
21705
21706 return array_map([new self(), 'reverseTransform'], $value);
21707 }
21708
21709 if (!\is_string($value)) {
21710 throw new TransformationFailedException('Expected a timezone identifier string.');
21711 }
21712
21713 $intlTimeZone = \IntlTimeZone::createTimeZone($value);
21714
21715 if ('Etc/Unknown' === $intlTimeZone->getID()) {
21716 throw new TransformationFailedException(\sprintf('Unknown timezone identifier "%s".', $value));
21717 }
21718
21719 return $intlTimeZone;
21720 }
21721}
21722
21723------------------------------------------------------------------------------------------------------------------------
21724 ./Extension/Core/DataTransformer/UuidToStringTransformer.php
21725------------------------------------------------------------------------------------------------------------------------
21726<?php
21727
21728/*
21729 * This file is part of the Symfony package.
21730 *
21731 * (c) Fabien Potencier <fabien@symfony.com>
21732 *
21733 * For the full copyright and license information, please view the LICENSE
21734 * file that was distributed with this source code.
21735 */
21736
21737namespace Symfony\Component\Form\Extension\Core\DataTransformer;
21738
21739use Symfony\Component\Form\DataTransformerInterface;
21740use Symfony\Component\Form\Exception\TransformationFailedException;
21741use Symfony\Component\Uid\Uuid;
21742
21743/**
21744 * Transforms between a UUID string and a Uuid object.
21745 *
21746 * @author Pavel Dyakonov <wapinet@mail.ru>
21747 *
21748 * @implements DataTransformerInterface<Uuid, string>
21749 */
21750class UuidToStringTransformer implements DataTransformerInterface
21751{
21752 /**
21753 * Transforms a Uuid object into a string.
21754 *
21755 * @param Uuid $value A Uuid object
21756 *
21757 * @throws TransformationFailedException If the given value is not a Uuid object
21758 */
21759 public function transform(mixed $value): ?string
21760 {
21761 if (null === $value) {
21762 return null;
21763 }
21764
21765 if (!$value instanceof Uuid) {
21766 throw new TransformationFailedException('Expected a Uuid.');
21767 }
21768
21769 return (string) $value;
21770 }
21771
21772 /**
21773 * Transforms a UUID string into a Uuid object.
21774 *
21775 * @param string $value A UUID string
21776 *
21777 * @throws TransformationFailedException If the given value is not a string,
21778 * or could not be transformed
21779 */
21780 public function reverseTransform(mixed $value): ?Uuid
21781 {
21782 if (null === $value || '' === $value) {
21783 return null;
21784 }
21785
21786 if (!\is_string($value)) {
21787 throw new TransformationFailedException('Expected a string.');
21788 }
21789
21790 if (!Uuid::isValid($value)) {
21791 throw new TransformationFailedException(\sprintf('The value "%s" is not a valid UUID.', $value));
21792 }
21793
21794 try {
21795 return Uuid::fromString($value);
21796 } catch (\InvalidArgumentException $e) {
21797 throw new TransformationFailedException(\sprintf('The value "%s" is not a valid UUID.', $value), $e->getCode(), $e);
21798 }
21799 }
21800}
21801
21802------------------------------------------------------------------------------------------------------------------------
21803 ./Extension/Validator/ValidatorTypeGuesser.php
21804------------------------------------------------------------------------------------------------------------------------
21805<?php
21806
21807/*
21808 * This file is part of the Symfony package.
21809 *
21810 * (c) Fabien Potencier <fabien@symfony.com>
21811 *
21812 * For the full copyright and license information, please view the LICENSE
21813 * file that was distributed with this source code.
21814 */
21815
21816namespace Symfony\Component\Form\Extension\Validator;
21817
21818use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
21819use Symfony\Component\Form\Extension\Core\Type\CollectionType;
21820use Symfony\Component\Form\Extension\Core\Type\CountryType;
21821use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
21822use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
21823use Symfony\Component\Form\Extension\Core\Type\DateType;
21824use Symfony\Component\Form\Extension\Core\Type\EmailType;
21825use Symfony\Component\Form\Extension\Core\Type\FileType;
21826use Symfony\Component\Form\Extension\Core\Type\IntegerType;
21827use Symfony\Component\Form\Extension\Core\Type\LanguageType;
21828use Symfony\Component\Form\Extension\Core\Type\LocaleType;
21829use Symfony\Component\Form\Extension\Core\Type\NumberType;
21830use Symfony\Component\Form\Extension\Core\Type\TextType;
21831use Symfony\Component\Form\Extension\Core\Type\TimeType;
21832use Symfony\Component\Form\Extension\Core\Type\UrlType;
21833use Symfony\Component\Form\FormTypeGuesserInterface;
21834use Symfony\Component\Form\Guess\Guess;
21835use Symfony\Component\Form\Guess\TypeGuess;
21836use Symfony\Component\Form\Guess\ValueGuess;
21837use Symfony\Component\Validator\Constraint;
21838use Symfony\Component\Validator\Constraints\Count;
21839use Symfony\Component\Validator\Constraints\Country;
21840use Symfony\Component\Validator\Constraints\Currency;
21841use Symfony\Component\Validator\Constraints\Date;
21842use Symfony\Component\Validator\Constraints\DateTime;
21843use Symfony\Component\Validator\Constraints\Email;
21844use Symfony\Component\Validator\Constraints\File;
21845use Symfony\Component\Validator\Constraints\Image;
21846use Symfony\Component\Validator\Constraints\Ip;
21847use Symfony\Component\Validator\Constraints\IsFalse;
21848use Symfony\Component\Validator\Constraints\IsTrue;
21849use Symfony\Component\Validator\Constraints\Language;
21850use Symfony\Component\Validator\Constraints\Length;
21851use Symfony\Component\Validator\Constraints\Locale;
21852use Symfony\Component\Validator\Constraints\NotBlank;
21853use Symfony\Component\Validator\Constraints\NotNull;
21854use Symfony\Component\Validator\Constraints\Range;
21855use Symfony\Component\Validator\Constraints\Regex;
21856use Symfony\Component\Validator\Constraints\Time;
21857use Symfony\Component\Validator\Constraints\Type;
21858use Symfony\Component\Validator\Constraints\Url;
21859use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
21860use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
21861
21862class ValidatorTypeGuesser implements FormTypeGuesserInterface
21863{
21864 public function __construct(
21865 private MetadataFactoryInterface $metadataFactory,
21866 ) {
21867 }
21868
21869 public function guessType(string $class, string $property): ?TypeGuess
21870 {
21871 return $this->guess($class, $property, $this->guessTypeForConstraint(...));
21872 }
21873
21874 public function guessRequired(string $class, string $property): ?ValueGuess
21875 {
21876 // If we don't find any constraint telling otherwise, we can assume
21877 // that a field is not required (with LOW_CONFIDENCE)
21878 return $this->guess($class, $property, $this->guessRequiredForConstraint(...), false);
21879 }
21880
21881 public function guessMaxLength(string $class, string $property): ?ValueGuess
21882 {
21883 return $this->guess($class, $property, $this->guessMaxLengthForConstraint(...));
21884 }
21885
21886 public function guessPattern(string $class, string $property): ?ValueGuess
21887 {
21888 return $this->guess($class, $property, $this->guessPatternForConstraint(...));
21889 }
21890
21891 /**
21892 * Guesses a field class name for a given constraint.
21893 */
21894 public function guessTypeForConstraint(Constraint $constraint): ?TypeGuess
21895 {
21896 switch ($constraint::class) {
21897 case Type::class:
21898 switch ($constraint->type) {
21899 case 'array':
21900 return new TypeGuess(CollectionType::class, [], Guess::MEDIUM_CONFIDENCE);
21901 case 'boolean':
21902 case 'bool':
21903 return new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE);
21904
21905 case 'double':
21906 case 'float':
21907 case 'numeric':
21908 case 'real':
21909 return new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE);
21910
21911 case 'integer':
21912 case 'int':
21913 case 'long':
21914 return new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE);
21915
21916 case \DateTime::class:
21917 case '\DateTime':
21918 return new TypeGuess(DateType::class, [], Guess::MEDIUM_CONFIDENCE);
21919
21920 case \DateTimeImmutable::class:
21921 case '\DateTimeImmutable':
21922 case \DateTimeInterface::class:
21923 case '\DateTimeInterface':
21924 return new TypeGuess(DateType::class, ['input' => 'datetime_immutable'], Guess::MEDIUM_CONFIDENCE);
21925
21926 case 'string':
21927 return new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE);
21928 }
21929 break;
21930
21931 case Country::class:
21932 return new TypeGuess(CountryType::class, [], Guess::HIGH_CONFIDENCE);
21933
21934 case Currency::class:
21935 return new TypeGuess(CurrencyType::class, [], Guess::HIGH_CONFIDENCE);
21936
21937 case Date::class:
21938 return new TypeGuess(DateType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE);
21939
21940 case DateTime::class:
21941 return new TypeGuess(DateTimeType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE);
21942
21943 case Email::class:
21944 return new TypeGuess(EmailType::class, [], Guess::HIGH_CONFIDENCE);
21945
21946 case File::class:
21947 case Image::class:
21948 $options = [];
21949 if ($constraint->mimeTypes) {
21950 $options = ['attr' => ['accept' => implode(',', (array) $constraint->mimeTypes)]];
21951 }
21952
21953 return new TypeGuess(FileType::class, $options, Guess::HIGH_CONFIDENCE);
21954
21955 case Language::class:
21956 return new TypeGuess(LanguageType::class, [], Guess::HIGH_CONFIDENCE);
21957
21958 case Locale::class:
21959 return new TypeGuess(LocaleType::class, [], Guess::HIGH_CONFIDENCE);
21960
21961 case Time::class:
21962 return new TypeGuess(TimeType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE);
21963
21964 case Url::class:
21965 return new TypeGuess(UrlType::class, [], Guess::HIGH_CONFIDENCE);
21966
21967 case Ip::class:
21968 return new TypeGuess(TextType::class, [], Guess::MEDIUM_CONFIDENCE);
21969
21970 case Length::class:
21971 case Regex::class:
21972 return new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE);
21973
21974 case Range::class:
21975 return new TypeGuess(NumberType::class, [], Guess::LOW_CONFIDENCE);
21976
21977 case Count::class:
21978 return new TypeGuess(CollectionType::class, [], Guess::LOW_CONFIDENCE);
21979
21980 case IsTrue::class:
21981 case IsFalse::class:
21982 return new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE);
21983 }
21984
21985 return null;
21986 }
21987
21988 /**
21989 * Guesses whether a field is required based on the given constraint.
21990 */
21991 public function guessRequiredForConstraint(Constraint $constraint): ?ValueGuess
21992 {
21993 return match ($constraint::class) {
21994 NotNull::class,
21995 NotBlank::class,
21996 IsTrue::class => new ValueGuess(true, Guess::HIGH_CONFIDENCE),
21997 default => null,
21998 };
21999 }
22000
22001 /**
22002 * Guesses a field's maximum length based on the given constraint.
22003 */
22004 public function guessMaxLengthForConstraint(Constraint $constraint): ?ValueGuess
22005 {
22006 switch ($constraint::class) {
22007 case Length::class:
22008 if (is_numeric($constraint->max)) {
22009 return new ValueGuess($constraint->max, Guess::HIGH_CONFIDENCE);
22010 }
22011 break;
22012
22013 case Type::class:
22014 if (\in_array($constraint->type, ['double', 'float', 'numeric', 'real'])) {
22015 return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
22016 }
22017 break;
22018
22019 case Range::class:
22020 if (is_numeric($constraint->max)) {
22021 return new ValueGuess(\strlen((string) $constraint->max), Guess::LOW_CONFIDENCE);
22022 }
22023 break;
22024 }
22025
22026 return null;
22027 }
22028
22029 /**
22030 * Guesses a field's pattern based on the given constraint.
22031 */
22032 public function guessPatternForConstraint(Constraint $constraint): ?ValueGuess
22033 {
22034 switch ($constraint::class) {
22035 case Length::class:
22036 if (is_numeric($constraint->min)) {
22037 return new ValueGuess(\sprintf('.{%s,}', (string) $constraint->min), Guess::LOW_CONFIDENCE);
22038 }
22039 break;
22040
22041 case Regex::class:
22042 $htmlPattern = $constraint->getHtmlPattern();
22043
22044 if (null !== $htmlPattern) {
22045 return new ValueGuess($htmlPattern, Guess::HIGH_CONFIDENCE);
22046 }
22047 break;
22048
22049 case Range::class:
22050 if (is_numeric($constraint->min)) {
22051 return new ValueGuess(\sprintf('.{%s,}', \strlen((string) $constraint->min)), Guess::LOW_CONFIDENCE);
22052 }
22053 break;
22054
22055 case Type::class:
22056 if (\in_array($constraint->type, ['double', 'float', 'numeric', 'real'])) {
22057 return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
22058 }
22059 break;
22060 }
22061
22062 return null;
22063 }
22064
22065 /**
22066 * Iterates over the constraints of a property, executes a constraints on
22067 * them and returns the best guess.
22068 *
22069 * @param \Closure $closure The closure that returns a guess
22070 * for a given constraint
22071 * @param mixed $defaultValue The default value assumed if no other value
22072 * can be guessed
22073 */
22074 protected function guess(string $class, string $property, \Closure $closure, mixed $defaultValue = null): ?Guess
22075 {
22076 $guesses = [];
22077 $classMetadata = $this->metadataFactory->getMetadataFor($class);
22078
22079 if ($classMetadata instanceof ClassMetadataInterface && $classMetadata->hasPropertyMetadata($property)) {
22080 foreach ($classMetadata->getPropertyMetadata($property) as $memberMetadata) {
22081 foreach ($memberMetadata->getConstraints() as $constraint) {
22082 if ($guess = $closure($constraint)) {
22083 $guesses[] = $guess;
22084 }
22085 }
22086 }
22087 }
22088
22089 if (null !== $defaultValue) {
22090 $guesses[] = new ValueGuess($defaultValue, Guess::LOW_CONFIDENCE);
22091 }
22092
22093 return Guess::getBestGuess($guesses);
22094 }
22095}
22096
22097------------------------------------------------------------------------------------------------------------------------
22098 ./Extension/Validator/ValidatorExtension.php
22099------------------------------------------------------------------------------------------------------------------------
22100<?php
22101
22102/*
22103 * This file is part of the Symfony package.
22104 *
22105 * (c) Fabien Potencier <fabien@symfony.com>
22106 *
22107 * For the full copyright and license information, please view the LICENSE
22108 * file that was distributed with this source code.
22109 */
22110
22111namespace Symfony\Component\Form\Extension\Validator;
22112
22113use Symfony\Component\Form\AbstractExtension;
22114use Symfony\Component\Form\Extension\Validator\Constraints\Form;
22115use Symfony\Component\Form\FormRendererInterface;
22116use Symfony\Component\Form\FormTypeGuesserInterface;
22117use Symfony\Component\Validator\Constraints\Traverse;
22118use Symfony\Component\Validator\Mapping\ClassMetadata;
22119use Symfony\Component\Validator\Validator\ValidatorInterface;
22120use Symfony\Contracts\Translation\TranslatorInterface;
22121
22122/**
22123 * Extension supporting the Symfony Validator component in forms.
22124 *
22125 * @author Bernhard Schussek <bschussek@gmail.com>
22126 */
22127class ValidatorExtension extends AbstractExtension
22128{
22129 public function __construct(
22130 private ValidatorInterface $validator,
22131 private bool $legacyErrorMessages = true,
22132 private ?FormRendererInterface $formRenderer = null,
22133 private ?TranslatorInterface $translator = null,
22134 ) {
22135 $metadata = $validator->getMetadataFor(\Symfony\Component\Form\Form::class);
22136
22137 // Register the form constraints in the validator programmatically.
22138 // This functionality is required when using the Form component without
22139 // the DIC, where the XML file is loaded automatically. Thus the following
22140 // code must be kept synchronized with validation.xml
22141
22142 /* @var ClassMetadata $metadata */
22143 $metadata->addConstraint(new Form());
22144 $metadata->addConstraint(new Traverse(false));
22145 }
22146
22147 public function loadTypeGuesser(): ?FormTypeGuesserInterface
22148 {
22149 return new ValidatorTypeGuesser($this->validator);
22150 }
22151
22152 protected function loadTypeExtensions(): array
22153 {
22154 return [
22155 new Type\FormTypeValidatorExtension($this->validator, $this->legacyErrorMessages, $this->formRenderer, $this->translator),
22156 new Type\RepeatedTypeValidatorExtension(),
22157 new Type\SubmitTypeValidatorExtension(),
22158 ];
22159 }
22160}
22161
22162------------------------------------------------------------------------------------------------------------------------
22163 ./Extension/Validator/ViolationMapper/ViolationPath.php
22164------------------------------------------------------------------------------------------------------------------------
22165<?php
22166
22167/*
22168 * This file is part of the Symfony package.
22169 *
22170 * (c) Fabien Potencier <fabien@symfony.com>
22171 *
22172 * For the full copyright and license information, please view the LICENSE
22173 * file that was distributed with this source code.
22174 */
22175
22176namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
22177
22178use Symfony\Component\Form\Exception\OutOfBoundsException;
22179use Symfony\Component\PropertyAccess\PropertyPath;
22180use Symfony\Component\PropertyAccess\PropertyPathInterface;
22181
22182/**
22183 * @author Bernhard Schussek <bschussek@gmail.com>
22184 *
22185 * @implements \IteratorAggregate<int, string>
22186 */
22187class ViolationPath implements \IteratorAggregate, PropertyPathInterface
22188{
22189 /** @var list<string> */
22190 private array $elements = [];
22191 private array $isIndex = [];
22192 private array $mapsForm = [];
22193 private string $pathAsString = '';
22194 private int $length = 0;
22195
22196 /**
22197 * Creates a new violation path from a string.
22198 *
22199 * @param string $violationPath The property path of a {@link \Symfony\Component\Validator\ConstraintViolation} object
22200 */
22201 public function __construct(string $violationPath)
22202 {
22203 $path = new PropertyPath($violationPath);
22204 $elements = $path->getElements();
22205 $data = false;
22206
22207 for ($i = 0, $l = \count($elements); $i < $l; ++$i) {
22208 if (!$data) {
22209 // The element "data" has not yet been passed
22210 if ('children' === $elements[$i] && $path->isProperty($i)) {
22211 // Skip element "children"
22212 ++$i;
22213
22214 // Next element must exist and must be an index
22215 // Otherwise consider this the end of the path
22216 if ($i >= $l || !$path->isIndex($i)) {
22217 break;
22218 }
22219
22220 // All the following index items (regardless if .children is
22221 // explicitly used) are children and grand-children
22222 for (; $i < $l && $path->isIndex($i); ++$i) {
22223 $this->elements[] = $elements[$i];
22224 $this->isIndex[] = true;
22225 $this->mapsForm[] = true;
22226 }
22227
22228 // Rewind the pointer as the last element above didn't match
22229 // (even if the pointer was moved forward)
22230 --$i;
22231 } elseif ('data' === $elements[$i] && $path->isProperty($i)) {
22232 // Skip element "data"
22233 ++$i;
22234
22235 // End of path
22236 if ($i >= $l) {
22237 break;
22238 }
22239
22240 $this->elements[] = $elements[$i];
22241 $this->isIndex[] = $path->isIndex($i);
22242 $this->mapsForm[] = false;
22243 $data = true;
22244 } else {
22245 // Neither "children" nor "data" property found
22246 // Consider this the end of the path
22247 break;
22248 }
22249 } else {
22250 // Already after the "data" element
22251 // Pick everything as is
22252 $this->elements[] = $elements[$i];
22253 $this->isIndex[] = $path->isIndex($i);
22254 $this->mapsForm[] = false;
22255 }
22256 }
22257
22258 $this->length = \count($this->elements);
22259
22260 $this->buildString();
22261 }
22262
22263 public function __toString(): string
22264 {
22265 return $this->pathAsString;
22266 }
22267
22268 public function getLength(): int
22269 {
22270 return $this->length;
22271 }
22272
22273 public function getParent(): ?PropertyPathInterface
22274 {
22275 if ($this->length <= 1) {
22276 return null;
22277 }
22278
22279 $parent = clone $this;
22280
22281 --$parent->length;
22282 array_pop($parent->elements);
22283 array_pop($parent->isIndex);
22284 array_pop($parent->mapsForm);
22285
22286 $parent->buildString();
22287
22288 return $parent;
22289 }
22290
22291 public function getElements(): array
22292 {
22293 return $this->elements;
22294 }
22295
22296 public function getElement(int $index): string
22297 {
22298 if (!isset($this->elements[$index])) {
22299 throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index));
22300 }
22301
22302 return $this->elements[$index];
22303 }
22304
22305 public function isProperty(int $index): bool
22306 {
22307 if (!isset($this->isIndex[$index])) {
22308 throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index));
22309 }
22310
22311 return !$this->isIndex[$index];
22312 }
22313
22314 public function isIndex(int $index): bool
22315 {
22316 if (!isset($this->isIndex[$index])) {
22317 throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index));
22318 }
22319
22320 return $this->isIndex[$index];
22321 }
22322
22323 public function isNullSafe(int $index): bool
22324 {
22325 return false;
22326 }
22327
22328 /**
22329 * Returns whether an element maps directly to a form.
22330 *
22331 * Consider the following violation path:
22332 *
22333 * children[address].children[office].data.street
22334 *
22335 * In this example, "address" and "office" map to forms, while
22336 * "street does not.
22337 *
22338 * @throws OutOfBoundsException if the offset is invalid
22339 */
22340 public function mapsForm(int $index): bool
22341 {
22342 if (!isset($this->mapsForm[$index])) {
22343 throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index));
22344 }
22345
22346 return $this->mapsForm[$index];
22347 }
22348
22349 /**
22350 * Returns a new iterator for this path.
22351 */
22352 public function getIterator(): ViolationPathIterator
22353 {
22354 return new ViolationPathIterator($this);
22355 }
22356
22357 /**
22358 * Builds the string representation from the elements.
22359 */
22360 private function buildString(): void
22361 {
22362 $this->pathAsString = '';
22363 $data = false;
22364
22365 foreach ($this->elements as $index => $element) {
22366 if ($this->mapsForm[$index]) {
22367 $this->pathAsString .= ".children[$element]";
22368 } elseif (!$data) {
22369 $this->pathAsString .= '.data'.($this->isIndex[$index] ? "[$element]" : ".$element");
22370 $data = true;
22371 } else {
22372 $this->pathAsString .= $this->isIndex[$index] ? "[$element]" : ".$element";
22373 }
22374 }
22375
22376 if ('' !== $this->pathAsString) {
22377 // remove leading dot
22378 $this->pathAsString = substr($this->pathAsString, 1);
22379 }
22380 }
22381}
22382
22383------------------------------------------------------------------------------------------------------------------------
22384 ./Extension/Validator/ViolationMapper/ViolationPathIterator.php
22385------------------------------------------------------------------------------------------------------------------------
22386<?php
22387
22388/*
22389 * This file is part of the Symfony package.
22390 *
22391 * (c) Fabien Potencier <fabien@symfony.com>
22392 *
22393 * For the full copyright and license information, please view the LICENSE
22394 * file that was distributed with this source code.
22395 */
22396
22397namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
22398
22399use Symfony\Component\PropertyAccess\PropertyPathIterator;
22400
22401/**
22402 * @author Bernhard Schussek <bschussek@gmail.com>
22403 */
22404class ViolationPathIterator extends PropertyPathIterator
22405{
22406 public function __construct(ViolationPath $violationPath)
22407 {
22408 parent::__construct($violationPath);
22409 }
22410
22411 public function mapsForm(): bool
22412 {
22413 return $this->path->mapsForm($this->key());
22414 }
22415}
22416
22417------------------------------------------------------------------------------------------------------------------------
22418 ./Extension/Validator/ViolationMapper/MappingRule.php
22419------------------------------------------------------------------------------------------------------------------------
22420<?php
22421
22422/*
22423 * This file is part of the Symfony package.
22424 *
22425 * (c) Fabien Potencier <fabien@symfony.com>
22426 *
22427 * For the full copyright and license information, please view the LICENSE
22428 * file that was distributed with this source code.
22429 */
22430
22431namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
22432
22433use Symfony\Component\Form\Exception\ErrorMappingException;
22434use Symfony\Component\Form\FormInterface;
22435
22436/**
22437 * @author Bernhard Schussek <bschussek@gmail.com>
22438 */
22439class MappingRule
22440{
22441 public function __construct(
22442 private FormInterface $origin,
22443 private string $propertyPath,
22444 private string $targetPath,
22445 ) {
22446 }
22447
22448 public function getOrigin(): FormInterface
22449 {
22450 return $this->origin;
22451 }
22452
22453 /**
22454 * Matches a property path against the rule path.
22455 *
22456 * If the rule matches, the form mapped by the rule is returned.
22457 * Otherwise this method returns false.
22458 */
22459 public function match(string $propertyPath): ?FormInterface
22460 {
22461 return $propertyPath === $this->propertyPath ? $this->getTarget() : null;
22462 }
22463
22464 /**
22465 * Matches a property path against a prefix of the rule path.
22466 */
22467 public function isPrefix(string $propertyPath): bool
22468 {
22469 $length = \strlen($propertyPath);
22470 $prefix = substr($this->propertyPath, 0, $length);
22471 $next = $this->propertyPath[$length] ?? null;
22472
22473 return $prefix === $propertyPath && ('[' === $next || '.' === $next);
22474 }
22475
22476 /**
22477 * @throws ErrorMappingException
22478 */
22479 public function getTarget(): FormInterface
22480 {
22481 $childNames = explode('.', $this->targetPath);
22482 $target = $this->origin;
22483
22484 foreach ($childNames as $childName) {
22485 if (!$target->has($childName)) {
22486 throw new ErrorMappingException(\sprintf('The child "%s" of "%s" mapped by the rule "%s" in "%s" does not exist.', $childName, $target->getName(), $this->targetPath, $this->origin->getName()));
22487 }
22488 $target = $target->get($childName);
22489 }
22490
22491 return $target;
22492 }
22493}
22494
22495------------------------------------------------------------------------------------------------------------------------
22496 ./Extension/Validator/ViolationMapper/RelativePath.php
22497------------------------------------------------------------------------------------------------------------------------
22498<?php
22499
22500/*
22501 * This file is part of the Symfony package.
22502 *
22503 * (c) Fabien Potencier <fabien@symfony.com>
22504 *
22505 * For the full copyright and license information, please view the LICENSE
22506 * file that was distributed with this source code.
22507 */
22508
22509namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
22510
22511use Symfony\Component\Form\FormInterface;
22512use Symfony\Component\PropertyAccess\PropertyPath;
22513
22514/**
22515 * @author Bernhard Schussek <bschussek@gmail.com>
22516 */
22517class RelativePath extends PropertyPath
22518{
22519 public function __construct(
22520 private FormInterface $root,
22521 string $propertyPath,
22522 ) {
22523 parent::__construct($propertyPath);
22524 }
22525
22526 public function getRoot(): FormInterface
22527 {
22528 return $this->root;
22529 }
22530}
22531
22532------------------------------------------------------------------------------------------------------------------------
22533 ./Extension/Validator/ViolationMapper/ViolationMapperInterface.php
22534------------------------------------------------------------------------------------------------------------------------
22535<?php
22536
22537/*
22538 * This file is part of the Symfony package.
22539 *
22540 * (c) Fabien Potencier <fabien@symfony.com>
22541 *
22542 * For the full copyright and license information, please view the LICENSE
22543 * file that was distributed with this source code.
22544 */
22545
22546namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
22547
22548use Symfony\Component\Form\FormInterface;
22549use Symfony\Component\Validator\ConstraintViolation;
22550
22551/**
22552 * @author Bernhard Schussek <bschussek@gmail.com>
22553 */
22554interface ViolationMapperInterface
22555{
22556 /**
22557 * Maps a constraint violation to a form in the form tree under
22558 * the given form.
22559 *
22560 * @param bool $allowNonSynchronized Whether to allow mapping to non-synchronized forms
22561 */
22562 public function mapViolation(ConstraintViolation $violation, FormInterface $form, bool $allowNonSynchronized = false): void;
22563}
22564
22565------------------------------------------------------------------------------------------------------------------------
22566 ./Extension/Validator/ViolationMapper/ViolationMapper.php
22567------------------------------------------------------------------------------------------------------------------------
22568<?php
22569
22570/*
22571 * This file is part of the Symfony package.
22572 *
22573 * (c) Fabien Potencier <fabien@symfony.com>
22574 *
22575 * For the full copyright and license information, please view the LICENSE
22576 * file that was distributed with this source code.
22577 */
22578
22579namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
22580
22581use Symfony\Component\Form\FileUploadError;
22582use Symfony\Component\Form\FormError;
22583use Symfony\Component\Form\FormInterface;
22584use Symfony\Component\Form\FormRendererInterface;
22585use Symfony\Component\Form\Util\InheritDataAwareIterator;
22586use Symfony\Component\PropertyAccess\PropertyPathBuilder;
22587use Symfony\Component\PropertyAccess\PropertyPathIterator;
22588use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
22589use Symfony\Component\Validator\Constraints\File;
22590use Symfony\Component\Validator\ConstraintViolation;
22591use Symfony\Contracts\Translation\TranslatorInterface;
22592
22593/**
22594 * @author Bernhard Schussek <bschussek@gmail.com>
22595 */
22596class ViolationMapper implements ViolationMapperInterface
22597{
22598 private bool $allowNonSynchronized = false;
22599
22600 public function __construct(
22601 private ?FormRendererInterface $formRenderer = null,
22602 private ?TranslatorInterface $translator = null,
22603 ) {
22604 }
22605
22606 public function mapViolation(ConstraintViolation $violation, FormInterface $form, bool $allowNonSynchronized = false): void
22607 {
22608 $this->allowNonSynchronized = $allowNonSynchronized;
22609
22610 // The scope is the currently found most specific form that
22611 // an error should be mapped to. After setting the scope, the
22612 // mapper will try to continue to find more specific matches in
22613 // the children of scope. If it cannot, the error will be
22614 // mapped to this scope.
22615 $scope = null;
22616
22617 $violationPath = null;
22618 $relativePath = null;
22619 $match = false;
22620
22621 // Don't create a ViolationPath instance for empty property paths
22622 if ('' !== $violation->getPropertyPath()) {
22623 $violationPath = new ViolationPath($violation->getPropertyPath());
22624 $relativePath = $this->reconstructPath($violationPath, $form);
22625 }
22626
22627 // This case happens if the violation path is empty and thus
22628 // the violation should be mapped to the root form
22629 if (null === $violationPath) {
22630 $scope = $form;
22631 }
22632
22633 // In general, mapping happens from the root form to the leaf forms
22634 // First, the rules of the root form are applied to determine
22635 // the subsequent descendant. The rules of this descendant are then
22636 // applied to find the next and so on, until we have found the
22637 // most specific form that matches the violation.
22638
22639 // If any of the forms found in this process is not synchronized,
22640 // mapping is aborted. Non-synchronized forms could not reverse
22641 // transform the value entered by the user, thus any further violations
22642 // caused by the (invalid) reverse transformed value should be
22643 // ignored.
22644
22645 if (null !== $relativePath) {
22646 // Set the scope to the root of the relative path
22647 // This root will usually be $form. If the path contains
22648 // an unmapped form though, the last unmapped form found
22649 // will be the root of the path.
22650 $scope = $relativePath->getRoot();
22651 $it = new PropertyPathIterator($relativePath);
22652
22653 while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) {
22654 $scope = $child;
22655 $it->next();
22656 $match = true;
22657 }
22658 }
22659
22660 // This case happens if an error happened in the data under a
22661 // form inheriting its parent data that does not match any of the
22662 // children of that form.
22663 if (null !== $violationPath && !$match) {
22664 // If we could not map the error to anything more specific
22665 // than the root element, map it to the innermost directly
22666 // mapped form of the violation path
22667 // e.g. "children[foo].children[bar].data.baz"
22668 // Here the innermost directly mapped child is "bar"
22669
22670 $scope = $form;
22671 $it = new ViolationPathIterator($violationPath);
22672
22673 // Note: acceptsErrors() will always return true for forms inheriting
22674 // their parent data, because these forms can never be non-synchronized
22675 // (they don't do any data transformation on their own)
22676 while ($this->acceptsErrors($scope) && $it->valid() && $it->mapsForm()) {
22677 if (!$scope->has($it->current())) {
22678 // Break if we find a reference to a non-existing child
22679 break;
22680 }
22681
22682 $scope = $scope->get($it->current());
22683 $it->next();
22684 }
22685 }
22686
22687 // Follow dot rules until we have the final target
22688 $mapping = $scope->getConfig()->getOption('error_mapping');
22689
22690 while ($this->acceptsErrors($scope) && isset($mapping['.'])) {
22691 $dotRule = new MappingRule($scope, '.', $mapping['.']);
22692 $scope = $dotRule->getTarget();
22693 $mapping = $scope->getConfig()->getOption('error_mapping');
22694 }
22695
22696 // Only add the error if the form is synchronized
22697 if ($this->acceptsErrors($scope)) {
22698 if ($violation->getConstraint() instanceof File && (string) \UPLOAD_ERR_INI_SIZE === $violation->getCode()) {
22699 $errorsTarget = $scope;
22700
22701 while (null !== $errorsTarget->getParent() && $errorsTarget->getConfig()->getErrorBubbling()) {
22702 $errorsTarget = $errorsTarget->getParent();
22703 }
22704
22705 $errors = $errorsTarget->getErrors();
22706 $errorsTarget->clearErrors();
22707
22708 foreach ($errors as $error) {
22709 if (!$error instanceof FileUploadError) {
22710 $errorsTarget->addError($error);
22711 }
22712 }
22713 }
22714
22715 $message = $violation->getMessage();
22716 $messageTemplate = $violation->getMessageTemplate();
22717
22718 if (str_contains($message, '{{ label }}') || str_contains($messageTemplate, '{{ label }}')) {
22719 $form = $scope;
22720
22721 do {
22722 $labelFormat = $form->getConfig()->getOption('label_format');
22723 } while (null === $labelFormat && null !== $form = $form->getParent());
22724
22725 if (null !== $labelFormat) {
22726 $label = str_replace(
22727 [
22728 '%name%',
22729 '%id%',
22730 ],
22731 [
22732 $scope->getName(),
22733 (string) $scope->getPropertyPath(),
22734 ],
22735 $labelFormat
22736 );
22737 } else {
22738 $label = $scope->getConfig()->getOption('label');
22739 }
22740
22741 if (false !== $label) {
22742 if (null === $label && null !== $this->formRenderer) {
22743 $label = $this->formRenderer->humanize($scope->getName());
22744 } else {
22745 $label ??= $scope->getName();
22746 }
22747
22748 if (null !== $this->translator) {
22749 $form = $scope;
22750 $translationParameters[] = $form->getConfig()->getOption('label_translation_parameters', []);
22751
22752 do {
22753 $translationDomain = $form->getConfig()->getOption('translation_domain');
22754 array_unshift(
22755 $translationParameters,
22756 $form->getConfig()->getOption('label_translation_parameters', [])
22757 );
22758 } while (null === $translationDomain && null !== $form = $form->getParent());
22759
22760 $translationParameters = array_merge([], ...$translationParameters);
22761
22762 $label = $this->translator->trans(
22763 $label,
22764 $translationParameters,
22765 $translationDomain
22766 );
22767 }
22768
22769 $message = str_replace('{{ label }}', $label, $message);
22770 $messageTemplate = str_replace('{{ label }}', $label, $messageTemplate);
22771 }
22772 }
22773
22774 $scope->addError(new FormError(
22775 $message,
22776 $messageTemplate,
22777 $violation->getParameters(),
22778 $violation->getPlural(),
22779 $violation
22780 ));
22781 }
22782 }
22783
22784 /**
22785 * Tries to match the beginning of the property path at the
22786 * current position against the children of the scope.
22787 *
22788 * If a matching child is found, it is returned. Otherwise
22789 * null is returned.
22790 */
22791 private function matchChild(FormInterface $form, PropertyPathIteratorInterface $it): ?FormInterface
22792 {
22793 $target = null;
22794 $chunk = '';
22795 $foundAtIndex = null;
22796
22797 // Construct mapping rules for the given form
22798 $rules = [];
22799
22800 foreach ($form->getConfig()->getOption('error_mapping') as $propertyPath => $targetPath) {
22801 // Dot rules are considered at the very end
22802 if ('.' !== $propertyPath) {
22803 $rules[] = new MappingRule($form, $propertyPath, $targetPath);
22804 }
22805 }
22806
22807 $children = iterator_to_array(new \RecursiveIteratorIterator(new InheritDataAwareIterator($form)), false);
22808
22809 while ($it->valid()) {
22810 if ($it->isIndex()) {
22811 $chunk .= '['.$it->current().']';
22812 } else {
22813 $chunk .= ('' === $chunk ? '' : '.').$it->current();
22814 }
22815
22816 // Test mapping rules as long as we have any
22817 foreach ($rules as $key => $rule) {
22818 /* @var MappingRule $rule */
22819
22820 // Mapping rule matches completely, terminate.
22821 if (null !== ($form = $rule->match($chunk))) {
22822 return $form;
22823 }
22824
22825 // Keep only rules that have $chunk as prefix
22826 if (!$rule->isPrefix($chunk)) {
22827 unset($rules[$key]);
22828 }
22829 }
22830
22831 /** @var FormInterface $child */
22832 foreach ($children as $i => $child) {
22833 $childPath = (string) $child->getPropertyPath();
22834 if ($childPath === $chunk) {
22835 $target = $child;
22836 $foundAtIndex = $it->key();
22837 } elseif (str_starts_with($childPath, $chunk)) {
22838 continue;
22839 }
22840
22841 unset($children[$i]);
22842 }
22843
22844 $it->next();
22845 }
22846
22847 if (null !== $foundAtIndex) {
22848 $it->seek($foundAtIndex);
22849 }
22850
22851 return $target;
22852 }
22853
22854 /**
22855 * Reconstructs a property path from a violation path and a form tree.
22856 */
22857 private function reconstructPath(ViolationPath $violationPath, FormInterface $origin): ?RelativePath
22858 {
22859 $propertyPathBuilder = new PropertyPathBuilder($violationPath);
22860 $it = $violationPath->getIterator();
22861 $scope = $origin;
22862
22863 // Remember the current index in the builder
22864 $i = 0;
22865
22866 // Expand elements that map to a form (like "children[address]")
22867 for ($it->rewind(); $it->valid() && $it->mapsForm(); $it->next()) {
22868 if (!$scope->has($it->current())) {
22869 // Scope relates to a form that does not exist
22870 // Bail out
22871 break;
22872 }
22873
22874 // Process child form
22875 $scope = $scope->get($it->current());
22876
22877 if ($scope->getConfig()->getInheritData()) {
22878 // Form inherits its parent data
22879 // Cut the piece out of the property path and proceed
22880 $propertyPathBuilder->remove($i);
22881 } else {
22882 /* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */
22883 $propertyPath = $scope->getPropertyPath();
22884
22885 if (null === $propertyPath) {
22886 // Property path of a mapped form is null
22887 // Should not happen, bail out
22888 break;
22889 }
22890
22891 $propertyPathBuilder->replace($i, 1, $propertyPath);
22892 $i += $propertyPath->getLength();
22893 }
22894 }
22895
22896 $finalPath = $propertyPathBuilder->getPropertyPath();
22897
22898 return null !== $finalPath ? new RelativePath($origin, $finalPath) : null;
22899 }
22900
22901 private function acceptsErrors(FormInterface $form): bool
22902 {
22903 return $this->allowNonSynchronized || $form->isSynchronized();
22904 }
22905}
22906
22907------------------------------------------------------------------------------------------------------------------------
22908 ./Extension/Validator/Constraints/FormValidator.php
22909------------------------------------------------------------------------------------------------------------------------
22910<?php
22911
22912/*
22913 * This file is part of the Symfony package.
22914 *
22915 * (c) Fabien Potencier <fabien@symfony.com>
22916 *
22917 * For the full copyright and license information, please view the LICENSE
22918 * file that was distributed with this source code.
22919 */
22920
22921namespace Symfony\Component\Form\Extension\Validator\Constraints;
22922
22923use Symfony\Component\Form\FormInterface;
22924use Symfony\Component\Validator\Constraint;
22925use Symfony\Component\Validator\Constraints\Composite;
22926use Symfony\Component\Validator\Constraints\GroupSequence;
22927use Symfony\Component\Validator\Constraints\Valid;
22928use Symfony\Component\Validator\ConstraintValidator;
22929use Symfony\Component\Validator\Exception\UnexpectedTypeException;
22930
22931/**
22932 * @author Bernhard Schussek <bschussek@gmail.com>
22933 */
22934class FormValidator extends ConstraintValidator
22935{
22936 /**
22937 * @var \SplObjectStorage<FormInterface, array<int, string|string[]|GroupSequence>>
22938 */
22939 private \SplObjectStorage $resolvedGroups;
22940
22941 public function validate(mixed $form, Constraint $formConstraint): void
22942 {
22943 if (!$formConstraint instanceof Form) {
22944 throw new UnexpectedTypeException($formConstraint, Form::class);
22945 }
22946
22947 if (!$form instanceof FormInterface) {
22948 return;
22949 }
22950
22951 /* @var FormInterface $form */
22952 $config = $form->getConfig();
22953
22954 $validator = $this->context->getValidator()->inContext($this->context);
22955
22956 if ($form->isSubmitted() && $form->isSynchronized()) {
22957 // Validate the form data only if transformation succeeded
22958 $groups = $this->getValidationGroups($form);
22959
22960 if (!$groups) {
22961 return;
22962 }
22963
22964 $data = $form->getData();
22965 // Validate the data against its own constraints
22966 $validateDataGraph = $form->isRoot()
22967 && (\is_object($data) || \is_array($data))
22968 && (\is_array($groups) || ($groups instanceof GroupSequence && $groups->groups))
22969 ;
22970
22971 // Validate the data against the constraints defined in the form
22972 /** @var Constraint[] $constraints */
22973 $constraints = $config->getOption('constraints', []);
22974
22975 $hasChildren = $form->count() > 0;
22976
22977 if ($hasChildren && $form->isRoot()) {
22978 $this->resolvedGroups = new \SplObjectStorage();
22979 }
22980
22981 if ($groups instanceof GroupSequence) {
22982 // Validate the data, the form AND nested fields in sequence
22983 $violationsCount = $this->context->getViolations()->count();
22984
22985 foreach ($groups->groups as $group) {
22986 if ($validateDataGraph) {
22987 $validator->atPath('data')->validate($data, null, $group);
22988 }
22989
22990 if ($groupedConstraints = self::getConstraintsInGroups($constraints, $group)) {
22991 $validator->atPath('data')->validate($data, $groupedConstraints, $group);
22992 }
22993
22994 foreach ($form->all() as $field) {
22995 if ($field->isSubmitted()) {
22996 // remember to validate this field in one group only
22997 // otherwise resolving the groups would reuse the same
22998 // sequence recursively, thus some fields could fail
22999 // in different steps without breaking early enough
23000 $this->resolvedGroups[$field] = (array) $group;
23001 $fieldFormConstraint = new Form();
23002 $fieldFormConstraint->groups = $group;
23003 $this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath());
23004 $validator->atPath(\sprintf('children[%s]', $field->getName()))->validate($field, $fieldFormConstraint, $group);
23005 }
23006 }
23007
23008 if ($violationsCount < $this->context->getViolations()->count()) {
23009 break;
23010 }
23011 }
23012 } else {
23013 if ($validateDataGraph) {
23014 $validator->atPath('data')->validate($data, null, $groups);
23015 }
23016
23017 $groupedConstraints = [];
23018
23019 foreach ($constraints as $constraint) {
23020 // For the "Valid" constraint, validate the data in all groups
23021 if ($constraint instanceof Valid) {
23022 if (\is_object($data) || \is_array($data)) {
23023 $validator->atPath('data')->validate($data, $constraint, $groups);
23024 }
23025
23026 continue;
23027 }
23028
23029 // Otherwise validate a constraint only once for the first
23030 // matching group
23031 foreach ($groups as $group) {
23032 if (\in_array($group, $constraint->groups, true)) {
23033 $groupedConstraints[$group][] = $constraint;
23034
23035 // Prevent duplicate validation
23036 if (!$constraint instanceof Composite) {
23037 continue 2;
23038 }
23039 }
23040 }
23041 }
23042
23043 foreach ($groupedConstraints as $group => $constraint) {
23044 $validator->atPath('data')->validate($data, $constraint, $group);
23045 }
23046
23047 foreach ($form->all() as $field) {
23048 if ($field->isSubmitted()) {
23049 $this->resolvedGroups[$field] = $groups;
23050 $this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath());
23051 $validator->atPath(\sprintf('children[%s]', $field->getName()))->validate($field, $formConstraint);
23052 }
23053 }
23054 }
23055
23056 if ($hasChildren && $form->isRoot()) {
23057 // destroy storage to avoid memory leaks
23058 $this->resolvedGroups = new \SplObjectStorage();
23059 }
23060 } elseif (!$form->isSynchronized()) {
23061 $childrenSynchronized = true;
23062
23063 /** @var FormInterface $child */
23064 foreach ($form as $child) {
23065 if (!$child->isSynchronized()) {
23066 $childrenSynchronized = false;
23067 $this->context->setNode($this->context->getValue(), $child, $this->context->getMetadata(), $this->context->getPropertyPath());
23068 $validator->atPath(\sprintf('children[%s]', $child->getName()))->validate($child, $formConstraint);
23069 }
23070 }
23071
23072 // Mark the form with an error if it is not synchronized BUT all
23073 // of its children are synchronized. If any child is not
23074 // synchronized, an error is displayed there already and showing
23075 // a second error in its parent form is pointless, or worse, may
23076 // lead to duplicate errors if error bubbling is enabled on the
23077 // child.
23078 // See also https://github.com/symfony/symfony/issues/4359
23079 if ($childrenSynchronized) {
23080 $clientDataAsString = \is_scalar($form->getViewData())
23081 ? (string) $form->getViewData()
23082 : get_debug_type($form->getViewData());
23083
23084 $failure = $form->getTransformationFailure();
23085
23086 $this->context->setConstraint($formConstraint);
23087 $this->context->buildViolation($failure->getInvalidMessage() ?? $config->getOption('invalid_message'))
23088 ->setParameters(array_replace(
23089 ['{{ value }}' => $clientDataAsString],
23090 $config->getOption('invalid_message_parameters'),
23091 $failure->getInvalidMessageParameters()
23092 ))
23093 ->setInvalidValue($form->getViewData())
23094 ->setCode(Form::NOT_SYNCHRONIZED_ERROR)
23095 ->setCause($failure)
23096 ->addViolation();
23097 }
23098 }
23099
23100 // Mark the form with an error if it contains extra fields
23101 if (!$config->getOption('allow_extra_fields') && \count($form->getExtraData()) > 0) {
23102 $this->context->setConstraint($formConstraint);
23103 $this->context->buildViolation($config->getOption('extra_fields_message', ''))
23104 ->setParameter('{{ extra_fields }}', '"'.implode('", "', array_keys($form->getExtraData())).'"')
23105 ->setPlural(\count($form->getExtraData()))
23106 ->setInvalidValue($form->getExtraData())
23107 ->setCode(Form::NO_SUCH_FIELD_ERROR)
23108 ->addViolation();
23109 }
23110 }
23111
23112 /**
23113 * Returns the validation groups of the given form.
23114 *
23115 * @return string|GroupSequence|array<string|GroupSequence>
23116 */
23117 private function getValidationGroups(FormInterface $form): string|GroupSequence|array
23118 {
23119 // Determine the clicked button of the complete form tree
23120 $clickedButton = null;
23121
23122 if (method_exists($form, 'getClickedButton')) {
23123 $clickedButton = $form->getClickedButton();
23124 }
23125
23126 if (null !== $clickedButton) {
23127 $groups = $clickedButton->getConfig()->getOption('validation_groups');
23128
23129 if (null !== $groups) {
23130 return self::resolveValidationGroups($groups, $form);
23131 }
23132 }
23133
23134 do {
23135 $groups = $form->getConfig()->getOption('validation_groups');
23136
23137 if (null !== $groups) {
23138 return self::resolveValidationGroups($groups, $form);
23139 }
23140
23141 if (isset($this->resolvedGroups[$form])) {
23142 return $this->resolvedGroups[$form];
23143 }
23144
23145 $form = $form->getParent();
23146 } while (null !== $form);
23147
23148 return [Constraint::DEFAULT_GROUP];
23149 }
23150
23151 /**
23152 * Post-processes the validation groups option for a given form.
23153 *
23154 * @param string|GroupSequence|array<string|GroupSequence>|callable $groups The validation groups
23155 *
23156 * @return GroupSequence|array<string|GroupSequence>
23157 */
23158 private static function resolveValidationGroups(string|GroupSequence|array|callable $groups, FormInterface $form): GroupSequence|array
23159 {
23160 if (!\is_string($groups) && \is_callable($groups)) {
23161 $groups = $groups($form);
23162 }
23163
23164 if ($groups instanceof GroupSequence) {
23165 return $groups;
23166 }
23167
23168 return (array) $groups;
23169 }
23170
23171 private static function getConstraintsInGroups(array $constraints, string|array $group): array
23172 {
23173 $groups = (array) $group;
23174
23175 return array_filter($constraints, static function (Constraint $constraint) use ($groups) {
23176 foreach ($groups as $group) {
23177 if (\in_array($group, $constraint->groups, true)) {
23178 return true;
23179 }
23180 }
23181
23182 return false;
23183 });
23184 }
23185}
23186
23187------------------------------------------------------------------------------------------------------------------------
23188 ./Extension/Validator/Constraints/Form.php
23189------------------------------------------------------------------------------------------------------------------------
23190<?php
23191
23192/*
23193 * This file is part of the Symfony package.
23194 *
23195 * (c) Fabien Potencier <fabien@symfony.com>
23196 *
23197 * For the full copyright and license information, please view the LICENSE
23198 * file that was distributed with this source code.
23199 */
23200
23201namespace Symfony\Component\Form\Extension\Validator\Constraints;
23202
23203use Symfony\Component\Validator\Constraint;
23204
23205/**
23206 * @author Bernhard Schussek <bschussek@gmail.com>
23207 */
23208class Form extends Constraint
23209{
23210 public const NOT_SYNCHRONIZED_ERROR = '1dafa156-89e1-4736-b832-419c2e501fca';
23211 public const NO_SUCH_FIELD_ERROR = '6e5212ed-a197-4339-99aa-5654798a4854';
23212
23213 protected const ERROR_NAMES = [
23214 self::NOT_SYNCHRONIZED_ERROR => 'NOT_SYNCHRONIZED_ERROR',
23215 self::NO_SUCH_FIELD_ERROR => 'NO_SUCH_FIELD_ERROR',
23216 ];
23217
23218 public function getTargets(): string|array
23219 {
23220 return self::CLASS_CONSTRAINT;
23221 }
23222}
23223
23224------------------------------------------------------------------------------------------------------------------------
23225 ./Extension/Validator/EventListener/ValidationListener.php
23226------------------------------------------------------------------------------------------------------------------------
23227<?php
23228
23229/*
23230 * This file is part of the Symfony package.
23231 *
23232 * (c) Fabien Potencier <fabien@symfony.com>
23233 *
23234 * For the full copyright and license information, please view the LICENSE
23235 * file that was distributed with this source code.
23236 */
23237
23238namespace Symfony\Component\Form\Extension\Validator\EventListener;
23239
23240use Symfony\Component\EventDispatcher\EventSubscriberInterface;
23241use Symfony\Component\Form\Extension\Validator\Constraints\Form;
23242use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface;
23243use Symfony\Component\Form\FormEvent;
23244use Symfony\Component\Form\FormEvents;
23245use Symfony\Component\Validator\Validator\ValidatorInterface;
23246
23247/**
23248 * @author Bernhard Schussek <bschussek@gmail.com>
23249 */
23250class ValidationListener implements EventSubscriberInterface
23251{
23252 public static function getSubscribedEvents(): array
23253 {
23254 return [FormEvents::POST_SUBMIT => 'validateForm'];
23255 }
23256
23257 public function __construct(
23258 private ValidatorInterface $validator,
23259 private ViolationMapperInterface $violationMapper,
23260 ) {
23261 }
23262
23263 public function validateForm(FormEvent $event): void
23264 {
23265 $form = $event->getForm();
23266
23267 if ($form->isRoot()) {
23268 // Form groups are validated internally (FormValidator). Here we don't set groups as they are retrieved into the validator.
23269 foreach ($this->validator->validate($form) as $violation) {
23270 // Allow the "invalid" constraint to be put onto
23271 // non-synchronized forms
23272 $allowNonSynchronized = $violation->getConstraint() instanceof Form && Form::NOT_SYNCHRONIZED_ERROR === $violation->getCode();
23273
23274 $this->violationMapper->mapViolation($violation, $form, $allowNonSynchronized);
23275 }
23276 }
23277 }
23278}
23279
23280------------------------------------------------------------------------------------------------------------------------
23281 ./Extension/Validator/Type/SubmitTypeValidatorExtension.php
23282------------------------------------------------------------------------------------------------------------------------
23283<?php
23284
23285/*
23286 * This file is part of the Symfony package.
23287 *
23288 * (c) Fabien Potencier <fabien@symfony.com>
23289 *
23290 * For the full copyright and license information, please view the LICENSE
23291 * file that was distributed with this source code.
23292 */
23293
23294namespace Symfony\Component\Form\Extension\Validator\Type;
23295
23296use Symfony\Component\Form\Extension\Core\Type\SubmitType;
23297
23298/**
23299 * @author Bernhard Schussek <bschussek@gmail.com>
23300 */
23301class SubmitTypeValidatorExtension extends BaseValidatorExtension
23302{
23303 public static function getExtendedTypes(): iterable
23304 {
23305 return [SubmitType::class];
23306 }
23307}
23308
23309------------------------------------------------------------------------------------------------------------------------
23310 ./Extension/Validator/Type/FormTypeValidatorExtension.php
23311------------------------------------------------------------------------------------------------------------------------
23312<?php
23313
23314/*
23315 * This file is part of the Symfony package.
23316 *
23317 * (c) Fabien Potencier <fabien@symfony.com>
23318 *
23319 * For the full copyright and license information, please view the LICENSE
23320 * file that was distributed with this source code.
23321 */
23322
23323namespace Symfony\Component\Form\Extension\Validator\Type;
23324
23325use Symfony\Component\Form\Extension\Core\Type\FormType;
23326use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
23327use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
23328use Symfony\Component\Form\FormBuilderInterface;
23329use Symfony\Component\Form\FormRendererInterface;
23330use Symfony\Component\OptionsResolver\Options;
23331use Symfony\Component\OptionsResolver\OptionsResolver;
23332use Symfony\Component\Validator\Constraint;
23333use Symfony\Component\Validator\Validator\ValidatorInterface;
23334use Symfony\Contracts\Translation\TranslatorInterface;
23335
23336/**
23337 * @author Bernhard Schussek <bschussek@gmail.com>
23338 */
23339class FormTypeValidatorExtension extends BaseValidatorExtension
23340{
23341 private ViolationMapper $violationMapper;
23342
23343 public function __construct(
23344 private ValidatorInterface $validator,
23345 private bool $legacyErrorMessages = true,
23346 ?FormRendererInterface $formRenderer = null,
23347 ?TranslatorInterface $translator = null,
23348 ) {
23349 $this->violationMapper = new ViolationMapper($formRenderer, $translator);
23350 }
23351
23352 public function buildForm(FormBuilderInterface $builder, array $options): void
23353 {
23354 $builder->addEventSubscriber(new ValidationListener($this->validator, $this->violationMapper));
23355 }
23356
23357 public function configureOptions(OptionsResolver $resolver): void
23358 {
23359 parent::configureOptions($resolver);
23360
23361 // Constraint should always be converted to an array
23362 $constraintsNormalizer = static fn (Options $options, $constraints) => \is_object($constraints) ? [$constraints] : (array) $constraints;
23363
23364 $resolver->setDefaults([
23365 'error_mapping' => [],
23366 'constraints' => [],
23367 'invalid_message' => 'This value is not valid.',
23368 'invalid_message_parameters' => [],
23369 'allow_extra_fields' => false,
23370 'extra_fields_message' => 'This form should not contain extra fields.',
23371 ]);
23372 $resolver->setAllowedTypes('constraints', [Constraint::class, Constraint::class.'[]']);
23373 $resolver->setNormalizer('constraints', $constraintsNormalizer);
23374 }
23375
23376 public static function getExtendedTypes(): iterable
23377 {
23378 return [FormType::class];
23379 }
23380}
23381
23382------------------------------------------------------------------------------------------------------------------------
23383 ./Extension/Validator/Type/BaseValidatorExtension.php
23384------------------------------------------------------------------------------------------------------------------------
23385<?php
23386
23387/*
23388 * This file is part of the Symfony package.
23389 *
23390 * (c) Fabien Potencier <fabien@symfony.com>
23391 *
23392 * For the full copyright and license information, please view the LICENSE
23393 * file that was distributed with this source code.
23394 */
23395
23396namespace Symfony\Component\Form\Extension\Validator\Type;
23397
23398use Symfony\Component\Form\AbstractTypeExtension;
23399use Symfony\Component\OptionsResolver\Options;
23400use Symfony\Component\OptionsResolver\OptionsResolver;
23401use Symfony\Component\Validator\Constraints\GroupSequence;
23402
23403/**
23404 * Encapsulates common logic of {@link FormTypeValidatorExtension} and
23405 * {@link SubmitTypeValidatorExtension}.
23406 *
23407 * @author Bernhard Schussek <bschussek@gmail.com>
23408 */
23409abstract class BaseValidatorExtension extends AbstractTypeExtension
23410{
23411 public function configureOptions(OptionsResolver $resolver): void
23412 {
23413 // Make sure that validation groups end up as null, closure or array
23414 $validationGroupsNormalizer = static function (Options $options, $groups) {
23415 if (false === $groups) {
23416 return [];
23417 }
23418
23419 if (!$groups) {
23420 return null;
23421 }
23422
23423 if (\is_callable($groups)) {
23424 return $groups;
23425 }
23426
23427 if ($groups instanceof GroupSequence) {
23428 return $groups;
23429 }
23430
23431 return (array) $groups;
23432 };
23433
23434 $resolver->setDefaults([
23435 'validation_groups' => null,
23436 ]);
23437
23438 $resolver->setNormalizer('validation_groups', $validationGroupsNormalizer);
23439 }
23440}
23441
23442------------------------------------------------------------------------------------------------------------------------
23443 ./Extension/Validator/Type/RepeatedTypeValidatorExtension.php
23444------------------------------------------------------------------------------------------------------------------------
23445<?php
23446
23447/*
23448 * This file is part of the Symfony package.
23449 *
23450 * (c) Fabien Potencier <fabien@symfony.com>
23451 *
23452 * For the full copyright and license information, please view the LICENSE
23453 * file that was distributed with this source code.
23454 */
23455
23456namespace Symfony\Component\Form\Extension\Validator\Type;
23457
23458use Symfony\Component\Form\AbstractTypeExtension;
23459use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
23460use Symfony\Component\OptionsResolver\Options;
23461use Symfony\Component\OptionsResolver\OptionsResolver;
23462
23463/**
23464 * @author Bernhard Schussek <bschussek@gmail.com>
23465 */
23466class RepeatedTypeValidatorExtension extends AbstractTypeExtension
23467{
23468 public function configureOptions(OptionsResolver $resolver): void
23469 {
23470 // Map errors to the first field
23471 $errorMapping = static fn (Options $options) => ['.' => $options['first_name']];
23472
23473 $resolver->setDefaults([
23474 'error_mapping' => $errorMapping,
23475 ]);
23476 }
23477
23478 public static function getExtendedTypes(): iterable
23479 {
23480 return [RepeatedType::class];
23481 }
23482}
23483
23484------------------------------------------------------------------------------------------------------------------------
23485 ./Extension/Validator/Type/UploadValidatorExtension.php
23486------------------------------------------------------------------------------------------------------------------------
23487<?php
23488
23489/*
23490 * This file is part of the Symfony package.
23491 *
23492 * (c) Fabien Potencier <fabien@symfony.com>
23493 *
23494 * For the full copyright and license information, please view the LICENSE
23495 * file that was distributed with this source code.
23496 */
23497
23498namespace Symfony\Component\Form\Extension\Validator\Type;
23499
23500use Symfony\Component\Form\AbstractTypeExtension;
23501use Symfony\Component\Form\Extension\Core\Type\FormType;
23502use Symfony\Component\OptionsResolver\Options;
23503use Symfony\Component\OptionsResolver\OptionsResolver;
23504use Symfony\Contracts\Translation\TranslatorInterface;
23505
23506/**
23507 * @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
23508 * @author David Badura <d.a.badura@gmail.com>
23509 */
23510class UploadValidatorExtension extends AbstractTypeExtension
23511{
23512 public function __construct(
23513 private TranslatorInterface $translator,
23514 private ?string $translationDomain = null,
23515 ) {
23516 }
23517
23518 public function configureOptions(OptionsResolver $resolver): void
23519 {
23520 $translator = $this->translator;
23521 $translationDomain = $this->translationDomain;
23522 $resolver->setNormalizer('upload_max_size_message', static fn (Options $options, $message) => static fn () => $translator->trans($message(), [], $translationDomain));
23523 }
23524
23525 public static function getExtendedTypes(): iterable
23526 {
23527 return [FormType::class];
23528 }
23529}
23530
23531------------------------------------------------------------------------------------------------------------------------
23532 ./Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtension.php
23533------------------------------------------------------------------------------------------------------------------------
23534<?php
23535
23536/*
23537 * This file is part of the Symfony package.
23538 *
23539 * (c) Fabien Potencier <fabien@symfony.com>
23540 *
23541 * For the full copyright and license information, please view the LICENSE
23542 * file that was distributed with this source code.
23543 */
23544
23545namespace Symfony\Component\Form\Extension\PasswordHasher\Type;
23546
23547use Symfony\Component\Form\AbstractTypeExtension;
23548use Symfony\Component\Form\Extension\Core\Type\PasswordType;
23549use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
23550use Symfony\Component\Form\FormBuilderInterface;
23551use Symfony\Component\Form\FormEvents;
23552use Symfony\Component\OptionsResolver\OptionsResolver;
23553use Symfony\Component\PropertyAccess\PropertyPath;
23554
23555/**
23556 * @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
23557 */
23558class PasswordTypePasswordHasherExtension extends AbstractTypeExtension
23559{
23560 public function __construct(
23561 private PasswordHasherListener $passwordHasherListener,
23562 ) {
23563 }
23564
23565 public function buildForm(FormBuilderInterface $builder, array $options): void
23566 {
23567 if ($options['hash_property_path']) {
23568 $builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'registerPassword']);
23569 }
23570 }
23571
23572 public function configureOptions(OptionsResolver $resolver): void
23573 {
23574 $resolver->setDefaults([
23575 'hash_property_path' => null,
23576 ]);
23577
23578 $resolver->setAllowedTypes('hash_property_path', ['null', 'string', PropertyPath::class]);
23579
23580 $resolver->setInfo('hash_property_path', 'A valid PropertyAccess syntax where the hashed password will be set.');
23581 }
23582
23583 public static function getExtendedTypes(): iterable
23584 {
23585 return [PasswordType::class];
23586 }
23587}
23588
23589------------------------------------------------------------------------------------------------------------------------
23590 ./Extension/PasswordHasher/Type/FormTypePasswordHasherExtension.php
23591------------------------------------------------------------------------------------------------------------------------
23592<?php
23593
23594/*
23595 * This file is part of the Symfony package.
23596 *
23597 * (c) Fabien Potencier <fabien@symfony.com>
23598 *
23599 * For the full copyright and license information, please view the LICENSE
23600 * file that was distributed with this source code.
23601 */
23602
23603namespace Symfony\Component\Form\Extension\PasswordHasher\Type;
23604
23605use Symfony\Component\Form\AbstractTypeExtension;
23606use Symfony\Component\Form\Extension\Core\Type\FormType;
23607use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
23608use Symfony\Component\Form\FormBuilderInterface;
23609use Symfony\Component\Form\FormEvents;
23610
23611/**
23612 * @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
23613 */
23614class FormTypePasswordHasherExtension extends AbstractTypeExtension
23615{
23616 public function __construct(
23617 private PasswordHasherListener $passwordHasherListener,
23618 ) {
23619 }
23620
23621 public function buildForm(FormBuilderInterface $builder, array $options): void
23622 {
23623 $builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'hashPasswords']);
23624 }
23625
23626 public static function getExtendedTypes(): iterable
23627 {
23628 return [FormType::class];
23629 }
23630}
23631
23632------------------------------------------------------------------------------------------------------------------------
23633 ./Extension/PasswordHasher/EventListener/PasswordHasherListener.php
23634------------------------------------------------------------------------------------------------------------------------
23635<?php
23636
23637/*
23638 * This file is part of the Symfony package.
23639 *
23640 * (c) Fabien Potencier <fabien@symfony.com>
23641 *
23642 * For the full copyright and license information, please view the LICENSE
23643 * file that was distributed with this source code.
23644 */
23645
23646namespace Symfony\Component\Form\Extension\PasswordHasher\EventListener;
23647
23648use Symfony\Component\Form\Exception\InvalidConfigurationException;
23649use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
23650use Symfony\Component\Form\FormEvent;
23651use Symfony\Component\Form\FormInterface;
23652use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
23653use Symfony\Component\PropertyAccess\PropertyAccess;
23654use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
23655use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
23656
23657/**
23658 * @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
23659 * @author Gábor Egyed <gabor.egyed@gmail.com>
23660 */
23661class PasswordHasherListener
23662{
23663 private array $passwords = [];
23664
23665 public function __construct(
23666 private UserPasswordHasherInterface $passwordHasher,
23667 private ?PropertyAccessorInterface $propertyAccessor = null,
23668 ) {
23669 $this->propertyAccessor ??= PropertyAccess::createPropertyAccessor();
23670 }
23671
23672 public function registerPassword(FormEvent $event): void
23673 {
23674 if (null === $event->getData() || '' === $event->getData()) {
23675 return;
23676 }
23677
23678 $this->assertNotMapped($event->getForm());
23679
23680 $this->passwords[] = [
23681 'form' => $event->getForm(),
23682 'property_path' => $event->getForm()->getConfig()->getOption('hash_property_path'),
23683 'password' => $event->getData(),
23684 ];
23685 }
23686
23687 public function hashPasswords(FormEvent $event): void
23688 {
23689 $form = $event->getForm();
23690
23691 if (!$form->isRoot()) {
23692 return;
23693 }
23694
23695 if ($form->isValid()) {
23696 foreach ($this->passwords as $password) {
23697 $user = $this->getUser($password['form']);
23698
23699 $this->propertyAccessor->setValue(
23700 $user,
23701 $password['property_path'],
23702 $this->passwordHasher->hashPassword($user, $password['password'])
23703 );
23704 }
23705 }
23706
23707 $this->passwords = [];
23708 }
23709
23710 private function getTargetForm(FormInterface $form): FormInterface
23711 {
23712 if (!$parentForm = $form->getParent()) {
23713 return $form;
23714 }
23715
23716 $parentType = $parentForm->getConfig()->getType();
23717
23718 do {
23719 if ($parentType->getInnerType() instanceof RepeatedType) {
23720 return $parentForm;
23721 }
23722 } while ($parentType = $parentType->getParent());
23723
23724 return $form;
23725 }
23726
23727 private function getUser(FormInterface $form): PasswordAuthenticatedUserInterface
23728 {
23729 $parent = $this->getTargetForm($form)->getParent();
23730
23731 if (!($user = $parent?->getData()) || !$user instanceof PasswordAuthenticatedUserInterface) {
23732 throw new InvalidConfigurationException(\sprintf('The "hash_property_path" option only supports "%s" objects, "%s" given.', PasswordAuthenticatedUserInterface::class, get_debug_type($user)));
23733 }
23734
23735 return $user;
23736 }
23737
23738 private function assertNotMapped(FormInterface $form): void
23739 {
23740 if ($this->getTargetForm($form)->getConfig()->getMapped()) {
23741 throw new InvalidConfigurationException('The "hash_property_path" option cannot be used on mapped field.');
23742 }
23743 }
23744}
23745
23746------------------------------------------------------------------------------------------------------------------------
23747 ./Extension/PasswordHasher/PasswordHasherExtension.php
23748------------------------------------------------------------------------------------------------------------------------
23749<?php
23750
23751/*
23752 * This file is part of the Symfony package.
23753 *
23754 * (c) Fabien Potencier <fabien@symfony.com>
23755 *
23756 * For the full copyright and license information, please view the LICENSE
23757 * file that was distributed with this source code.
23758 */
23759
23760namespace Symfony\Component\Form\Extension\PasswordHasher;
23761
23762use Symfony\Component\Form\AbstractExtension;
23763use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
23764
23765/**
23766 * Integrates the PasswordHasher component with the Form library.
23767 *
23768 * @author Sébastien Alfaiate <s.alfaiate@webarea.fr>
23769 */
23770class PasswordHasherExtension extends AbstractExtension
23771{
23772 public function __construct(
23773 private PasswordHasherListener $passwordHasherListener,
23774 ) {
23775 }
23776
23777 protected function loadTypeExtensions(): array
23778 {
23779 return [
23780 new Type\FormTypePasswordHasherExtension($this->passwordHasherListener),
23781 new Type\PasswordTypePasswordHasherExtension($this->passwordHasherListener),
23782 ];
23783 }
23784}
23785
23786------------------------------------------------------------------------------------------------------------------------
23787 ./AbstractTypeExtension.php
23788------------------------------------------------------------------------------------------------------------------------
23789<?php
23790
23791/*
23792 * This file is part of the Symfony package.
23793 *
23794 * (c) Fabien Potencier <fabien@symfony.com>
23795 *
23796 * For the full copyright and license information, please view the LICENSE
23797 * file that was distributed with this source code.
23798 */
23799
23800namespace Symfony\Component\Form;
23801
23802use Symfony\Component\OptionsResolver\OptionsResolver;
23803
23804/**
23805 * @author Bernhard Schussek <bschussek@gmail.com>
23806 */
23807abstract class AbstractTypeExtension implements FormTypeExtensionInterface
23808{
23809 public function configureOptions(OptionsResolver $resolver): void
23810 {
23811 }
23812
23813 public function buildForm(FormBuilderInterface $builder, array $options): void
23814 {
23815 }
23816
23817 public function buildView(FormView $view, FormInterface $form, array $options): void
23818 {
23819 }
23820
23821 public function finishView(FormView $view, FormInterface $form, array $options): void
23822 {
23823 }
23824}
23825
23826------------------------------------------------------------------------------------------------------------------------
23827 ./FormFactoryInterface.php
23828------------------------------------------------------------------------------------------------------------------------
23829<?php
23830
23831/*
23832 * This file is part of the Symfony package.
23833 *
23834 * (c) Fabien Potencier <fabien@symfony.com>
23835 *
23836 * For the full copyright and license information, please view the LICENSE
23837 * file that was distributed with this source code.
23838 */
23839
23840namespace Symfony\Component\Form;
23841
23842use Symfony\Component\Form\Extension\Core\Type\FormType;
23843use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
23844
23845/**
23846 * Allows creating a form based on a name, a class or a property.
23847 *
23848 * @author Bernhard Schussek <bschussek@gmail.com>
23849 */
23850interface FormFactoryInterface
23851{
23852 /**
23853 * Returns a form.
23854 *
23855 * @see createBuilder()
23856 *
23857 * @param mixed $data The initial data
23858 *
23859 * @throws InvalidOptionsException if any given option is not applicable to the given type
23860 */
23861 public function create(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface;
23862
23863 /**
23864 * Returns a form.
23865 *
23866 * @see createNamedBuilder()
23867 *
23868 * @param mixed $data The initial data
23869 *
23870 * @throws InvalidOptionsException if any given option is not applicable to the given type
23871 */
23872 public function createNamed(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface;
23873
23874 /**
23875 * Returns a form for a property of a class.
23876 *
23877 * @see createBuilderForProperty()
23878 *
23879 * @param string $class The fully qualified class name
23880 * @param string $property The name of the property to guess for
23881 * @param mixed $data The initial data
23882 *
23883 * @throws InvalidOptionsException if any given option is not applicable to the form type
23884 */
23885 public function createForProperty(string $class, string $property, mixed $data = null, array $options = []): FormInterface;
23886
23887 /**
23888 * Returns a form builder.
23889 *
23890 * @param mixed $data The initial data
23891 *
23892 * @throws InvalidOptionsException if any given option is not applicable to the given type
23893 */
23894 public function createBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface;
23895
23896 /**
23897 * Returns a form builder.
23898 *
23899 * @param mixed $data The initial data
23900 *
23901 * @throws InvalidOptionsException if any given option is not applicable to the given type
23902 */
23903 public function createNamedBuilder(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface;
23904
23905 /**
23906 * Returns a form builder for a property of a class.
23907 *
23908 * If any of the 'required' and type options can be guessed,
23909 * and are not provided in the options argument, the guessed value is used.
23910 *
23911 * @param string $class The fully qualified class name
23912 * @param string $property The name of the property to guess for
23913 * @param mixed $data The initial data
23914 *
23915 * @throws InvalidOptionsException if any given option is not applicable to the form type
23916 */
23917 public function createBuilderForProperty(string $class, string $property, mixed $data = null, array $options = []): FormBuilderInterface;
23918}
23919
23920------------------------------------------------------------------------------------------------------------------------
23921 ./FormBuilder.php
23922------------------------------------------------------------------------------------------------------------------------
23923<?php
23924
23925/*
23926 * This file is part of the Symfony package.
23927 *
23928 * (c) Fabien Potencier <fabien@symfony.com>
23929 *
23930 * For the full copyright and license information, please view the LICENSE
23931 * file that was distributed with this source code.
23932 */
23933
23934namespace Symfony\Component\Form;
23935
23936use Symfony\Component\EventDispatcher\EventDispatcherInterface;
23937use Symfony\Component\Form\Exception\BadMethodCallException;
23938use Symfony\Component\Form\Exception\InvalidArgumentException;
23939use Symfony\Component\Form\Extension\Core\Type\TextType;
23940
23941/**
23942 * A builder for creating {@link Form} instances.
23943 *
23944 * @author Bernhard Schussek <bschussek@gmail.com>
23945 *
23946 * @implements \IteratorAggregate<string, FormBuilderInterface>
23947 */
23948class FormBuilder extends FormConfigBuilder implements \IteratorAggregate, FormBuilderInterface
23949{
23950 /**
23951 * The children of the form builder.
23952 *
23953 * @var FormBuilderInterface[]
23954 */
23955 private array $children = [];
23956
23957 /**
23958 * The data of children who haven't been converted to form builders yet.
23959 */
23960 private array $unresolvedChildren = [];
23961
23962 public function __construct(?string $name, ?string $dataClass, EventDispatcherInterface $dispatcher, FormFactoryInterface $factory, array $options = [])
23963 {
23964 parent::__construct($name, $dataClass, $dispatcher, $options);
23965
23966 $this->setFormFactory($factory);
23967 }
23968
23969 public function add(FormBuilderInterface|string $child, ?string $type = null, array $options = []): static
23970 {
23971 if ($this->locked) {
23972 throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
23973 }
23974
23975 if ($child instanceof FormBuilderInterface) {
23976 $this->children[$child->getName()] = $child;
23977
23978 // In case an unresolved child with the same name exists
23979 unset($this->unresolvedChildren[$child->getName()]);
23980
23981 return $this;
23982 }
23983
23984 // Add to "children" to maintain order
23985 $this->children[$child] = null;
23986 $this->unresolvedChildren[$child] = [$type, $options];
23987
23988 return $this;
23989 }
23990
23991 public function create(string $name, ?string $type = null, array $options = []): FormBuilderInterface
23992 {
23993 if ($this->locked) {
23994 throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
23995 }
23996
23997 if (null === $type && null === $this->getDataClass()) {
23998 $type = TextType::class;
23999 }
24000
24001 if (null !== $type) {
24002 return $this->getFormFactory()->createNamedBuilder($name, $type, null, $options);
24003 }
24004
24005 return $this->getFormFactory()->createBuilderForProperty($this->getDataClass(), $name, null, $options);
24006 }
24007
24008 public function get(string $name): FormBuilderInterface
24009 {
24010 if ($this->locked) {
24011 throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
24012 }
24013
24014 if (isset($this->unresolvedChildren[$name])) {
24015 return $this->resolveChild($name);
24016 }
24017
24018 if (isset($this->children[$name])) {
24019 return $this->children[$name];
24020 }
24021
24022 throw new InvalidArgumentException(\sprintf('The child with the name "%s" does not exist.', $name));
24023 }
24024
24025 public function remove(string $name): static
24026 {
24027 if ($this->locked) {
24028 throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
24029 }
24030
24031 unset($this->unresolvedChildren[$name], $this->children[$name]);
24032
24033 return $this;
24034 }
24035
24036 public function has(string $name): bool
24037 {
24038 if ($this->locked) {
24039 throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
24040 }
24041
24042 return isset($this->unresolvedChildren[$name]) || isset($this->children[$name]);
24043 }
24044
24045 public function all(): array
24046 {
24047 if ($this->locked) {
24048 throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
24049 }
24050
24051 $this->resolveChildren();
24052
24053 return $this->children;
24054 }
24055
24056 public function count(): int
24057 {
24058 if ($this->locked) {
24059 throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
24060 }
24061
24062 return \count($this->children);
24063 }
24064
24065 public function getFormConfig(): FormConfigInterface
24066 {
24067 /** @var self $config */
24068 $config = parent::getFormConfig();
24069
24070 $config->children = [];
24071 $config->unresolvedChildren = [];
24072
24073 return $config;
24074 }
24075
24076 public function getForm(): FormInterface
24077 {
24078 if ($this->locked) {
24079 throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
24080 }
24081
24082 $this->resolveChildren();
24083
24084 $form = new Form($this->getFormConfig());
24085
24086 foreach ($this->children as $child) {
24087 // Automatic initialization is only supported on root forms
24088 $form->add($child->setAutoInitialize(false)->getForm());
24089 }
24090
24091 if ($this->getAutoInitialize()) {
24092 // Automatically initialize the form if it is configured so
24093 $form->initialize();
24094 }
24095
24096 return $form;
24097 }
24098
24099 /**
24100 * @return \Traversable<string, FormBuilderInterface>
24101 */
24102 public function getIterator(): \Traversable
24103 {
24104 if ($this->locked) {
24105 throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
24106 }
24107
24108 return new \ArrayIterator($this->all());
24109 }
24110
24111 /**
24112 * Converts an unresolved child into a {@link FormBuilderInterface} instance.
24113 */
24114 private function resolveChild(string $name): FormBuilderInterface
24115 {
24116 [$type, $options] = $this->unresolvedChildren[$name];
24117
24118 unset($this->unresolvedChildren[$name]);
24119
24120 return $this->children[$name] = $this->create($name, $type, $options);
24121 }
24122
24123 /**
24124 * Converts all unresolved children into {@link FormBuilder} instances.
24125 */
24126 private function resolveChildren(): void
24127 {
24128 foreach ($this->unresolvedChildren as $name => $info) {
24129 $this->children[$name] = $this->create($name, $info[0], $info[1]);
24130 }
24131
24132 $this->unresolvedChildren = [];
24133 }
24134}
24135
24136------------------------------------------------------------------------------------------------------------------------
24137 ./FormRenderer.php
24138------------------------------------------------------------------------------------------------------------------------
24139<?php
24140
24141/*
24142 * This file is part of the Symfony package.
24143 *
24144 * (c) Fabien Potencier <fabien@symfony.com>
24145 *
24146 * For the full copyright and license information, please view the LICENSE
24147 * file that was distributed with this source code.
24148 */
24149
24150namespace Symfony\Component\Form;
24151
24152use Symfony\Component\Form\Exception\BadMethodCallException;
24153use Symfony\Component\Form\Exception\LogicException;
24154use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
24155use Twig\Environment;
24156
24157/**
24158 * Renders a form into HTML using a rendering engine.
24159 *
24160 * @author Bernhard Schussek <bschussek@gmail.com>
24161 */
24162class FormRenderer implements FormRendererInterface
24163{
24164 public const CACHE_KEY_VAR = 'unique_block_prefix';
24165
24166 private array $blockNameHierarchyMap = [];
24167 private array $hierarchyLevelMap = [];
24168 private array $variableStack = [];
24169
24170 public function __construct(
24171 private FormRendererEngineInterface $engine,
24172 private ?CsrfTokenManagerInterface $csrfTokenManager = null,
24173 ) {
24174 }
24175
24176 public function getEngine(): FormRendererEngineInterface
24177 {
24178 return $this->engine;
24179 }
24180
24181 public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void
24182 {
24183 $this->engine->setTheme($view, $themes, $useDefaultThemes);
24184 }
24185
24186 public function renderCsrfToken(string $tokenId): string
24187 {
24188 if (null === $this->csrfTokenManager) {
24189 throw new BadMethodCallException('CSRF tokens can only be generated if a CsrfTokenManagerInterface is injected in FormRenderer::__construct(). Try running "composer require symfony/security-csrf".');
24190 }
24191
24192 return $this->csrfTokenManager->getToken($tokenId)->getValue();
24193 }
24194
24195 public function renderBlock(FormView $view, string $blockName, array $variables = []): string
24196 {
24197 $resource = $this->engine->getResourceForBlockName($view, $blockName);
24198
24199 if (!$resource) {
24200 throw new LogicException(\sprintf('No block "%s" found while rendering the form.', $blockName));
24201 }
24202
24203 $viewCacheKey = $view->vars[self::CACHE_KEY_VAR];
24204
24205 // The variables are cached globally for a view (instead of for the
24206 // current suffix)
24207 if (!isset($this->variableStack[$viewCacheKey])) {
24208 $this->variableStack[$viewCacheKey] = [];
24209
24210 // The default variable scope contains all view variables, merged with
24211 // the variables passed explicitly to the helper
24212 $scopeVariables = $view->vars;
24213
24214 $varInit = true;
24215 } else {
24216 // Reuse the current scope and merge it with the explicitly passed variables
24217 $scopeVariables = end($this->variableStack[$viewCacheKey]);
24218
24219 $varInit = false;
24220 }
24221
24222 // Merge the passed with the existing attributes
24223 if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
24224 $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
24225 }
24226
24227 // Merge the passed with the exist *label* attributes
24228 if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
24229 $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
24230 }
24231
24232 // Do not use array_replace_recursive(), otherwise array variables
24233 // cannot be overwritten
24234 $variables = array_replace($scopeVariables, $variables);
24235
24236 $this->variableStack[$viewCacheKey][] = $variables;
24237
24238 // Do the rendering
24239 $html = $this->engine->renderBlock($view, $resource, $blockName, $variables);
24240
24241 // Clear the stack
24242 array_pop($this->variableStack[$viewCacheKey]);
24243
24244 if ($varInit) {
24245 unset($this->variableStack[$viewCacheKey]);
24246 }
24247
24248 return $html;
24249 }
24250
24251 public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, array $variables = []): string
24252 {
24253 $renderOnlyOnce = 'row' === $blockNameSuffix || 'widget' === $blockNameSuffix;
24254
24255 if ($renderOnlyOnce && $view->isRendered()) {
24256 // This is not allowed, because it would result in rendering same IDs multiple times, which is not valid.
24257 throw new BadMethodCallException(\sprintf('Field "%s" has already been rendered, save the result of previous render call to a variable and output that instead.', $view->vars['name']));
24258 }
24259
24260 // The cache key for storing the variables and types
24261 $viewCacheKey = $view->vars[self::CACHE_KEY_VAR];
24262 $viewAndSuffixCacheKey = $viewCacheKey.$blockNameSuffix;
24263
24264 // In templates, we have to deal with two kinds of block hierarchies:
24265 //
24266 // +---------+ +---------+
24267 // | Theme B | -------> | Theme A |
24268 // +---------+ +---------+
24269 //
24270 // form_widget -------> form_widget
24271 // ^
24272 // |
24273 // choice_widget -----> choice_widget
24274 //
24275 // The first kind of hierarchy is the theme hierarchy. This allows to
24276 // override the block "choice_widget" from Theme A in the extending
24277 // Theme B. This kind of inheritance needs to be supported by the
24278 // template engine and, for example, offers "parent()" or similar
24279 // functions to fall back from the custom to the parent implementation.
24280 //
24281 // The second kind of hierarchy is the form type hierarchy. This allows
24282 // to implement a custom "choice_widget" block (no matter in which theme),
24283 // or to fallback to the block of the parent type, which would be
24284 // "form_widget" in this example (again, no matter in which theme).
24285 // If the designer wants to explicitly fallback to "form_widget" in their
24286 // custom "choice_widget", for example because they only want to wrap
24287 // a <div> around the original implementation, they can call the
24288 // widget() function again to render the block for the parent type.
24289 //
24290 // The second kind is implemented in the following blocks.
24291 if (!isset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey])) {
24292 // INITIAL CALL
24293 // Calculate the hierarchy of template blocks and start on
24294 // the bottom level of the hierarchy (= "_<id>_<section>" block)
24295 $blockNameHierarchy = [];
24296 foreach ($view->vars['block_prefixes'] as $blockNamePrefix) {
24297 $blockNameHierarchy[] = $blockNamePrefix.'_'.$blockNameSuffix;
24298 }
24299 $hierarchyLevel = \count($blockNameHierarchy) - 1;
24300
24301 $hierarchyInit = true;
24302 } else {
24303 // RECURSIVE CALL
24304 // If a block recursively calls searchAndRenderBlock() again, resume rendering
24305 // using the parent type in the hierarchy.
24306 $blockNameHierarchy = $this->blockNameHierarchyMap[$viewAndSuffixCacheKey];
24307 $hierarchyLevel = $this->hierarchyLevelMap[$viewAndSuffixCacheKey] - 1;
24308
24309 $hierarchyInit = false;
24310 }
24311
24312 // The variables are cached globally for a view (instead of for the
24313 // current suffix)
24314 if (!isset($this->variableStack[$viewCacheKey])) {
24315 $this->variableStack[$viewCacheKey] = [];
24316
24317 // The default variable scope contains all view variables, merged with
24318 // the variables passed explicitly to the helper
24319 $scopeVariables = $view->vars;
24320
24321 $varInit = true;
24322 } else {
24323 // Reuse the current scope and merge it with the explicitly passed variables
24324 $scopeVariables = end($this->variableStack[$viewCacheKey]);
24325
24326 $varInit = false;
24327 }
24328
24329 // Load the resource where this block can be found
24330 $resource = $this->engine->getResourceForBlockNameHierarchy($view, $blockNameHierarchy, $hierarchyLevel);
24331
24332 // Update the current hierarchy level to the one at which the resource was
24333 // found. For example, if looking for "choice_widget", but only a resource
24334 // is found for its parent "form_widget", then the level is updated here
24335 // to the parent level.
24336 $hierarchyLevel = $this->engine->getResourceHierarchyLevel($view, $blockNameHierarchy, $hierarchyLevel);
24337
24338 // The actually existing block name in $resource
24339 $blockName = $blockNameHierarchy[$hierarchyLevel];
24340
24341 // Escape if no resource exists for this block
24342 if (!$resource) {
24343 if (\count($blockNameHierarchy) !== \count(array_unique($blockNameHierarchy))) {
24344 throw new LogicException(\sprintf('Unable to render the form because the block names array contains duplicates: "%s".', implode('", "', array_reverse($blockNameHierarchy))));
24345 }
24346
24347 throw new LogicException(\sprintf('Unable to render the form as none of the following blocks exist: "%s".', implode('", "', array_reverse($blockNameHierarchy))));
24348 }
24349
24350 // Merge the passed with the existing attributes
24351 if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
24352 $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
24353 }
24354
24355 // Merge the passed with the exist *label* attributes
24356 if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
24357 $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
24358 }
24359
24360 // Do not use array_replace_recursive(), otherwise array variables
24361 // cannot be overwritten
24362 $variables = array_replace($scopeVariables, $variables);
24363
24364 // In order to make recursive calls possible, we need to store the block hierarchy,
24365 // the current level of the hierarchy and the variables so that this method can
24366 // resume rendering one level higher of the hierarchy when it is called recursively.
24367 //
24368 // We need to store these values in maps (associative arrays) because within a
24369 // call to widget() another call to widget() can be made, but for a different view
24370 // object. These nested calls should not override each other.
24371 $this->blockNameHierarchyMap[$viewAndSuffixCacheKey] = $blockNameHierarchy;
24372 $this->hierarchyLevelMap[$viewAndSuffixCacheKey] = $hierarchyLevel;
24373
24374 // We also need to store the variables for the view so that we can render other
24375 // blocks for the same view using the same variables as in the outer block.
24376 $this->variableStack[$viewCacheKey][] = $variables;
24377
24378 // Do the rendering
24379 $html = $this->engine->renderBlock($view, $resource, $blockName, $variables);
24380
24381 // Clear the stack
24382 array_pop($this->variableStack[$viewCacheKey]);
24383
24384 // Clear the caches if they were filled for the first time within
24385 // this function call
24386 if ($hierarchyInit) {
24387 unset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey], $this->hierarchyLevelMap[$viewAndSuffixCacheKey]);
24388 }
24389
24390 if ($varInit) {
24391 unset($this->variableStack[$viewCacheKey]);
24392 }
24393
24394 if ($renderOnlyOnce) {
24395 $view->setRendered();
24396 }
24397
24398 return $html;
24399 }
24400
24401 public function humanize(string $text): string
24402 {
24403 return ucfirst(strtolower(trim(preg_replace(['/([A-Z])/', '/[_\s]+/'], ['_$1', ' '], $text))));
24404 }
24405
24406 /**
24407 * @internal
24408 */
24409 public function encodeCurrency(Environment $environment, string $text, string $widget = ''): string
24410 {
24411 if ('UTF-8' === $charset = $environment->getCharset()) {
24412 $text = htmlspecialchars($text, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
24413 } else {
24414 $text = htmlentities($text, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
24415 $text = iconv('UTF-8', $charset, $text);
24416 $widget = iconv('UTF-8', $charset, $widget);
24417 }
24418
24419 return str_replace('{{ widget }}', $widget, $text);
24420 }
24421}
24422
24423------------------------------------------------------------------------------------------------------------------------
24424 ./ResolvedFormTypeFactory.php
24425------------------------------------------------------------------------------------------------------------------------
24426<?php
24427
24428/*
24429 * This file is part of the Symfony package.
24430 *
24431 * (c) Fabien Potencier <fabien@symfony.com>
24432 *
24433 * For the full copyright and license information, please view the LICENSE
24434 * file that was distributed with this source code.
24435 */
24436
24437namespace Symfony\Component\Form;
24438
24439/**
24440 * @author Bernhard Schussek <bschussek@gmail.com>
24441 */
24442class ResolvedFormTypeFactory implements ResolvedFormTypeFactoryInterface
24443{
24444 public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface
24445 {
24446 return new ResolvedFormType($type, $typeExtensions, $parent);
24447 }
24448}
24449
24450------------------------------------------------------------------------------------------------------------------------
24451 ./SubmitButtonBuilder.php
24452------------------------------------------------------------------------------------------------------------------------
24453<?php
24454
24455/*
24456 * This file is part of the Symfony package.
24457 *
24458 * (c) Fabien Potencier <fabien@symfony.com>
24459 *
24460 * For the full copyright and license information, please view the LICENSE
24461 * file that was distributed with this source code.
24462 */
24463
24464namespace Symfony\Component\Form;
24465
24466/**
24467 * A builder for {@link SubmitButton} instances.
24468 *
24469 * @author Bernhard Schussek <bschussek@gmail.com>
24470 */
24471class SubmitButtonBuilder extends ButtonBuilder
24472{
24473 /**
24474 * Creates the button.
24475 */
24476 public function getForm(): SubmitButton
24477 {
24478 return new SubmitButton($this->getFormConfig());
24479 }
24480}
24481
24482------------------------------------------------------------------------------------------------------------------------
24483 ./FormExtensionInterface.php
24484------------------------------------------------------------------------------------------------------------------------
24485<?php
24486
24487/*
24488 * This file is part of the Symfony package.
24489 *
24490 * (c) Fabien Potencier <fabien@symfony.com>
24491 *
24492 * For the full copyright and license information, please view the LICENSE
24493 * file that was distributed with this source code.
24494 */
24495
24496namespace Symfony\Component\Form;
24497
24498/**
24499 * Interface for extensions which provide types, type extensions and a guesser.
24500 */
24501interface FormExtensionInterface
24502{
24503 /**
24504 * Returns a type by name.
24505 *
24506 * @param string $name The name of the type
24507 *
24508 * @throws Exception\InvalidArgumentException if the given type is not supported by this extension
24509 */
24510 public function getType(string $name): FormTypeInterface;
24511
24512 /**
24513 * Returns whether the given type is supported.
24514 *
24515 * @param string $name The name of the type
24516 */
24517 public function hasType(string $name): bool;
24518
24519 /**
24520 * Returns the extensions for the given type.
24521 *
24522 * @param string $name The name of the type
24523 *
24524 * @return FormTypeExtensionInterface[]
24525 */
24526 public function getTypeExtensions(string $name): array;
24527
24528 /**
24529 * Returns whether this extension provides type extensions for the given type.
24530 *
24531 * @param string $name The name of the type
24532 */
24533 public function hasTypeExtensions(string $name): bool;
24534
24535 /**
24536 * Returns the type guesser provided by this extension.
24537 */
24538 public function getTypeGuesser(): ?FormTypeGuesserInterface;
24539}
24540
24541------------------------------------------------------------------------------------------------------------------------
24542 ./FormRendererInterface.php
24543------------------------------------------------------------------------------------------------------------------------
24544<?php
24545
24546/*
24547 * This file is part of the Symfony package.
24548 *
24549 * (c) Fabien Potencier <fabien@symfony.com>
24550 *
24551 * For the full copyright and license information, please view the LICENSE
24552 * file that was distributed with this source code.
24553 */
24554
24555namespace Symfony\Component\Form;
24556
24557/**
24558 * Renders a form into HTML.
24559 *
24560 * @author Bernhard Schussek <bschussek@gmail.com>
24561 */
24562interface FormRendererInterface
24563{
24564 /**
24565 * Returns the engine used by this renderer.
24566 */
24567 public function getEngine(): FormRendererEngineInterface;
24568
24569 /**
24570 * Sets the theme(s) to be used for rendering a view and its children.
24571 *
24572 * @param FormView $view The view to assign the theme(s) to
24573 * @param mixed $themes The theme(s). The type of these themes
24574 * is open to the implementation.
24575 * @param bool $useDefaultThemes If true, will use default themes specified
24576 * in the renderer
24577 */
24578 public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void;
24579
24580 /**
24581 * Renders a named block of the form theme.
24582 *
24583 * @param FormView $view The view for which to render the block
24584 * @param array $variables The variables to pass to the template
24585 */
24586 public function renderBlock(FormView $view, string $blockName, array $variables = []): string;
24587
24588 /**
24589 * Searches and renders a block for a given name suffix.
24590 *
24591 * The block is searched by combining the block names stored in the
24592 * form view with the given suffix. If a block name is found, that
24593 * block is rendered.
24594 *
24595 * If this method is called recursively, the block search is continued
24596 * where a block was found before.
24597 *
24598 * @param FormView $view The view for which to render the block
24599 * @param array $variables The variables to pass to the template
24600 */
24601 public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, array $variables = []): string;
24602
24603 /**
24604 * Renders a CSRF token.
24605 *
24606 * Use this helper for CSRF protection without the overhead of creating a
24607 * form.
24608 *
24609 * <input type="hidden" name="token" value="<?php $renderer->renderCsrfToken('rm_user_'.$user->getId()) ?>">
24610 *
24611 * Check the token in your action using the same token ID.
24612 *
24613 * // $csrfProvider being an instance of Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface
24614 * if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
24615 * throw new \RuntimeException('CSRF attack detected.');
24616 * }
24617 */
24618 public function renderCsrfToken(string $tokenId): string;
24619
24620 /**
24621 * Makes a technical name human readable.
24622 *
24623 * Sequences of underscores are replaced by single spaces. The first letter
24624 * of the resulting string is capitalized, while all other letters are
24625 * turned to lowercase.
24626 */
24627 public function humanize(string $text): string;
24628}
24629
24630------------------------------------------------------------------------------------------------------------------------
24631 ./FileUploadError.php
24632------------------------------------------------------------------------------------------------------------------------
24633<?php
24634
24635/*
24636 * This file is part of the Symfony package.
24637 *
24638 * (c) Fabien Potencier <fabien@symfony.com>
24639 *
24640 * For the full copyright and license information, please view the LICENSE
24641 * file that was distributed with this source code.
24642 */
24643
24644namespace Symfony\Component\Form;
24645
24646/**
24647 * @internal
24648 */
24649class FileUploadError extends FormError
24650{
24651}
24652
24653------------------------------------------------------------------------------------------------------------------------
24654 ./CallbackTransformer.php
24655------------------------------------------------------------------------------------------------------------------------
24656<?php
24657
24658/*
24659 * This file is part of the Symfony package.
24660 *
24661 * (c) Fabien Potencier <fabien@symfony.com>
24662 *
24663 * For the full copyright and license information, please view the LICENSE
24664 * file that was distributed with this source code.
24665 */
24666
24667namespace Symfony\Component\Form;
24668
24669class CallbackTransformer implements DataTransformerInterface
24670{
24671 private \Closure $transform;
24672 private \Closure $reverseTransform;
24673
24674 public function __construct(callable $transform, callable $reverseTransform)
24675 {
24676 $this->transform = $transform(...);
24677 $this->reverseTransform = $reverseTransform(...);
24678 }
24679
24680 public function transform(mixed $data): mixed
24681 {
24682 return ($this->transform)($data);
24683 }
24684
24685 public function reverseTransform(mixed $data): mixed
24686 {
24687 return ($this->reverseTransform)($data);
24688 }
24689}
24690