Advertisement
actual-batman

form

Mar 9th, 2025
172
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 823.44 KB | None | 0 0
  1.  
  2. ------------------------------------------------------------------------------------------------------------------------
  3.  ./AbstractRendererEngine.php
  4. ------------------------------------------------------------------------------------------------------------------------
  5. <?php
  6.  
  7. /*
  8.  * This file is part of the Symfony package.
  9.  *
  10.  * (c) Fabien Potencier <[email protected]>
  11.  *
  12.  * For the full copyright and license information, please view the LICENSE
  13.  * file that was distributed with this source code.
  14.  */
  15.  
  16. namespace Symfony\Component\Form;
  17.  
  18. use Symfony\Contracts\Service\ResetInterface;
  19.  
  20. /**
  21.  * Default implementation of {@link FormRendererEngineInterface}.
  22.  *
  23.  * @author Bernhard Schussek <[email protected]>
  24.  */
  25. abstract 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 <[email protected]>
  202.  *
  203.  * For the full copyright and license information, please view the LICENSE
  204.  * file that was distributed with this source code.
  205.  */
  206.  
  207. namespace Symfony\Component\Form;
  208.  
  209. use Symfony\Component\Form\Extension\Core\Type\FormType;
  210. use Symfony\Component\Form\Extension\Core\Type\TextType;
  211.  
  212. class 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 <[email protected]>
  308.  *
  309.  * For the full copyright and license information, please view the LICENSE
  310.  * file that was distributed with this source code.
  311.  */
  312.  
  313. namespace Symfony\Component\Form;
  314.  
  315. use Symfony\Component\Form\Extension\Core\Type\FormType;
  316. use Symfony\Component\Form\Util\StringUtil;
  317. use Symfony\Component\OptionsResolver\OptionsResolver;
  318.  
  319. /**
  320.  * @author Bernhard Schussek <[email protected]>
  321.  */
  322. abstract 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 <[email protected]>
  378.  *
  379.  * For the full copyright and license information, please view the LICENSE
  380.  * file that was distributed with this source code.
  381.  */
  382.  
  383. namespace Symfony\Component\Form;
  384.  
  385. use Symfony\Component\OptionsResolver\OptionsResolver;
  386.  
  387. /**
  388.  * A wrapper for a form type and its extensions.
  389.  *
  390.  * @author Bernhard Schussek <[email protected]>
  391.  */
  392. interface 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 <[email protected]>
  462.  *
  463.  * For the full copyright and license information, please view the LICENSE
  464.  * file that was distributed with this source code.
  465.  */
  466.  
  467. namespace Symfony\Component\Form;
  468.  
  469. /**
  470.  * A form element whose errors can be cleared.
  471.  *
  472.  * @author Colin O'Dell <[email protected]>
  473.  */
  474. interface 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 <[email protected]>
  495.  *
  496.  * For the full copyright and license information, please view the LICENSE
  497.  * file that was distributed with this source code.
  498.  */
  499.  
  500. namespace Symfony\Component\Form;
  501.  
  502. /**
  503.  * A builder for FormFactoryInterface objects.
  504.  *
  505.  * @author Bernhard Schussek <[email protected]>
  506.  */
  507. interface 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 <[email protected]>
  595.  *
  596.  * For the full copyright and license information, please view the LICENSE
  597.  * file that was distributed with this source code.
  598.  */
  599.  
  600. namespace Symfony\Component\Form;
  601.  
  602. use Symfony\Component\EventDispatcher\EventDispatcher;
  603. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  604. use Symfony\Component\OptionsResolver\Exception\ExceptionInterface;
  605. use Symfony\Component\OptionsResolver\OptionsResolver;
  606.  
  607. /**
  608.  * A wrapper for a form type and its extensions.
  609.  *
  610.  * @author Bernhard Schussek <[email protected]>
  611.  */
  612. class 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 <[email protected]>
  771.  *
  772.  * For the full copyright and license information, please view the LICENSE
  773.  * file that was distributed with this source code.
  774.  */
  775.  
  776. namespace 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 <[email protected]>
  826.  */
  827. final 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 <[email protected]>
  862.  *
  863.  * For the full copyright and license information, please view the LICENSE
  864.  * file that was distributed with this source code.
  865.  */
  866.  
  867. namespace Symfony\Component\Form;
  868.  
  869. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  870. use Symfony\Component\PropertyAccess\PropertyPathInterface;
  871.  
  872. /**
  873.  * @author Bernhard Schussek <[email protected]>
  874.  */
  875. interface 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 <[email protected]>
  1126.  *
  1127.  * For the full copyright and license information, please view the LICENSE
  1128.  * file that was distributed with this source code.
  1129.  */
  1130.  
  1131. namespace Symfony\Component\Form;
  1132.  
  1133. /**
  1134.  * @author Bernhard Schussek <[email protected]>
  1135.  *
  1136.  * @extends \Traversable<string, FormBuilderInterface>
  1137.  */
  1138. interface 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 <[email protected]>
  1199.  *
  1200.  * For the full copyright and license information, please view the LICENSE
  1201.  * file that was distributed with this source code.
  1202.  */
  1203.  
  1204. namespace Symfony\Component\Form\Command;
  1205.  
  1206. use Symfony\Component\Console\Attribute\AsCommand;
  1207. use Symfony\Component\Console\Command\Command;
  1208. use Symfony\Component\Console\Completion\CompletionInput;
  1209. use Symfony\Component\Console\Completion\CompletionSuggestions;
  1210. use Symfony\Component\Console\Exception\InvalidArgumentException;
  1211. use Symfony\Component\Console\Input\InputArgument;
  1212. use Symfony\Component\Console\Input\InputInterface;
  1213. use Symfony\Component\Console\Input\InputOption;
  1214. use Symfony\Component\Console\Output\OutputInterface;
  1215. use Symfony\Component\Console\Style\SymfonyStyle;
  1216. use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
  1217. use Symfony\Component\Form\Console\Helper\DescriptorHelper;
  1218. use Symfony\Component\Form\Extension\Core\CoreExtension;
  1219. use Symfony\Component\Form\FormRegistryInterface;
  1220. use Symfony\Component\Form\FormTypeInterface;
  1221.  
  1222. /**
  1223.  * A console command for retrieving information about form types.
  1224.  *
  1225.  * @author Yonel Ceruto <[email protected]>
  1226.  */
  1227. #[AsCommand(name: 'debug:form', description: 'Display form type information')]
  1228. class 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'
  1251. The <info>%command.name%</info> command displays information about form types.
  1252.  
  1253.   <info>php %command.full_name%</info>
  1254.  
  1255. The command lists all built-in types, services types, type extensions and
  1256. guessers 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.  
  1261. The command lists all defined options that contains the given form type,
  1262. as well as their parents and type extensions.
  1263.  
  1264.   <info>php %command.full_name% ChoiceType choice_value</info>
  1265.  
  1266. Use the <info>--show-deprecated</info> option to display form types with
  1267. deprecated 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.  
  1272. The command displays the definition of the given option name.
  1273.  
  1274.   <info>php %command.full_name% --format=json</info>
  1275.  
  1276. The command lists everything in a machine readable json format.
  1277. EOF
  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 <[email protected]>
  1483.  *
  1484.  * For the full copyright and license information, please view the LICENSE
  1485.  * file that was distributed with this source code.
  1486.  */
  1487.  
  1488. namespace Symfony\Component\Form;
  1489.  
  1490. use Symfony\Component\OptionsResolver\OptionsResolver;
  1491.  
  1492. /**
  1493.  * @author Bernhard Schussek <[email protected]>
  1494.  */
  1495. interface 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 <[email protected]>
  1552.  *
  1553.  * For the full copyright and license information, please view the LICENSE
  1554.  * file that was distributed with this source code.
  1555.  */
  1556.  
  1557. namespace Symfony\Component\Form;
  1558.  
  1559. use Symfony\Component\Form\Event\PostSetDataEvent;
  1560. use Symfony\Component\Form\Event\PostSubmitEvent;
  1561. use Symfony\Component\Form\Event\PreSetDataEvent;
  1562. use Symfony\Component\Form\Event\PreSubmitEvent;
  1563. use Symfony\Component\Form\Event\SubmitEvent;
  1564. use Symfony\Component\Form\Exception\AlreadySubmittedException;
  1565. use Symfony\Component\Form\Exception\LogicException;
  1566. use Symfony\Component\Form\Exception\OutOfBoundsException;
  1567. use Symfony\Component\Form\Exception\RuntimeException;
  1568. use Symfony\Component\Form\Exception\TransformationFailedException;
  1569. use Symfony\Component\Form\Extension\Core\Type\TextType;
  1570. use Symfony\Component\Form\Util\FormUtil;
  1571. use Symfony\Component\Form\Util\InheritDataAwareIterator;
  1572. use Symfony\Component\Form\Util\OrderedHashMap;
  1573. use Symfony\Component\PropertyAccess\PropertyPath;
  1574. use 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 <[email protected]>
  1612.  * @author Bernhard Schussek <[email protected]>
  1613.  *
  1614.  * @implements \IteratorAggregate<string, FormInterface>
  1615.  */
  1616. class 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 <[email protected]>
  2566.  *
  2567.  * For the full copyright and license information, please view the LICENSE
  2568.  * file that was distributed with this source code.
  2569.  */
  2570.  
  2571. namespace Symfony\Component\Form;
  2572.  
  2573. use Symfony\Component\PropertyAccess\PropertyPathInterface;
  2574.  
  2575. /**
  2576.  * A form group bundling multiple forms in a hierarchical structure.
  2577.  *
  2578.  * @author Bernhard Schussek <[email protected]>
  2579.  *
  2580.  * @extends \ArrayAccess<string, FormInterface>
  2581.  * @extends \Traversable<string, FormInterface>
  2582.  */
  2583. interface 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 <[email protected]>
  2859.  *
  2860.  * For the full copyright and license information, please view the LICENSE
  2861.  * file that was distributed with this source code.
  2862.  */
  2863.  
  2864. namespace Symfony\Component\Form;
  2865.  
  2866. use Symfony\Component\Form\Exception\BadMethodCallException;
  2867.  
  2868. /**
  2869.  * Wraps errors in forms.
  2870.  *
  2871.  * @author Bernhard Schussek <[email protected]>
  2872.  */
  2873. class 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 <[email protected]>
  2979.  *
  2980.  * For the full copyright and license information, please view the LICENSE
  2981.  * file that was distributed with this source code.
  2982.  */
  2983.  
  2984. namespace Symfony\Component\Form;
  2985.  
  2986. use Symfony\Contracts\EventDispatcher\Event;
  2987.  
  2988. /**
  2989.  * @author Bernhard Schussek <[email protected]>
  2990.  */
  2991. class 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 <[email protected]>
  3033.  *
  3034.  * For the full copyright and license information, please view the LICENSE
  3035.  * file that was distributed with this source code.
  3036.  */
  3037.  
  3038. namespace Symfony\Component\Form\DependencyInjection;
  3039.  
  3040. use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
  3041. use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
  3042. use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
  3043. use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
  3044. use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
  3045. use Symfony\Component\DependencyInjection\ContainerBuilder;
  3046. use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
  3047. use 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 <[email protected]>
  3054.  */
  3055. class 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 <[email protected]>
  3177.  *
  3178.  * For the full copyright and license information, please view the LICENSE
  3179.  * file that was distributed with this source code.
  3180.  */
  3181.  
  3182. namespace Symfony\Component\Form;
  3183.  
  3184. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  3185. use Symfony\Component\Form\Guess\Guess;
  3186. use Symfony\Component\Form\Guess\TypeGuess;
  3187. use Symfony\Component\Form\Guess\ValueGuess;
  3188.  
  3189. class 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 <[email protected]>
  3266.  *
  3267.  * For the full copyright and license information, please view the LICENSE
  3268.  * file that was distributed with this source code.
  3269.  */
  3270.  
  3271. namespace Symfony\Component\Form;
  3272.  
  3273. use Symfony\Component\Form\Event\PostSetDataEvent;
  3274. use Symfony\Component\Form\Event\PostSubmitEvent;
  3275. use Symfony\Component\Form\Event\PreSetDataEvent;
  3276. use Symfony\Component\Form\Event\PreSubmitEvent;
  3277. use 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 <[email protected]>
  3287.  */
  3288. final 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 <[email protected]>
  3383.  *
  3384.  * For the full copyright and license information, please view the LICENSE
  3385.  * file that was distributed with this source code.
  3386.  */
  3387.  
  3388. namespace Symfony\Component\Form;
  3389.  
  3390. /**
  3391.  * The central registry of the Form component.
  3392.  *
  3393.  * @author Bernhard Schussek <[email protected]>
  3394.  */
  3395. interface 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 <[email protected]>
  3433.  *
  3434.  * For the full copyright and license information, please view the LICENSE
  3435.  * file that was distributed with this source code.
  3436.  */
  3437.  
  3438. namespace Symfony\Component\Form;
  3439.  
  3440. use Symfony\Component\OptionsResolver\OptionsResolver;
  3441.  
  3442. /**
  3443.  * @author Bernhard Schussek <[email protected]>
  3444.  */
  3445. interface 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 <[email protected]>
  3535.  *
  3536.  * For the full copyright and license information, please view the LICENSE
  3537.  * file that was distributed with this source code.
  3538.  */
  3539.  
  3540. namespace Symfony\Component\Form\Exception;
  3541.  
  3542. /**
  3543.  * Base OutOfBoundsException for Form component.
  3544.  *
  3545.  * @author Alexander Kotynia <[email protected]>
  3546.  */
  3547. class 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 <[email protected]>
  3560.  *
  3561.  * For the full copyright and license information, please view the LICENSE
  3562.  * file that was distributed with this source code.
  3563.  */
  3564.  
  3565. namespace Symfony\Component\Form\Exception;
  3566.  
  3567. class 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 <[email protected]>
  3580.  *
  3581.  * For the full copyright and license information, please view the LICENSE
  3582.  * file that was distributed with this source code.
  3583.  */
  3584.  
  3585. namespace Symfony\Component\Form\Exception;
  3586.  
  3587. /**
  3588.  * Indicates a value transformation error.
  3589.  *
  3590.  * @author Bernhard Schussek <[email protected]>
  3591.  */
  3592. class 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 <[email protected]>
  3636.  *
  3637.  * For the full copyright and license information, please view the LICENSE
  3638.  * file that was distributed with this source code.
  3639.  */
  3640.  
  3641. namespace Symfony\Component\Form\Exception;
  3642.  
  3643. /**
  3644.  * Base RuntimeException for the Form component.
  3645.  *
  3646.  * @author Bernhard Schussek <[email protected]>
  3647.  */
  3648. class 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 <[email protected]>
  3661.  *
  3662.  * For the full copyright and license information, please view the LICENSE
  3663.  * file that was distributed with this source code.
  3664.  */
  3665.  
  3666. namespace Symfony\Component\Form\Exception;
  3667.  
  3668. /**
  3669.  * Base ExceptionInterface for the Form component.
  3670.  *
  3671.  * @author Bernhard Schussek <[email protected]>
  3672.  */
  3673. interface 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 <[email protected]>
  3686.  *
  3687.  * For the full copyright and license information, please view the LICENSE
  3688.  * file that was distributed with this source code.
  3689.  */
  3690.  
  3691. namespace Symfony\Component\Form\Exception;
  3692.  
  3693. /**
  3694.  * Base BadMethodCallException for the Form component.
  3695.  *
  3696.  * @author Bernhard Schussek <[email protected]>
  3697.  */
  3698. class 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 <[email protected]>
  3711.  *
  3712.  * For the full copyright and license information, please view the LICENSE
  3713.  * file that was distributed with this source code.
  3714.  */
  3715.  
  3716. namespace Symfony\Component\Form\Exception;
  3717.  
  3718. /**
  3719.  * Base InvalidArgumentException for the Form component.
  3720.  *
  3721.  * @author Bernhard Schussek <[email protected]>
  3722.  */
  3723. class 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 <[email protected]>
  3736.  *
  3737.  * For the full copyright and license information, please view the LICENSE
  3738.  * file that was distributed with this source code.
  3739.  */
  3740.  
  3741. namespace Symfony\Component\Form\Exception;
  3742.  
  3743. /**
  3744.  * Base LogicException for Form component.
  3745.  *
  3746.  * @author Alexander Kotynia <[email protected]>
  3747.  */
  3748. class 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 <[email protected]>
  3761.  *
  3762.  * For the full copyright and license information, please view the LICENSE
  3763.  * file that was distributed with this source code.
  3764.  */
  3765.  
  3766. namespace Symfony\Component\Form\Exception;
  3767.  
  3768. class 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 <[email protected]>
  3781.  *
  3782.  * For the full copyright and license information, please view the LICENSE
  3783.  * file that was distributed with this source code.
  3784.  */
  3785.  
  3786. namespace Symfony\Component\Form\Exception;
  3787.  
  3788. class 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 <[email protected]>
  3801.  *
  3802.  * For the full copyright and license information, please view the LICENSE
  3803.  * file that was distributed with this source code.
  3804.  */
  3805.  
  3806. namespace Symfony\Component\Form\Exception;
  3807.  
  3808. class 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 <[email protected]>
  3821.  *
  3822.  * For the full copyright and license information, please view the LICENSE
  3823.  * file that was distributed with this source code.
  3824.  */
  3825.  
  3826. namespace 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 <[email protected]>
  3833.  */
  3834. class 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 <[email protected]>
  3847.  *
  3848.  * For the full copyright and license information, please view the LICENSE
  3849.  * file that was distributed with this source code.
  3850.  */
  3851.  
  3852. namespace Symfony\Component\Form\Exception;
  3853.  
  3854. class 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 <[email protected]>
  3871.  *
  3872.  * For the full copyright and license information, please view the LICENSE
  3873.  * file that was distributed with this source code.
  3874.  */
  3875.  
  3876. namespace Symfony\Component\Form\Event;
  3877.  
  3878. use 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.  */
  3885. final 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 <[email protected]>
  3898.  *
  3899.  * For the full copyright and license information, please view the LICENSE
  3900.  * file that was distributed with this source code.
  3901.  */
  3902.  
  3903. namespace Symfony\Component\Form\Event;
  3904.  
  3905. use 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.  */
  3914. final 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 <[email protected]>
  3927.  *
  3928.  * For the full copyright and license information, please view the LICENSE
  3929.  * file that was distributed with this source code.
  3930.  */
  3931.  
  3932. namespace Symfony\Component\Form\Event;
  3933.  
  3934. use Symfony\Component\Form\Exception\BadMethodCallException;
  3935. use 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.  */
  3943. final 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 <[email protected]>
  3960.  *
  3961.  * For the full copyright and license information, please view the LICENSE
  3962.  * file that was distributed with this source code.
  3963.  */
  3964.  
  3965. namespace Symfony\Component\Form\Event;
  3966.  
  3967. use Symfony\Component\Form\Exception\BadMethodCallException;
  3968. use 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.  */
  3976. final 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 <[email protected]>
  3993.  *
  3994.  * For the full copyright and license information, please view the LICENSE
  3995.  * file that was distributed with this source code.
  3996.  */
  3997.  
  3998. namespace Symfony\Component\Form\Event;
  3999.  
  4000. use 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.  */
  4008. final 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 <[email protected]>
  4021.  *
  4022.  * For the full copyright and license information, please view the LICENSE
  4023.  * file that was distributed with this source code.
  4024.  */
  4025.  
  4026. namespace Symfony\Component\Form\Util;
  4027.  
  4028. /**
  4029.  * Iterator for {@link OrderedHashMap} objects.
  4030.  *
  4031.  * @author Bernhard Schussek <[email protected]>
  4032.  *
  4033.  * @internal
  4034.  *
  4035.  * @template-covariant TValue
  4036.  *
  4037.  * @implements \Iterator<string, TValue>
  4038.  */
  4039. class 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 <[email protected]>
  4140.  *
  4141.  * For the full copyright and license information, please view the LICENSE
  4142.  * file that was distributed with this source code.
  4143.  */
  4144.  
  4145. namespace Symfony\Component\Form\Util;
  4146.  
  4147. use Symfony\Component\HttpFoundation\RequestStack;
  4148.  
  4149. /**
  4150.  * @author Bernhard Schussek <[email protected]>
  4151.  */
  4152. class 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 <[email protected]>
  4235.  *
  4236.  * For the full copyright and license information, please view the LICENSE
  4237.  * file that was distributed with this source code.
  4238.  */
  4239.  
  4240. namespace 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 <[email protected]>
  4253.  */
  4254. class 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 <[email protected]>
  4276.  *
  4277.  * For the full copyright and license information, please view the LICENSE
  4278.  * file that was distributed with this source code.
  4279.  */
  4280.  
  4281. namespace Symfony\Component\Form\Util;
  4282.  
  4283. use Symfony\Component\OptionsResolver\Exception\AccessException;
  4284. use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
  4285. use Symfony\Component\OptionsResolver\OptionsResolver;
  4286.  
  4287. /**
  4288.  * @author Yonel Ceruto <[email protected]>
  4289.  *
  4290.  * @internal
  4291.  */
  4292. class 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 <[email protected]>
  4390.  *
  4391.  * For the full copyright and license information, please view the LICENSE
  4392.  * file that was distributed with this source code.
  4393.  */
  4394.  
  4395. namespace 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 <[email protected]>
  4449.  *
  4450.  * @template TValue
  4451.  *
  4452.  * @implements \ArrayAccess<string, TValue>
  4453.  * @implements \IteratorAggregate<string, TValue>
  4454.  */
  4455. class 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 <[email protected]>
  4549.  *
  4550.  * For the full copyright and license information, please view the LICENSE
  4551.  * file that was distributed with this source code.
  4552.  */
  4553.  
  4554. namespace Symfony\Component\Form\Util;
  4555.  
  4556. /**
  4557.  * @author Issei Murasawa <[email protected]>
  4558.  * @author Bernhard Schussek <[email protected]>
  4559.  */
  4560. class 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 <[email protected]>
  4606.  *
  4607.  * For the full copyright and license information, please view the LICENSE
  4608.  * file that was distributed with this source code.
  4609.  */
  4610.  
  4611. namespace Symfony\Component\Form\Util;
  4612.  
  4613. /**
  4614.  * @author Bernhard Schussek <[email protected]>
  4615.  */
  4616. class 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 <[email protected]>
  4678.  *
  4679.  * For the full copyright and license information, please view the LICENSE
  4680.  * file that was distributed with this source code.
  4681.  */
  4682.  
  4683. namespace Symfony\Component\Form;
  4684.  
  4685. /**
  4686.  * @author Bernhard Schussek <[email protected]>
  4687.  */
  4688. interface 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 <[email protected]>
  4720.  *
  4721.  * For the full copyright and license information, please view the LICENSE
  4722.  * file that was distributed with this source code.
  4723.  */
  4724.  
  4725. namespace Symfony\Component\Form\ChoiceList\Loader;
  4726.  
  4727. /**
  4728.  * Callback choice loader optimized for Intl choice types.
  4729.  *
  4730.  * @author Jules Pietri <[email protected]>
  4731.  * @author Yonel Ceruto <[email protected]>
  4732.  */
  4733. class 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 <[email protected]>
  4762.  *
  4763.  * For the full copyright and license information, please view the LICENSE
  4764.  * file that was distributed with this source code.
  4765.  */
  4766.  
  4767. namespace Symfony\Component\Form\ChoiceList\Loader;
  4768.  
  4769. use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
  4770. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  4771.  
  4772. /**
  4773.  * @author Jules Pietri <[email protected]>
  4774.  */
  4775. abstract 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 <[email protected]>
  4832.  *
  4833.  * For the full copyright and license information, please view the LICENSE
  4834.  * file that was distributed with this source code.
  4835.  */
  4836.  
  4837. namespace 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 <[email protected]>
  4843.  */
  4844. class 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 <[email protected]>
  4900.  *
  4901.  * For the full copyright and license information, please view the LICENSE
  4902.  * file that was distributed with this source code.
  4903.  */
  4904.  
  4905. namespace Symfony\Component\Form\ChoiceList\Loader;
  4906.  
  4907. use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
  4908. use 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 <[email protected]>
  4914.  */
  4915. class 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 <[email protected]>
  4958.  *
  4959.  * For the full copyright and license information, please view the LICENSE
  4960.  * file that was distributed with this source code.
  4961.  */
  4962.  
  4963. namespace Symfony\Component\Form\ChoiceList\Loader;
  4964.  
  4965. /**
  4966.  * Loads an {@link ArrayChoiceList} instance from a callable returning iterable choices.
  4967.  *
  4968.  * @author Jules Pietri <[email protected]>
  4969.  */
  4970. class 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 <[email protected]>
  4997.  *
  4998.  * For the full copyright and license information, please view the LICENSE
  4999.  * file that was distributed with this source code.
  5000.  */
  5001.  
  5002. namespace Symfony\Component\Form\ChoiceList\Loader;
  5003.  
  5004. use 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 <[email protected]>
  5014.  */
  5015. interface 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 <[email protected]>
  5073.  *
  5074.  * For the full copyright and license information, please view the LICENSE
  5075.  * file that was distributed with this source code.
  5076.  */
  5077.  
  5078. namespace Symfony\Component\Form\ChoiceList;
  5079.  
  5080. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
  5081. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
  5082. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFilter;
  5083. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
  5084. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
  5085. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceTranslationParameters;
  5086. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
  5087. use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy;
  5088. use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice;
  5089. use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
  5090. use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
  5091. use Symfony\Component\Form\FormTypeExtensionInterface;
  5092. use Symfony\Component\Form\FormTypeInterface;
  5093.  
  5094. /**
  5095.  * A set of convenient static methods to create cacheable choice list options.
  5096.  *
  5097.  * @author Jules Pietri <[email protected]>
  5098.  */
  5099. final 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 <[email protected]>
  5226.  *
  5227.  * For the full copyright and license information, please view the LICENSE
  5228.  * file that was distributed with this source code.
  5229.  */
  5230.  
  5231. namespace 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 <[email protected]>
  5244.  */
  5245. interface 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 <[email protected]>
  5368.  *
  5369.  * For the full copyright and license information, please view the LICENSE
  5370.  * file that was distributed with this source code.
  5371.  */
  5372.  
  5373. namespace Symfony\Component\Form\ChoiceList;
  5374.  
  5375. use 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 <[email protected]>
  5388.  */
  5389. class 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 <[email protected]>
  5455.  *
  5456.  * For the full copyright and license information, please view the LICENSE
  5457.  * file that was distributed with this source code.
  5458.  */
  5459.  
  5460. namespace 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 <[email protected]>
  5472.  */
  5473. class 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 <[email protected]>
  5669.  *
  5670.  * For the full copyright and license information, please view the LICENSE
  5671.  * file that was distributed with this source code.
  5672.  */
  5673.  
  5674. namespace Symfony\Component\Form\ChoiceList\Factory;
  5675.  
  5676. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  5677. use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
  5678. use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
  5679. use 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 <[email protected]>
  5688.  * @author Jules Pietri <[email protected]>
  5689.  */
  5690. class 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 <[email protected]>
  5896.  *
  5897.  * For the full copyright and license information, please view the LICENSE
  5898.  * file that was distributed with this source code.
  5899.  */
  5900.  
  5901. namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
  5902.  
  5903. use Symfony\Component\Form\FormTypeExtensionInterface;
  5904. use 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 <[email protected]>
  5913.  */
  5914. final 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 <[email protected]>
  5927.  *
  5928.  * For the full copyright and license information, please view the LICENSE
  5929.  * file that was distributed with this source code.
  5930.  */
  5931.  
  5932. namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
  5933.  
  5934. use Symfony\Component\Form\FormTypeExtensionInterface;
  5935. use 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 <[email protected]>
  5944.  */
  5945. final 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 <[email protected]>
  5958.  *
  5959.  * For the full copyright and license information, please view the LICENSE
  5960.  * file that was distributed with this source code.
  5961.  */
  5962.  
  5963. namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
  5964.  
  5965. use Symfony\Component\Form\FormTypeExtensionInterface;
  5966. use 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 <[email protected]>
  5975.  */
  5976. final 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 <[email protected]>
  5989.  *
  5990.  * For the full copyright and license information, please view the LICENSE
  5991.  * file that was distributed with this source code.
  5992.  */
  5993.  
  5994. namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
  5995.  
  5996. use Symfony\Component\Form\FormTypeExtensionInterface;
  5997. use 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 <[email protected]>
  6006.  */
  6007. final 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 <[email protected]>
  6020.  *
  6021.  * For the full copyright and license information, please view the LICENSE
  6022.  * file that was distributed with this source code.
  6023.  */
  6024.  
  6025. namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
  6026.  
  6027. use Symfony\Component\Form\FormTypeExtensionInterface;
  6028. use 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 <[email protected]>
  6037.  */
  6038. final 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 <[email protected]>
  6051.  *
  6052.  * For the full copyright and license information, please view the LICENSE
  6053.  * file that was distributed with this source code.
  6054.  */
  6055.  
  6056. namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
  6057.  
  6058. use Symfony\Component\Form\FormTypeExtensionInterface;
  6059. use 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 <[email protected]>
  6068.  */
  6069. final 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 <[email protected]>
  6082.  *
  6083.  * For the full copyright and license information, please view the LICENSE
  6084.  * file that was distributed with this source code.
  6085.  */
  6086.  
  6087. namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
  6088.  
  6089. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  6090. use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
  6091. use Symfony\Component\Form\FormTypeExtensionInterface;
  6092. use 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 <[email protected]>
  6101.  */
  6102. final 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 <[email protected]>
  6129.  *
  6130.  * For the full copyright and license information, please view the LICENSE
  6131.  * file that was distributed with this source code.
  6132.  */
  6133.  
  6134. namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
  6135.  
  6136. use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
  6137. use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
  6138. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  6139. use Symfony\Component\Form\FormTypeExtensionInterface;
  6140. use 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 <[email protected]>
  6150.  */
  6151. abstract 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 <[email protected]>
  6188.  *
  6189.  * For the full copyright and license information, please view the LICENSE
  6190.  * file that was distributed with this source code.
  6191.  */
  6192.  
  6193. namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
  6194.  
  6195. use Symfony\Component\Form\FormTypeExtensionInterface;
  6196. use 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 <[email protected]>
  6205.  */
  6206. final 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 <[email protected]>
  6219.  *
  6220.  * For the full copyright and license information, please view the LICENSE
  6221.  * file that was distributed with this source code.
  6222.  */
  6223.  
  6224. namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
  6225.  
  6226. use Symfony\Component\Form\FormTypeExtensionInterface;
  6227. use 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 <[email protected]>
  6236.  */
  6237. final 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 <[email protected]>
  6250.  *
  6251.  * For the full copyright and license information, please view the LICENSE
  6252.  * file that was distributed with this source code.
  6253.  */
  6254.  
  6255. namespace Symfony\Component\Form\ChoiceList\Factory;
  6256.  
  6257. use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
  6258. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  6259. use Symfony\Component\Form\ChoiceList\LazyChoiceList;
  6260. use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
  6261. use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
  6262. use Symfony\Component\Form\ChoiceList\Loader\FilterChoiceLoaderDecorator;
  6263. use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
  6264. use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
  6265. use Symfony\Component\Form\ChoiceList\View\ChoiceView;
  6266. use Symfony\Contracts\Translation\TranslatableInterface;
  6267.  
  6268. /**
  6269.  * Default implementation of {@link ChoiceListFactoryInterface}.
  6270.  *
  6271.  * @author Bernhard Schussek <[email protected]>
  6272.  * @author Jules Pietri <[email protected]>
  6273.  */
  6274. class 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 <[email protected]>
  6557.  *
  6558.  * For the full copyright and license information, please view the LICENSE
  6559.  * file that was distributed with this source code.
  6560.  */
  6561.  
  6562. namespace Symfony\Component\Form\ChoiceList\Factory;
  6563.  
  6564. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  6565. use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
  6566. use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
  6567.  
  6568. /**
  6569.  * Creates {@link ChoiceListInterface} instances.
  6570.  *
  6571.  * @author Bernhard Schussek <[email protected]>
  6572.  */
  6573. interface 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 <[email protected]>
  6646.  *
  6647.  * For the full copyright and license information, please view the LICENSE
  6648.  * file that was distributed with this source code.
  6649.  */
  6650.  
  6651. namespace Symfony\Component\Form\ChoiceList\Factory;
  6652.  
  6653. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  6654. use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
  6655. use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
  6656. use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
  6657. use Symfony\Component\PropertyAccess\PropertyAccess;
  6658. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  6659. use Symfony\Component\PropertyAccess\PropertyPath;
  6660. use 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 <[email protected]>
  6677.  */
  6678. class 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 <[email protected]>
  6839.  *
  6840.  * For the full copyright and license information, please view the LICENSE
  6841.  * file that was distributed with this source code.
  6842.  */
  6843.  
  6844. namespace Symfony\Component\Form\ChoiceList\View;
  6845.  
  6846. use Symfony\Contracts\Translation\TranslatableInterface;
  6847.  
  6848. /**
  6849.  * Represents a choice in templates.
  6850.  *
  6851.  * @author Bernhard Schussek <[email protected]>
  6852.  */
  6853. class 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 <[email protected]>
  6883.  *
  6884.  * For the full copyright and license information, please view the LICENSE
  6885.  * file that was distributed with this source code.
  6886.  */
  6887.  
  6888. namespace 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 <[email protected]>
  6898.  */
  6899. class 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 <[email protected]>
  6941.  *
  6942.  * For the full copyright and license information, please view the LICENSE
  6943.  * file that was distributed with this source code.
  6944.  */
  6945.  
  6946. namespace Symfony\Component\Form\ChoiceList\View;
  6947.  
  6948. /**
  6949.  * Represents a group of choices in templates.
  6950.  *
  6951.  * @author Bernhard Schussek <[email protected]>
  6952.  *
  6953.  * @implements \IteratorAggregate<array-key, ChoiceGroupView|ChoiceView>
  6954.  */
  6955. class 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 <[email protected]>
  6986.  *
  6987.  * For the full copyright and license information, please view the LICENSE
  6988.  * file that was distributed with this source code.
  6989.  */
  6990.  
  6991. namespace Symfony\Component\Form;
  6992.  
  6993. /**
  6994.  * Submits forms if they were submitted.
  6995.  *
  6996.  * @author Bernhard Schussek <[email protected]>
  6997.  */
  6998. interface 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 <[email protected]>
  7020.  *
  7021.  * For the full copyright and license information, please view the LICENSE
  7022.  * file that was distributed with this source code.
  7023.  */
  7024.  
  7025. namespace 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 <[email protected]>
  7032.  */
  7033. class 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 <[email protected]>
  7076.  *
  7077.  * For the full copyright and license information, please view the LICENSE
  7078.  * file that was distributed with this source code.
  7079.  */
  7080.  
  7081. namespace Symfony\Component\Form\Guess;
  7082.  
  7083. use 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 <[email protected]>
  7093.  */
  7094. abstract 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 <[email protected]>
  7181.  *
  7182.  * For the full copyright and license information, please view the LICENSE
  7183.  * file that was distributed with this source code.
  7184.  */
  7185.  
  7186. namespace Symfony\Component\Form\Guess;
  7187.  
  7188. /**
  7189.  * Contains a guessed value.
  7190.  *
  7191.  * @author Bernhard Schussek <[email protected]>
  7192.  */
  7193. class 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 <[email protected]>
  7223.  *
  7224.  * For the full copyright and license information, please view the LICENSE
  7225.  * file that was distributed with this source code.
  7226.  */
  7227.  
  7228. namespace Symfony\Component\Form;
  7229.  
  7230. use Symfony\Component\Form\Extension\Core\CoreExtension;
  7231.  
  7232. /**
  7233.  * The default implementation of FormFactoryBuilderInterface.
  7234.  *
  7235.  * @author Bernhard Schussek <[email protected]>
  7236.  */
  7237. class 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 <[email protected]>
  7379.  *
  7380.  * For the full copyright and license information, please view the LICENSE
  7381.  * file that was distributed with this source code.
  7382.  */
  7383.  
  7384. namespace Symfony\Component\Form;
  7385.  
  7386. use Symfony\Component\Form\Exception\InvalidArgumentException;
  7387. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  7388.  
  7389. /**
  7390.  * @author Bernhard Schussek <[email protected]>
  7391.  */
  7392. abstract 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 <[email protected]>
  7560.  *
  7561.  * For the full copyright and license information, please view the LICENSE
  7562.  * file that was distributed with this source code.
  7563.  */
  7564.  
  7565. namespace Symfony\Component\Form;
  7566.  
  7567. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  7568. use Symfony\Component\Form\Util\FormUtil;
  7569. use Symfony\Component\Form\Util\ServerParams;
  7570.  
  7571. /**
  7572.  * A request handler using PHP super globals $_GET, $_POST and $_SERVER.
  7573.  *
  7574.  * @author Bernhard Schussek <[email protected]>
  7575.  */
  7576. class 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 <[email protected]>
  7804.  *
  7805.  * For the full copyright and license information, please view the LICENSE
  7806.  * file that was distributed with this source code.
  7807.  */
  7808.  
  7809. namespace Symfony\Component\Form;
  7810.  
  7811. /**
  7812.  * A button that submits the form.
  7813.  *
  7814.  * @author Bernhard Schussek <[email protected]>
  7815.  */
  7816. class 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 <[email protected]>
  7857.  *
  7858.  * For the full copyright and license information, please view the LICENSE
  7859.  * file that was distributed with this source code.
  7860.  */
  7861.  
  7862. namespace Symfony\Component\Form\Test\Traits;
  7863.  
  7864. use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
  7865. use Symfony\Component\Form\Test\TypeTestCase;
  7866. use Symfony\Component\Validator\ConstraintViolationList;
  7867. use Symfony\Component\Validator\Mapping\ClassMetadata;
  7868. use Symfony\Component\Validator\Validator\ValidatorInterface;
  7869.  
  7870. trait 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 <[email protected]>
  7902.  *
  7903.  * For the full copyright and license information, please view the LICENSE
  7904.  * file that was distributed with this source code.
  7905.  */
  7906.  
  7907. namespace 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 <[email protected]>
  7916.  */
  7917. abstract 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 <[email protected]>
  7969.  *
  7970.  * For the full copyright and license information, please view the LICENSE
  7971.  * file that was distributed with this source code.
  7972.  */
  7973.  
  7974. namespace Symfony\Component\Form\Test;
  7975.  
  7976. use Symfony\Component\Form\FormBuilderInterface as BaseFormBuilderInterface;
  7977.  
  7978. interface 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 <[email protected]>
  7991.  *
  7992.  * For the full copyright and license information, please view the LICENSE
  7993.  * file that was distributed with this source code.
  7994.  */
  7995.  
  7996. namespace Symfony\Component\Form\Test;
  7997.  
  7998. use PHPUnit\Framework\TestCase;
  7999. use Symfony\Component\Form\FormExtensionInterface;
  8000. use Symfony\Component\Form\FormFactoryInterface;
  8001. use Symfony\Component\Form\Forms;
  8002. use Symfony\Component\Form\FormTypeExtensionInterface;
  8003. use Symfony\Component\Form\FormTypeGuesserInterface;
  8004. use Symfony\Component\Form\FormTypeInterface;
  8005.  
  8006. /**
  8007.  * @author Bernhard Schussek <[email protected]>
  8008.  */
  8009. abstract 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 <[email protected]>
  8065.  *
  8066.  * For the full copyright and license information, please view the LICENSE
  8067.  * file that was distributed with this source code.
  8068.  */
  8069.  
  8070. namespace Symfony\Component\Form\Test;
  8071.  
  8072. use Symfony\Component\Form\FormInterface as BaseFormInterface;
  8073.  
  8074. interface 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 <[email protected]>
  8087.  *
  8088.  * For the full copyright and license information, please view the LICENSE
  8089.  * file that was distributed with this source code.
  8090.  */
  8091.  
  8092. namespace Symfony\Component\Form\Test;
  8093.  
  8094. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  8095. use Symfony\Component\Form\FormBuilder;
  8096. use Symfony\Component\Form\FormExtensionInterface;
  8097. use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
  8098.  
  8099. abstract 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 <[email protected]>
  8152.  *
  8153.  * For the full copyright and license information, please view the LICENSE
  8154.  * file that was distributed with this source code.
  8155.  */
  8156.  
  8157. namespace Symfony\Component\Form;
  8158.  
  8159. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  8160. use Symfony\Component\PropertyAccess\PropertyPathInterface;
  8161.  
  8162. /**
  8163.  * The configuration of a {@link Form} object.
  8164.  *
  8165.  * @author Bernhard Schussek <[email protected]>
  8166.  */
  8167. interface 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 <[email protected]>
  8352.  *
  8353.  * For the full copyright and license information, please view the LICENSE
  8354.  * file that was distributed with this source code.
  8355.  */
  8356.  
  8357. namespace Symfony\Component\Form;
  8358.  
  8359. use Symfony\Component\Form\Exception\AlreadySubmittedException;
  8360. use Symfony\Component\Form\Exception\BadMethodCallException;
  8361. use Symfony\Component\Form\Exception\TransformationFailedException;
  8362. use Symfony\Component\PropertyAccess\PropertyPathInterface;
  8363.  
  8364. /**
  8365.  * A form button.
  8366.  *
  8367.  * @author Bernhard Schussek <[email protected]>
  8368.  *
  8369.  * @implements \IteratorAggregate<string, FormInterface>
  8370.  */
  8371. class 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 <[email protected]>
  8725.  *
  8726.  * For the full copyright and license information, please view the LICENSE
  8727.  * file that was distributed with this source code.
  8728.  */
  8729.  
  8730. namespace Symfony\Component\Form;
  8731.  
  8732. /**
  8733.  * @author Bernhard Schussek <[email protected]>
  8734.  */
  8735. interface 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 <[email protected]>
  8791.  *
  8792.  * For the full copyright and license information, please view the LICENSE
  8793.  * file that was distributed with this source code.
  8794.  */
  8795.  
  8796. namespace Symfony\Component\Form;
  8797.  
  8798. use Symfony\Component\Form\Exception\BadMethodCallException;
  8799. use Symfony\Component\Form\Exception\InvalidArgumentException;
  8800. use Symfony\Component\Form\Exception\LogicException;
  8801. use Symfony\Component\Form\Exception\OutOfBoundsException;
  8802. use 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 <[email protected]>
  8815.  *
  8816.  * @template T of FormError|FormErrorIterator
  8817.  *
  8818.  * @implements \ArrayAccess<int, T>
  8819.  * @implements \RecursiveIterator<int, T>
  8820.  * @implements \SeekableIterator<int, T>
  8821.  */
  8822. class 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 <[email protected]>
  9069.  *
  9070.  * For the full copyright and license information, please view the LICENSE
  9071.  * file that was distributed with this source code.
  9072.  */
  9073.  
  9074. namespace Symfony\Component\Form;
  9075.  
  9076. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  9077. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  9078. use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
  9079. use Symfony\Component\Form\Exception\BadMethodCallException;
  9080. use Symfony\Component\Form\Exception\InvalidArgumentException;
  9081. use Symfony\Component\PropertyAccess\PropertyPath;
  9082. use Symfony\Component\PropertyAccess\PropertyPathInterface;
  9083.  
  9084. /**
  9085.  * A basic form configuration.
  9086.  *
  9087.  * @author Bernhard Schussek <[email protected]>
  9088.  */
  9089. class 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 <[email protected]>
  9726.  *
  9727.  * For the full copyright and license information, please view the LICENSE
  9728.  * file that was distributed with this source code.
  9729.  */
  9730.  
  9731. namespace Symfony\Component\Form;
  9732.  
  9733. use Symfony\Component\Form\Exception\ExceptionInterface;
  9734. use Symfony\Component\Form\Exception\InvalidArgumentException;
  9735. use Symfony\Component\Form\Exception\LogicException;
  9736. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  9737.  
  9738. /**
  9739.  * The central registry of the Form component.
  9740.  *
  9741.  * @author Bernhard Schussek <[email protected]>
  9742.  */
  9743. class 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 <[email protected]>
  9886.  *
  9887.  * For the full copyright and license information, please view the LICENSE
  9888.  * file that was distributed with this source code.
  9889.  */
  9890.  
  9891. namespace Symfony\Component\Form;
  9892.  
  9893. use Symfony\Component\Form\Exception\BadMethodCallException;
  9894.  
  9895. /**
  9896.  * @author Bernhard Schussek <[email protected]>
  9897.  *
  9898.  * @implements \ArrayAccess<int|string, FormView>
  9899.  * @implements \IteratorAggregate<int|string, FormView>
  9900.  */
  9901. class 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 <[email protected]>
  10042.  *
  10043.  * For the full copyright and license information, please view the LICENSE
  10044.  * file that was distributed with this source code.
  10045.  */
  10046.  
  10047. namespace Symfony\Component\Form;
  10048.  
  10049. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  10050. use Symfony\Component\Form\Exception\BadMethodCallException;
  10051. use Symfony\Component\Form\Exception\InvalidArgumentException;
  10052. use Symfony\Component\PropertyAccess\PropertyPathInterface;
  10053.  
  10054. /**
  10055.  * A builder for {@link Button} instances.
  10056.  *
  10057.  * @author Bernhard Schussek <[email protected]>
  10058.  *
  10059.  * @implements \IteratorAggregate<string, FormBuilderInterface>
  10060.  */
  10061. class 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 <[email protected]>
  10715.  *
  10716.  * For the full copyright and license information, please view the LICENSE
  10717.  * file that was distributed with this source code.
  10718.  */
  10719.  
  10720. namespace 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 <[email protected]>
  10726.  */
  10727. interface 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 <[email protected]>
  10769.  *
  10770.  * For the full copyright and license information, please view the LICENSE
  10771.  * file that was distributed with this source code.
  10772.  */
  10773.  
  10774. namespace 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 <[email protected]>
  10783.  */
  10784. class 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 <[email protected]>
  10811.  *
  10812.  * For the full copyright and license information, please view the LICENSE
  10813.  * file that was distributed with this source code.
  10814.  */
  10815.  
  10816. namespace Symfony\Component\Form;
  10817.  
  10818. /**
  10819.  * Adapter for rendering form templates with a specific templating engine.
  10820.  *
  10821.  * @author Bernhard Schussek <[email protected]>
  10822.  */
  10823. interface 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 <[email protected]>
  10947.  *
  10948.  * For the full copyright and license information, please view the LICENSE
  10949.  * file that was distributed with this source code.
  10950.  */
  10951.  
  10952. namespace Symfony\Component\Form;
  10953.  
  10954. use Symfony\Component\Form\Exception\InvalidArgumentException;
  10955.  
  10956. /**
  10957.  * A form extension with preloaded types, type extensions and type guessers.
  10958.  *
  10959.  * @author Bernhard Schussek <[email protected]>
  10960.  */
  10961. class 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 <[email protected]>
  11021.  *
  11022.  * For the full copyright and license information, please view the LICENSE
  11023.  * file that was distributed with this source code.
  11024.  */
  11025.  
  11026. namespace Symfony\Component\Form;
  11027.  
  11028. use Symfony\Component\Form\Exception\TransformationFailedException;
  11029.  
  11030. /**
  11031.  * Transforms a value between different representations.
  11032.  *
  11033.  * @author Bernhard Schussek <[email protected]>
  11034.  *
  11035.  * @template TValue
  11036.  * @template TTransformedValue
  11037.  */
  11038. interface 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 <[email protected]>
  11120.  *
  11121.  * For the full copyright and license information, please view the LICENSE
  11122.  * file that was distributed with this source code.
  11123.  */
  11124.  
  11125. namespace Symfony\Component\Form;
  11126.  
  11127. /**
  11128.  * A type that should be converted into a {@link SubmitButton} instance.
  11129.  *
  11130.  * @author Bernhard Schussek <[email protected]>
  11131.  */
  11132. interface 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 <[email protected]>
  11145.  *
  11146.  * For the full copyright and license information, please view the LICENSE
  11147.  * file that was distributed with this source code.
  11148.  */
  11149.  
  11150. namespace 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 <[email protected]>
  11160.  */
  11161. interface 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 <[email protected]>
  11183.  *
  11184.  * For the full copyright and license information, please view the LICENSE
  11185.  * file that was distributed with this source code.
  11186.  */
  11187.  
  11188. namespace Symfony\Component\Form\Console\Helper;
  11189.  
  11190. use Symfony\Component\Console\Helper\DescriptorHelper as BaseDescriptorHelper;
  11191. use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
  11192. use Symfony\Component\Form\Console\Descriptor\JsonDescriptor;
  11193. use Symfony\Component\Form\Console\Descriptor\TextDescriptor;
  11194.  
  11195. /**
  11196.  * @author Yonel Ceruto <[email protected]>
  11197.  *
  11198.  * @internal
  11199.  */
  11200. class 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 <[email protected]>
  11220.  *
  11221.  * For the full copyright and license information, please view the LICENSE
  11222.  * file that was distributed with this source code.
  11223.  */
  11224.  
  11225. namespace Symfony\Component\Form\Console\Descriptor;
  11226.  
  11227. use Symfony\Component\Form\ResolvedFormTypeInterface;
  11228. use Symfony\Component\OptionsResolver\OptionsResolver;
  11229.  
  11230. /**
  11231.  * @author Yonel Ceruto <[email protected]>
  11232.  *
  11233.  * @internal
  11234.  */
  11235. class 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 <[email protected]>
  11342.  *
  11343.  * For the full copyright and license information, please view the LICENSE
  11344.  * file that was distributed with this source code.
  11345.  */
  11346.  
  11347. namespace Symfony\Component\Form\Console\Descriptor;
  11348.  
  11349. use Symfony\Component\Console\Helper\Dumper;
  11350. use Symfony\Component\Console\Helper\TableSeparator;
  11351. use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
  11352. use Symfony\Component\Form\ResolvedFormTypeInterface;
  11353. use Symfony\Component\OptionsResolver\OptionsResolver;
  11354.  
  11355. /**
  11356.  * @author Yonel Ceruto <[email protected]>
  11357.  *
  11358.  * @internal
  11359.  */
  11360. class 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 <[email protected]>
  11562.  *
  11563.  * For the full copyright and license information, please view the LICENSE
  11564.  * file that was distributed with this source code.
  11565.  */
  11566.  
  11567. namespace Symfony\Component\Form\Console\Descriptor;
  11568.  
  11569. use Symfony\Component\Console\Descriptor\DescriptorInterface;
  11570. use Symfony\Component\Console\Input\ArrayInput;
  11571. use Symfony\Component\Console\Output\OutputInterface;
  11572. use Symfony\Component\Console\Style\OutputStyle;
  11573. use Symfony\Component\Console\Style\SymfonyStyle;
  11574. use Symfony\Component\Form\ResolvedFormTypeInterface;
  11575. use Symfony\Component\Form\Util\OptionsResolverWrapper;
  11576. use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector;
  11577. use Symfony\Component\OptionsResolver\Exception\NoConfigurationException;
  11578. use Symfony\Component\OptionsResolver\OptionsResolver;
  11579.  
  11580. /**
  11581.  * @author Yonel Ceruto <[email protected]>
  11582.  *
  11583.  * @internal
  11584.  */
  11585. abstract 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 <[email protected]>
  11761.  *
  11762.  * For the full copyright and license information, please view the LICENSE
  11763.  * file that was distributed with this source code.
  11764.  */
  11765.  
  11766. namespace Symfony\Component\Form;
  11767.  
  11768. /**
  11769.  * A clickable form element.
  11770.  *
  11771.  * @author Bernhard Schussek <[email protected]>
  11772.  */
  11773. interface 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 <[email protected]>
  11790.  *
  11791.  * For the full copyright and license information, please view the LICENSE
  11792.  * file that was distributed with this source code.
  11793.  */
  11794.  
  11795. namespace Symfony\Component\Form;
  11796.  
  11797. /**
  11798.  * A type that should be converted into a {@link Button} instance.
  11799.  *
  11800.  * @author Bernhard Schussek <[email protected]>
  11801.  */
  11802. interface 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 <[email protected]>
  11815.  *
  11816.  * For the full copyright and license information, please view the LICENSE
  11817.  * file that was distributed with this source code.
  11818.  */
  11819.  
  11820. namespace Symfony\Component\Form\Extension\DependencyInjection;
  11821.  
  11822. use Psr\Container\ContainerInterface;
  11823. use Symfony\Component\Form\Exception\InvalidArgumentException;
  11824. use Symfony\Component\Form\FormExtensionInterface;
  11825. use Symfony\Component\Form\FormTypeExtensionInterface;
  11826. use Symfony\Component\Form\FormTypeGuesserChain;
  11827. use Symfony\Component\Form\FormTypeGuesserInterface;
  11828. use Symfony\Component\Form\FormTypeInterface;
  11829.  
  11830. class 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 <[email protected]>
  11915.  *
  11916.  * For the full copyright and license information, please view the LICENSE
  11917.  * file that was distributed with this source code.
  11918.  */
  11919.  
  11920. namespace Symfony\Component\Form\Extension\DataCollector;
  11921.  
  11922. use Symfony\Component\Form\AbstractExtension;
  11923.  
  11924. /**
  11925.  * Extension for collecting data of the forms on a page.
  11926.  *
  11927.  * @author Robert Schönthal <[email protected]>
  11928.  * @author Bernhard Schussek <[email protected]>
  11929.  */
  11930. class 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 <[email protected]>
  11954.  *
  11955.  * For the full copyright and license information, please view the LICENSE
  11956.  * file that was distributed with this source code.
  11957.  */
  11958.  
  11959. namespace Symfony\Component\Form\Extension\DataCollector\Proxy;
  11960.  
  11961. use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
  11962. use Symfony\Component\Form\FormTypeInterface;
  11963. use Symfony\Component\Form\ResolvedFormTypeFactoryInterface;
  11964. use Symfony\Component\Form\ResolvedFormTypeInterface;
  11965.  
  11966. /**
  11967.  * Proxy that wraps resolved types into {@link ResolvedTypeDataCollectorProxy}
  11968.  * instances.
  11969.  *
  11970.  * @author Bernhard Schussek <[email protected]>
  11971.  */
  11972. class 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 <[email protected]>
  11998.  *
  11999.  * For the full copyright and license information, please view the LICENSE
  12000.  * file that was distributed with this source code.
  12001.  */
  12002.  
  12003. namespace Symfony\Component\Form\Extension\DataCollector\Proxy;
  12004.  
  12005. use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
  12006. use Symfony\Component\Form\FormBuilderInterface;
  12007. use Symfony\Component\Form\FormFactoryInterface;
  12008. use Symfony\Component\Form\FormInterface;
  12009. use Symfony\Component\Form\FormTypeInterface;
  12010. use Symfony\Component\Form\FormView;
  12011. use Symfony\Component\Form\ResolvedFormTypeInterface;
  12012. use 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 <[email protected]>
  12018.  */
  12019. class 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 <[email protected]>
  12111.  *
  12112.  * For the full copyright and license information, please view the LICENSE
  12113.  * file that was distributed with this source code.
  12114.  */
  12115.  
  12116. namespace Symfony\Component\Form\Extension\DataCollector\Type;
  12117.  
  12118. use Symfony\Component\Form\AbstractTypeExtension;
  12119. use Symfony\Component\Form\Extension\Core\Type\FormType;
  12120. use Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener;
  12121. use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
  12122. use Symfony\Component\Form\FormBuilderInterface;
  12123.  
  12124. /**
  12125.  * Type extension for collecting data of a form with this type.
  12126.  *
  12127.  * @author Robert Schönthal <[email protected]>
  12128.  * @author Bernhard Schussek <[email protected]>
  12129.  */
  12130. class 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 <[email protected]>
  12159.  *
  12160.  * For the full copyright and license information, please view the LICENSE
  12161.  * file that was distributed with this source code.
  12162.  */
  12163.  
  12164. namespace Symfony\Component\Form\Extension\DataCollector\EventListener;
  12165.  
  12166. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  12167. use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
  12168. use Symfony\Component\Form\FormEvent;
  12169. use 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 <[email protected]>
  12176.  */
  12177. class 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 <[email protected]>
  12233.  *
  12234.  * For the full copyright and license information, please view the LICENSE
  12235.  * file that was distributed with this source code.
  12236.  */
  12237.  
  12238. namespace Symfony\Component\Form\Extension\DataCollector;
  12239.  
  12240. use Symfony\Component\Form\FormInterface;
  12241. use Symfony\Component\Form\FormView;
  12242. use Symfony\Component\Validator\ConstraintViolationInterface;
  12243.  
  12244. /**
  12245.  * Default implementation of {@link FormDataExtractorInterface}.
  12246.  *
  12247.  * @author Bernhard Schussek <[email protected]>
  12248.  */
  12249. class 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 <[email protected]>
  12391.  *
  12392.  * For the full copyright and license information, please view the LICENSE
  12393.  * file that was distributed with this source code.
  12394.  */
  12395.  
  12396. namespace Symfony\Component\Form\Extension\DataCollector;
  12397.  
  12398. use Symfony\Component\Form\FormInterface;
  12399. use Symfony\Component\Form\FormView;
  12400. use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
  12401. use Symfony\Component\VarDumper\Cloner\Data;
  12402.  
  12403. /**
  12404.  * Collects and structures information about forms.
  12405.  *
  12406.  * @author Bernhard Schussek <[email protected]>
  12407.  */
  12408. interface 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 <[email protected]>
  12478.  *
  12479.  * For the full copyright and license information, please view the LICENSE
  12480.  * file that was distributed with this source code.
  12481.  */
  12482.  
  12483. namespace Symfony\Component\Form\Extension\DataCollector;
  12484.  
  12485. use Symfony\Component\Form\FormInterface;
  12486. use Symfony\Component\Form\FormView;
  12487. use Symfony\Component\HttpFoundation\Request;
  12488. use Symfony\Component\HttpFoundation\Response;
  12489. use Symfony\Component\HttpKernel\DataCollector\DataCollector;
  12490. use Symfony\Component\Validator\ConstraintViolationInterface;
  12491. use Symfony\Component\VarDumper\Caster\Caster;
  12492. use Symfony\Component\VarDumper\Caster\ClassStub;
  12493. use Symfony\Component\VarDumper\Caster\StubCaster;
  12494. use Symfony\Component\VarDumper\Cloner\Data;
  12495. use Symfony\Component\VarDumper\Cloner\Stub;
  12496.  
  12497. /**
  12498.  * Data collector for {@link FormInterface} instances.
  12499.  *
  12500.  * @author Robert Schönthal <[email protected]>
  12501.  * @author Bernhard Schussek <[email protected]>
  12502.  *
  12503.  * @final
  12504.  */
  12505. class 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 <[email protected]>
  12781.  *
  12782.  * For the full copyright and license information, please view the LICENSE
  12783.  * file that was distributed with this source code.
  12784.  */
  12785.  
  12786. namespace Symfony\Component\Form\Extension\DataCollector;
  12787.  
  12788. use Symfony\Component\Form\FormInterface;
  12789. use Symfony\Component\Form\FormView;
  12790.  
  12791. /**
  12792.  * Extracts arrays of information out of forms.
  12793.  *
  12794.  * @author Bernhard Schussek <[email protected]>
  12795.  */
  12796. interface 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 <[email protected]>
  12828.  *
  12829.  * For the full copyright and license information, please view the LICENSE
  12830.  * file that was distributed with this source code.
  12831.  */
  12832.  
  12833. namespace Symfony\Component\Form\Extension\HttpFoundation;
  12834.  
  12835. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  12836. use Symfony\Component\Form\FormError;
  12837. use Symfony\Component\Form\FormInterface;
  12838. use Symfony\Component\Form\RequestHandlerInterface;
  12839. use Symfony\Component\Form\Util\FormUtil;
  12840. use Symfony\Component\Form\Util\ServerParams;
  12841. use Symfony\Component\HttpFoundation\File\File;
  12842. use Symfony\Component\HttpFoundation\File\UploadedFile;
  12843. use 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 <[email protected]>
  12850.  */
  12851. class 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 <[email protected]>
  12955.  *
  12956.  * For the full copyright and license information, please view the LICENSE
  12957.  * file that was distributed with this source code.
  12958.  */
  12959.  
  12960. namespace Symfony\Component\Form\Extension\HttpFoundation\Type;
  12961.  
  12962. use Symfony\Component\Form\AbstractTypeExtension;
  12963. use Symfony\Component\Form\Extension\Core\Type\FormType;
  12964. use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
  12965. use Symfony\Component\Form\FormBuilderInterface;
  12966. use Symfony\Component\Form\RequestHandlerInterface;
  12967.  
  12968. /**
  12969.  * @author Bernhard Schussek <[email protected]>
  12970.  */
  12971. class 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 <[email protected]>
  13000.  *
  13001.  * For the full copyright and license information, please view the LICENSE
  13002.  * file that was distributed with this source code.
  13003.  */
  13004.  
  13005. namespace Symfony\Component\Form\Extension\HttpFoundation;
  13006.  
  13007. use Symfony\Component\Form\AbstractExtension;
  13008.  
  13009. /**
  13010.  * Integrates the HttpFoundation component with the Form library.
  13011.  *
  13012.  * @author Bernhard Schussek <[email protected]>
  13013.  */
  13014. class 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 <[email protected]>
  13033.  *
  13034.  * For the full copyright and license information, please view the LICENSE
  13035.  * file that was distributed with this source code.
  13036.  */
  13037.  
  13038. namespace Symfony\Component\Form\Extension\HtmlSanitizer\Type;
  13039.  
  13040. use Psr\Container\ContainerInterface;
  13041. use Symfony\Component\Form\AbstractTypeExtension;
  13042. use Symfony\Component\Form\Extension\Core\Type\TextType;
  13043. use Symfony\Component\Form\FormBuilderInterface;
  13044. use Symfony\Component\Form\FormEvent;
  13045. use Symfony\Component\Form\FormEvents;
  13046. use Symfony\Component\OptionsResolver\OptionsResolver;
  13047.  
  13048. /**
  13049.  * @author Titouan Galopin <[email protected]>
  13050.  */
  13051. class 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 <[email protected]>
  13103.  *
  13104.  * For the full copyright and license information, please view the LICENSE
  13105.  * file that was distributed with this source code.
  13106.  */
  13107.  
  13108. namespace Symfony\Component\Form\Extension\HtmlSanitizer;
  13109.  
  13110. use Psr\Container\ContainerInterface;
  13111. use Symfony\Component\Form\AbstractExtension;
  13112.  
  13113. /**
  13114.  * Integrates the HtmlSanitizer component with the Form library.
  13115.  *
  13116.  * @author Nicolas Grekas <[email protected]>
  13117.  */
  13118. class 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 <[email protected]>
  13143.  *
  13144.  * For the full copyright and license information, please view the LICENSE
  13145.  * file that was distributed with this source code.
  13146.  */
  13147.  
  13148. namespace Symfony\Component\Form\Extension\Csrf\Type;
  13149.  
  13150. use Symfony\Component\Form\AbstractTypeExtension;
  13151. use Symfony\Component\Form\Extension\Core\Type\FormType;
  13152. use Symfony\Component\Form\Extension\Core\Type\HiddenType;
  13153. use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
  13154. use Symfony\Component\Form\FormBuilderInterface;
  13155. use Symfony\Component\Form\FormInterface;
  13156. use Symfony\Component\Form\FormView;
  13157. use Symfony\Component\Form\Util\ServerParams;
  13158. use Symfony\Component\OptionsResolver\Options;
  13159. use Symfony\Component\OptionsResolver\OptionsResolver;
  13160. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  13161. use Symfony\Contracts\Translation\TranslatorInterface;
  13162.  
  13163. /**
  13164.  * @author Bernhard Schussek <[email protected]>
  13165.  */
  13166. class 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 <[email protected]>
  13267.  *
  13268.  * For the full copyright and license information, please view the LICENSE
  13269.  * file that was distributed with this source code.
  13270.  */
  13271.  
  13272. namespace Symfony\Component\Form\Extension\Csrf;
  13273.  
  13274. use Symfony\Component\Form\AbstractExtension;
  13275. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  13276. use Symfony\Contracts\Translation\TranslatorInterface;
  13277.  
  13278. /**
  13279.  * This extension protects forms by using a CSRF token.
  13280.  *
  13281.  * @author Bernhard Schussek <[email protected]>
  13282.  */
  13283. class 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 <[email protected]>
  13309.  *
  13310.  * For the full copyright and license information, please view the LICENSE
  13311.  * file that was distributed with this source code.
  13312.  */
  13313.  
  13314. namespace Symfony\Component\Form\Extension\Csrf\EventListener;
  13315.  
  13316. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13317. use Symfony\Component\Form\FormError;
  13318. use Symfony\Component\Form\FormEvent;
  13319. use Symfony\Component\Form\FormEvents;
  13320. use Symfony\Component\Form\Util\ServerParams;
  13321. use Symfony\Component\Security\Csrf\CsrfToken;
  13322. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  13323. use Symfony\Contracts\Translation\TranslatorInterface;
  13324.  
  13325. /**
  13326.  * @author Bernhard Schussek <[email protected]>
  13327.  */
  13328. class 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 <[email protected]>
  13389.  *
  13390.  * For the full copyright and license information, please view the LICENSE
  13391.  * file that was distributed with this source code.
  13392.  */
  13393.  
  13394. namespace Symfony\Component\Form\Extension\Core\EventListener;
  13395.  
  13396. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13397. use Symfony\Component\Form\Event\PostSetDataEvent;
  13398. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  13399. use Symfony\Component\Form\FormEvent;
  13400. use Symfony\Component\Form\FormEvents;
  13401. use 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 <[email protected]>
  13407.  */
  13408. class 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 <[email protected]>
  13610.  *
  13611.  * For the full copyright and license information, please view the LICENSE
  13612.  * file that was distributed with this source code.
  13613.  */
  13614.  
  13615. namespace Symfony\Component\Form\Extension\Core\EventListener;
  13616.  
  13617. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13618. use Symfony\Component\Form\FormEvent;
  13619. use Symfony\Component\Form\FormEvents;
  13620. use Symfony\Component\Form\Util\StringUtil;
  13621.  
  13622. /**
  13623.  * Trims string data.
  13624.  *
  13625.  * @author Bernhard Schussek <[email protected]>
  13626.  */
  13627. class 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 <[email protected]>
  13655.  *
  13656.  * For the full copyright and license information, please view the LICENSE
  13657.  * file that was distributed with this source code.
  13658.  */
  13659.  
  13660. namespace Symfony\Component\Form\Extension\Core\EventListener;
  13661.  
  13662. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13663. use Symfony\Component\Form\FormEvent;
  13664. use 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 <[email protected]>
  13670.  */
  13671. class 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 <[email protected]>
  13705.  *
  13706.  * For the full copyright and license information, please view the LICENSE
  13707.  * file that was distributed with this source code.
  13708.  */
  13709.  
  13710. namespace Symfony\Component\Form\Extension\Core\EventListener;
  13711.  
  13712. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13713. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  13714. use Symfony\Component\Form\FormEvent;
  13715. use Symfony\Component\Form\FormEvents;
  13716.  
  13717. /**
  13718.  * @author Bernhard Schussek <[email protected]>
  13719.  */
  13720. class 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 <[email protected]>
  13815.  *
  13816.  * For the full copyright and license information, please view the LICENSE
  13817.  * file that was distributed with this source code.
  13818.  */
  13819.  
  13820. namespace Symfony\Component\Form\Extension\Core\EventListener;
  13821.  
  13822. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13823. use Symfony\Component\Form\FormError;
  13824. use Symfony\Component\Form\FormEvent;
  13825. use Symfony\Component\Form\FormEvents;
  13826. use Symfony\Contracts\Translation\TranslatorInterface;
  13827.  
  13828. /**
  13829.  * @author Christian Flothmann <[email protected]>
  13830.  */
  13831. class 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 <[email protected]>
  13882.  *
  13883.  * For the full copyright and license information, please view the LICENSE
  13884.  * file that was distributed with this source code.
  13885.  */
  13886.  
  13887. namespace Symfony\Component\Form\Extension\Core;
  13888.  
  13889. use Symfony\Component\Form\AbstractExtension;
  13890. use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
  13891. use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
  13892. use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
  13893. use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
  13894. use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension;
  13895. use Symfony\Component\PropertyAccess\PropertyAccess;
  13896. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  13897. use Symfony\Contracts\Translation\TranslatorInterface;
  13898.  
  13899. /**
  13900.  * Represents the main form extension, which loads the core functionality.
  13901.  *
  13902.  * @author Bernhard Schussek <[email protected]>
  13903.  */
  13904. class 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 <[email protected]>
  13976.  *
  13977.  * For the full copyright and license information, please view the LICENSE
  13978.  * file that was distributed with this source code.
  13979.  */
  13980.  
  13981. namespace Symfony\Component\Form\Extension\Core\Type;
  13982.  
  13983. use Symfony\Component\Form\AbstractType;
  13984. use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
  13985. use Symfony\Component\Form\FormBuilderInterface;
  13986. use Symfony\Component\Form\FormInterface;
  13987. use Symfony\Component\Form\FormView;
  13988. use Symfony\Component\OptionsResolver\OptionsResolver;
  13989.  
  13990. class 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 <[email protected]>
  14040.  *
  14041.  * For the full copyright and license information, please view the LICENSE
  14042.  * file that was distributed with this source code.
  14043.  */
  14044.  
  14045. namespace Symfony\Component\Form\Extension\Core\Type;
  14046.  
  14047. use Symfony\Component\Form\AbstractType;
  14048. use Symfony\Component\Form\Exception\LogicException;
  14049. use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
  14050. use Symfony\Component\Form\Extension\Core\DataTransformer\StringToFloatTransformer;
  14051. use Symfony\Component\Form\FormBuilderInterface;
  14052. use Symfony\Component\Form\FormInterface;
  14053. use Symfony\Component\Form\FormView;
  14054. use Symfony\Component\OptionsResolver\Options;
  14055. use Symfony\Component\OptionsResolver\OptionsResolver;
  14056.  
  14057. class 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 <[email protected]>
  14136.  *
  14137.  * For the full copyright and license information, please view the LICENSE
  14138.  * file that was distributed with this source code.
  14139.  */
  14140.  
  14141. namespace Symfony\Component\Form\Extension\Core\Type;
  14142.  
  14143. use Symfony\Component\Form\AbstractTypeExtension;
  14144. use Symfony\Component\Form\Extension\Core\EventListener\TransformationFailureListener;
  14145. use Symfony\Component\Form\FormBuilderInterface;
  14146. use Symfony\Contracts\Translation\TranslatorInterface;
  14147.  
  14148. /**
  14149.  * @author Christian Flothmann <[email protected]>
  14150.  */
  14151. class 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 <[email protected]>
  14180.  *
  14181.  * For the full copyright and license information, please view the LICENSE
  14182.  * file that was distributed with this source code.
  14183.  */
  14184.  
  14185. namespace Symfony\Component\Form\Extension\Core\Type;
  14186.  
  14187. use Symfony\Component\Form\AbstractType;
  14188. use Symfony\Component\Form\Exception\InvalidConfigurationException;
  14189. use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToArrayTransformer;
  14190. use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToStringTransformer;
  14191. use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
  14192. use Symfony\Component\Form\FormBuilderInterface;
  14193. use Symfony\Component\Form\FormInterface;
  14194. use Symfony\Component\Form\FormView;
  14195. use Symfony\Component\Form\ReversedTransformer;
  14196. use Symfony\Component\OptionsResolver\Options;
  14197. use Symfony\Component\OptionsResolver\OptionsResolver;
  14198.  
  14199. /**
  14200.  * @author Steffen Roßkamp <[email protected]>
  14201.  */
  14202. class 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 <[email protected]>
  14449.  *
  14450.  * For the full copyright and license information, please view the LICENSE
  14451.  * file that was distributed with this source code.
  14452.  */
  14453.  
  14454. namespace Symfony\Component\Form\Extension\Core\Type;
  14455.  
  14456. use Symfony\Component\Form\AbstractType;
  14457. use Symfony\Component\OptionsResolver\Options;
  14458. use Symfony\Component\OptionsResolver\OptionsResolver;
  14459. use Symfony\Contracts\Translation\TranslatableInterface;
  14460.  
  14461. /**
  14462.  * A choice type for native PHP enums.
  14463.  *
  14464.  * @author Alexander M. Turek <[email protected]>
  14465.  */
  14466. final 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 <[email protected]>
  14507.  *
  14508.  * For the full copyright and license information, please view the LICENSE
  14509.  * file that was distributed with this source code.
  14510.  */
  14511.  
  14512. namespace Symfony\Component\Form\Extension\Core\Type;
  14513.  
  14514. use Symfony\Component\Form\AbstractRendererEngine;
  14515. use Symfony\Component\Form\AbstractType;
  14516. use Symfony\Component\Form\Exception\LogicException;
  14517. use Symfony\Component\Form\FormBuilderInterface;
  14518. use Symfony\Component\Form\FormInterface;
  14519. use Symfony\Component\Form\FormView;
  14520. use 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 <[email protected]>
  14529.  */
  14530. abstract 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 <[email protected]>
  14661.  *
  14662.  * For the full copyright and license information, please view the LICENSE
  14663.  * file that was distributed with this source code.
  14664.  */
  14665.  
  14666. namespace Symfony\Component\Form\Extension\Core\Type;
  14667.  
  14668. use Symfony\Component\Form\AbstractType;
  14669. use Symfony\Component\Form\Extension\Core\DataTransformer\UlidToStringTransformer;
  14670. use Symfony\Component\Form\FormBuilderInterface;
  14671. use Symfony\Component\OptionsResolver\OptionsResolver;
  14672.  
  14673. /**
  14674.  * @author Pavel Dyakonov <[email protected]>
  14675.  */
  14676. class 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 <[email protected]>
  14703.  *
  14704.  * For the full copyright and license information, please view the LICENSE
  14705.  * file that was distributed with this source code.
  14706.  */
  14707.  
  14708. namespace Symfony\Component\Form\Extension\Core\Type;
  14709.  
  14710. use Symfony\Component\Form\AbstractType;
  14711. use Symfony\Component\Form\Extension\Core\EventListener\FixUrlProtocolListener;
  14712. use Symfony\Component\Form\FormBuilderInterface;
  14713. use Symfony\Component\Form\FormInterface;
  14714. use Symfony\Component\Form\FormView;
  14715. use Symfony\Component\OptionsResolver\Options;
  14716. use Symfony\Component\OptionsResolver\OptionsResolver;
  14717.  
  14718. class 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 <[email protected]>
  14769.  *
  14770.  * For the full copyright and license information, please view the LICENSE
  14771.  * file that was distributed with this source code.
  14772.  */
  14773.  
  14774. namespace Symfony\Component\Form\Extension\Core\Type;
  14775.  
  14776. use Symfony\Component\Form\AbstractType;
  14777. use Symfony\Component\Form\Extension\Core\DataTransformer\ValueToDuplicatesTransformer;
  14778. use Symfony\Component\Form\FormBuilderInterface;
  14779. use Symfony\Component\OptionsResolver\OptionsResolver;
  14780.  
  14781. class 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 <[email protected]>
  14839.  *
  14840.  * For the full copyright and license information, please view the LICENSE
  14841.  * file that was distributed with this source code.
  14842.  */
  14843.  
  14844. namespace Symfony\Component\Form\Extension\Core\Type;
  14845.  
  14846. use Symfony\Component\Form\AbstractType;
  14847. use Symfony\Component\Form\ButtonTypeInterface;
  14848.  
  14849. /**
  14850.  * A reset button.
  14851.  *
  14852.  * @author Bernhard Schussek <[email protected]>
  14853.  */
  14854. class 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 <[email protected]>
  14876.  *
  14877.  * For the full copyright and license information, please view the LICENSE
  14878.  * file that was distributed with this source code.
  14879.  */
  14880.  
  14881. namespace Symfony\Component\Form\Extension\Core\Type;
  14882.  
  14883. use Symfony\Component\Form\AbstractType;
  14884. use Symfony\Component\Form\FormInterface;
  14885. use Symfony\Component\Form\FormView;
  14886. use Symfony\Component\Form\SubmitButtonTypeInterface;
  14887. use Symfony\Component\OptionsResolver\OptionsResolver;
  14888.  
  14889. /**
  14890.  * A submit button.
  14891.  *
  14892.  * @author Bernhard Schussek <[email protected]>
  14893.  */
  14894. class 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 <[email protected]>
  14931.  *
  14932.  * For the full copyright and license information, please view the LICENSE
  14933.  * file that was distributed with this source code.
  14934.  */
  14935.  
  14936. namespace Symfony\Component\Form\Extension\Core\Type;
  14937.  
  14938. use Symfony\Component\Form\AbstractType;
  14939. use Symfony\Component\Form\Exception\InvalidConfigurationException;
  14940. use Symfony\Component\Form\Exception\LogicException;
  14941. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
  14942. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
  14943. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
  14944. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
  14945. use Symfony\Component\Form\FormBuilderInterface;
  14946. use Symfony\Component\Form\FormEvent;
  14947. use Symfony\Component\Form\FormEvents;
  14948. use Symfony\Component\Form\FormInterface;
  14949. use Symfony\Component\Form\FormView;
  14950. use Symfony\Component\Form\ReversedTransformer;
  14951. use Symfony\Component\OptionsResolver\Options;
  14952. use Symfony\Component\OptionsResolver\OptionsResolver;
  14953.  
  14954. class 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 <[email protected]>
  15315.  *
  15316.  * For the full copyright and license information, please view the LICENSE
  15317.  * file that was distributed with this source code.
  15318.  */
  15319.  
  15320. namespace Symfony\Component\Form\Extension\Core\Type;
  15321.  
  15322. use Symfony\Component\Form\AbstractType;
  15323. use Symfony\Component\OptionsResolver\OptionsResolver;
  15324.  
  15325. class 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 <[email protected]>
  15354.  *
  15355.  * For the full copyright and license information, please view the LICENSE
  15356.  * file that was distributed with this source code.
  15357.  */
  15358.  
  15359. namespace Symfony\Component\Form\Extension\Core\Type;
  15360.  
  15361. use Symfony\Component\Form\AbstractType;
  15362. use Symfony\Component\Form\Exception\LogicException;
  15363. use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
  15364. use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
  15365. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
  15366. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
  15367. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToHtml5LocalDateTimeTransformer;
  15368. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
  15369. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
  15370. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
  15371. use Symfony\Component\Form\FormBuilderInterface;
  15372. use Symfony\Component\Form\FormEvent;
  15373. use Symfony\Component\Form\FormEvents;
  15374. use Symfony\Component\Form\FormInterface;
  15375. use Symfony\Component\Form\FormView;
  15376. use Symfony\Component\Form\ReversedTransformer;
  15377. use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
  15378. use Symfony\Component\OptionsResolver\Options;
  15379. use Symfony\Component\OptionsResolver\OptionsResolver;
  15380.  
  15381. class 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 <[email protected]>
  15704.  *
  15705.  * For the full copyright and license information, please view the LICENSE
  15706.  * file that was distributed with this source code.
  15707.  */
  15708.  
  15709. namespace Symfony\Component\Form\Extension\Core\Type;
  15710.  
  15711. use Symfony\Component\Form\AbstractType;
  15712. use Symfony\Component\OptionsResolver\OptionsResolver;
  15713.  
  15714. class 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 <[email protected]>
  15743.  *
  15744.  * For the full copyright and license information, please view the LICENSE
  15745.  * file that was distributed with this source code.
  15746.  */
  15747.  
  15748. namespace Symfony\Component\Form\Extension\Core\Type;
  15749.  
  15750. use Symfony\Component\Form\AbstractType;
  15751. use Symfony\Component\OptionsResolver\OptionsResolver;
  15752.  
  15753. class 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 <[email protected]>
  15782.  *
  15783.  * For the full copyright and license information, please view the LICENSE
  15784.  * file that was distributed with this source code.
  15785.  */
  15786.  
  15787. namespace Symfony\Component\Form\Extension\Core\Type;
  15788.  
  15789. use Symfony\Component\Form\AbstractType;
  15790. use Symfony\Component\OptionsResolver\OptionsResolver;
  15791.  
  15792. class 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 <[email protected]>
  15821.  *
  15822.  * For the full copyright and license information, please view the LICENSE
  15823.  * file that was distributed with this source code.
  15824.  */
  15825.  
  15826. namespace Symfony\Component\Form\Extension\Core\Type;
  15827.  
  15828. use Symfony\Component\Form\AbstractType;
  15829. use Symfony\Component\Form\FormBuilderInterface;
  15830. use Symfony\Component\Form\FormError;
  15831. use Symfony\Component\Form\FormEvent;
  15832. use Symfony\Component\Form\FormEvents;
  15833. use Symfony\Component\OptionsResolver\OptionsResolver;
  15834. use Symfony\Contracts\Translation\TranslatorInterface;
  15835.  
  15836. class 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 <[email protected]>
  15905.  *
  15906.  * For the full copyright and license information, please view the LICENSE
  15907.  * file that was distributed with this source code.
  15908.  */
  15909.  
  15910. namespace Symfony\Component\Form\Extension\Core\Type;
  15911.  
  15912. use Symfony\Component\Form\AbstractType;
  15913. use Symfony\Component\Form\ChoiceList\ChoiceList;
  15914. use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
  15915. use Symfony\Component\Form\Exception\LogicException;
  15916. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeZoneToStringTransformer;
  15917. use Symfony\Component\Form\Extension\Core\DataTransformer\IntlTimeZoneToStringTransformer;
  15918. use Symfony\Component\Form\FormBuilderInterface;
  15919. use Symfony\Component\Intl\Intl;
  15920. use Symfony\Component\Intl\Timezones;
  15921. use Symfony\Component\OptionsResolver\Options;
  15922. use Symfony\Component\OptionsResolver\OptionsResolver;
  15923.  
  15924. class 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 <[email protected]>
  16032.  *
  16033.  * For the full copyright and license information, please view the LICENSE
  16034.  * file that was distributed with this source code.
  16035.  */
  16036.  
  16037. namespace Symfony\Component\Form\Extension\Core\Type;
  16038.  
  16039. use Symfony\Component\Form\AbstractType;
  16040. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  16041. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
  16042. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
  16043. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFilter;
  16044. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
  16045. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
  16046. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceTranslationParameters;
  16047. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
  16048. use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy;
  16049. use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice;
  16050. use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
  16051. use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
  16052. use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
  16053. use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
  16054. use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
  16055. use Symfony\Component\Form\ChoiceList\Loader\LazyChoiceLoader;
  16056. use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
  16057. use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
  16058. use Symfony\Component\Form\ChoiceList\View\ChoiceView;
  16059. use Symfony\Component\Form\Event\PreSubmitEvent;
  16060. use Symfony\Component\Form\Exception\LogicException;
  16061. use Symfony\Component\Form\Exception\TransformationFailedException;
  16062. use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper;
  16063. use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper;
  16064. use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
  16065. use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
  16066. use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
  16067. use Symfony\Component\Form\FormBuilderInterface;
  16068. use Symfony\Component\Form\FormError;
  16069. use Symfony\Component\Form\FormEvent;
  16070. use Symfony\Component\Form\FormEvents;
  16071. use Symfony\Component\Form\FormInterface;
  16072. use Symfony\Component\Form\FormView;
  16073. use Symfony\Component\OptionsResolver\Options;
  16074. use Symfony\Component\OptionsResolver\OptionsResolver;
  16075. use Symfony\Component\PropertyAccess\PropertyPath;
  16076. use Symfony\Contracts\Translation\TranslatorInterface;
  16077.  
  16078. class 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 <[email protected]>
  16524.  *
  16525.  * For the full copyright and license information, please view the LICENSE
  16526.  * file that was distributed with this source code.
  16527.  */
  16528.  
  16529. namespace Symfony\Component\Form\Extension\Core\Type;
  16530.  
  16531. use Symfony\Component\Form\AbstractType;
  16532. use Symfony\Component\Form\ChoiceList\ChoiceList;
  16533. use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
  16534. use Symfony\Component\Form\Exception\LogicException;
  16535. use Symfony\Component\Intl\Exception\MissingResourceException;
  16536. use Symfony\Component\Intl\Intl;
  16537. use Symfony\Component\Intl\Languages;
  16538. use Symfony\Component\OptionsResolver\Options;
  16539. use Symfony\Component\OptionsResolver\OptionsResolver;
  16540.  
  16541. class 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 <[email protected]>
  16611.  *
  16612.  * For the full copyright and license information, please view the LICENSE
  16613.  * file that was distributed with this source code.
  16614.  */
  16615.  
  16616. namespace Symfony\Component\Form\Extension\Core\Type;
  16617.  
  16618. use Symfony\Component\Form\AbstractType;
  16619. use Symfony\Component\Form\ChoiceList\ChoiceList;
  16620. use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
  16621. use Symfony\Component\Form\Exception\LogicException;
  16622. use Symfony\Component\Intl\Currencies;
  16623. use Symfony\Component\Intl\Intl;
  16624. use Symfony\Component\OptionsResolver\Options;
  16625. use Symfony\Component\OptionsResolver\OptionsResolver;
  16626.  
  16627. class 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 <[email protected]>
  16669.  *
  16670.  * For the full copyright and license information, please view the LICENSE
  16671.  * file that was distributed with this source code.
  16672.  */
  16673.  
  16674. namespace Symfony\Component\Form\Extension\Core\Type;
  16675.  
  16676. use Symfony\Component\Form\AbstractType;
  16677. use Symfony\Component\Form\Exception\LogicException;
  16678. use Symfony\Component\Form\Extension\Core\DataTransformer\WeekToArrayTransformer;
  16679. use Symfony\Component\Form\FormBuilderInterface;
  16680. use Symfony\Component\Form\FormInterface;
  16681. use Symfony\Component\Form\FormView;
  16682. use Symfony\Component\Form\ReversedTransformer;
  16683. use Symfony\Component\OptionsResolver\Options;
  16684. use Symfony\Component\OptionsResolver\OptionsResolver;
  16685.  
  16686. class 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 <[email protected]>
  16844.  *
  16845.  * For the full copyright and license information, please view the LICENSE
  16846.  * file that was distributed with this source code.
  16847.  */
  16848.  
  16849. namespace Symfony\Component\Form\Extension\Core\Type;
  16850.  
  16851. use Symfony\Component\Form\AbstractType;
  16852. use Symfony\Component\Form\Exception\LogicException;
  16853. use Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer;
  16854. use Symfony\Component\Form\FormBuilderInterface;
  16855. use Symfony\Component\Form\FormInterface;
  16856. use Symfony\Component\Form\FormView;
  16857. use Symfony\Component\OptionsResolver\Options;
  16858. use Symfony\Component\OptionsResolver\OptionsResolver;
  16859.  
  16860. class 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 <[email protected]>
  16985.  *
  16986.  * For the full copyright and license information, please view the LICENSE
  16987.  * file that was distributed with this source code.
  16988.  */
  16989.  
  16990. namespace Symfony\Component\Form\Extension\Core\Type;
  16991.  
  16992. use Symfony\Component\Form\AbstractType;
  16993. use Symfony\Component\OptionsResolver\OptionsResolver;
  16994.  
  16995. class 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 <[email protected]>
  17024.  *
  17025.  * For the full copyright and license information, please view the LICENSE
  17026.  * file that was distributed with this source code.
  17027.  */
  17028.  
  17029. namespace Symfony\Component\Form\Extension\Core\Type;
  17030.  
  17031. use Symfony\Component\Form\AbstractType;
  17032. use Symfony\Component\Form\DataTransformerInterface;
  17033. use Symfony\Component\Form\FormBuilderInterface;
  17034. use Symfony\Component\OptionsResolver\OptionsResolver;
  17035.  
  17036. class 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 <[email protected]>
  17083.  *
  17084.  * For the full copyright and license information, please view the LICENSE
  17085.  * file that was distributed with this source code.
  17086.  */
  17087.  
  17088. namespace Symfony\Component\Form\Extension\Core\Type;
  17089.  
  17090. use Symfony\Component\Form\Exception\LogicException;
  17091. use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor;
  17092. use Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor;
  17093. use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
  17094. use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
  17095. use Symfony\Component\Form\Extension\Core\EventListener\TrimListener;
  17096. use Symfony\Component\Form\FormBuilderInterface;
  17097. use Symfony\Component\Form\FormInterface;
  17098. use Symfony\Component\Form\FormView;
  17099. use Symfony\Component\OptionsResolver\Options;
  17100. use Symfony\Component\OptionsResolver\OptionsResolver;
  17101. use Symfony\Component\PropertyAccess\PropertyAccess;
  17102. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  17103. use Symfony\Contracts\Translation\TranslatableInterface;
  17104.  
  17105. class 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 <[email protected]>
  17293.  *
  17294.  * For the full copyright and license information, please view the LICENSE
  17295.  * file that was distributed with this source code.
  17296.  */
  17297.  
  17298. namespace Symfony\Component\Form\Extension\Core\Type;
  17299.  
  17300. use Symfony\Component\Form\AbstractType;
  17301. use Symfony\Component\OptionsResolver\OptionsResolver;
  17302.  
  17303. class 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 <[email protected]>
  17332.  *
  17333.  * For the full copyright and license information, please view the LICENSE
  17334.  * file that was distributed with this source code.
  17335.  */
  17336.  
  17337. namespace Symfony\Component\Form\Extension\Core\Type;
  17338.  
  17339. use Symfony\Component\Form\AbstractType;
  17340. use Symfony\Component\Form\FormInterface;
  17341. use Symfony\Component\Form\FormView;
  17342. use Symfony\Component\OptionsResolver\OptionsResolver;
  17343.  
  17344. class 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 <[email protected]>
  17382.  *
  17383.  * For the full copyright and license information, please view the LICENSE
  17384.  * file that was distributed with this source code.
  17385.  */
  17386.  
  17387. namespace Symfony\Component\Form\Extension\Core\Type;
  17388.  
  17389. use Symfony\Component\Form\AbstractType;
  17390. use Symfony\Component\Form\Event\PreSubmitEvent;
  17391. use Symfony\Component\Form\FileUploadError;
  17392. use Symfony\Component\Form\FormBuilderInterface;
  17393. use Symfony\Component\Form\FormEvent;
  17394. use Symfony\Component\Form\FormEvents;
  17395. use Symfony\Component\Form\FormInterface;
  17396. use Symfony\Component\Form\FormView;
  17397. use Symfony\Component\HttpFoundation\File\File;
  17398. use Symfony\Component\OptionsResolver\Options;
  17399. use Symfony\Component\OptionsResolver\OptionsResolver;
  17400. use Symfony\Contracts\Translation\TranslatorInterface;
  17401.  
  17402. class 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 <[email protected]>
  17615.  *
  17616.  * For the full copyright and license information, please view the LICENSE
  17617.  * file that was distributed with this source code.
  17618.  */
  17619.  
  17620. namespace Symfony\Component\Form\Extension\Core\Type;
  17621.  
  17622. use Symfony\Component\Form\AbstractType;
  17623. use Symfony\Component\Form\ChoiceList\ChoiceList;
  17624. use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
  17625. use Symfony\Component\Form\Exception\LogicException;
  17626. use Symfony\Component\Intl\Countries;
  17627. use Symfony\Component\Intl\Intl;
  17628. use Symfony\Component\OptionsResolver\Options;
  17629. use Symfony\Component\OptionsResolver\OptionsResolver;
  17630.  
  17631. class 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 <[email protected]>
  17676.  *
  17677.  * For the full copyright and license information, please view the LICENSE
  17678.  * file that was distributed with this source code.
  17679.  */
  17680.  
  17681. namespace Symfony\Component\Form\Extension\Core\Type;
  17682.  
  17683. use Symfony\Component\Form\ButtonTypeInterface;
  17684. use Symfony\Component\OptionsResolver\OptionsResolver;
  17685.  
  17686. /**
  17687.  * A form button.
  17688.  *
  17689.  * @author Bernhard Schussek <[email protected]>
  17690.  */
  17691. class 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 <[email protected]>
  17720.  *
  17721.  * For the full copyright and license information, please view the LICENSE
  17722.  * file that was distributed with this source code.
  17723.  */
  17724.  
  17725. namespace Symfony\Component\Form\Extension\Core\Type;
  17726.  
  17727. use Symfony\Component\Form\AbstractType;
  17728. use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer;
  17729. use Symfony\Component\Form\FormBuilderInterface;
  17730. use Symfony\Component\Form\FormInterface;
  17731. use Symfony\Component\Form\FormView;
  17732. use Symfony\Component\OptionsResolver\OptionsResolver;
  17733.  
  17734. class 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 <[email protected]>
  17800.  *
  17801.  * For the full copyright and license information, please view the LICENSE
  17802.  * file that was distributed with this source code.
  17803.  */
  17804.  
  17805. namespace Symfony\Component\Form\Extension\Core\Type;
  17806.  
  17807. use Symfony\Component\Form\AbstractType;
  17808. use Symfony\Component\OptionsResolver\OptionsResolver;
  17809.  
  17810. class 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 <[email protected]>
  17842.  *
  17843.  * For the full copyright and license information, please view the LICENSE
  17844.  * file that was distributed with this source code.
  17845.  */
  17846.  
  17847. namespace Symfony\Component\Form\Extension\Core\Type;
  17848.  
  17849. use Symfony\Component\Form\AbstractType;
  17850. use Symfony\Component\Form\Exception\LogicException;
  17851. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
  17852. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
  17853. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
  17854. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
  17855. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
  17856. use Symfony\Component\Form\FormBuilderInterface;
  17857. use Symfony\Component\Form\FormEvent;
  17858. use Symfony\Component\Form\FormEvents;
  17859. use Symfony\Component\Form\FormInterface;
  17860. use Symfony\Component\Form\FormView;
  17861. use Symfony\Component\Form\ReversedTransformer;
  17862. use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
  17863. use Symfony\Component\OptionsResolver\Options;
  17864. use Symfony\Component\OptionsResolver\OptionsResolver;
  17865.  
  17866. class 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 <[email protected]>
  18237.  *
  18238.  * For the full copyright and license information, please view the LICENSE
  18239.  * file that was distributed with this source code.
  18240.  */
  18241.  
  18242. namespace Symfony\Component\Form\Extension\Core\Type;
  18243.  
  18244. use Symfony\Component\Form\AbstractType;
  18245. use Symfony\Component\Form\ChoiceList\ChoiceList;
  18246. use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
  18247. use Symfony\Component\Form\Exception\LogicException;
  18248. use Symfony\Component\Intl\Intl;
  18249. use Symfony\Component\Intl\Locales;
  18250. use Symfony\Component\OptionsResolver\Options;
  18251. use Symfony\Component\OptionsResolver\OptionsResolver;
  18252.  
  18253. class 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 <[email protected]>
  18295.  *
  18296.  * For the full copyright and license information, please view the LICENSE
  18297.  * file that was distributed with this source code.
  18298.  */
  18299.  
  18300. namespace Symfony\Component\Form\Extension\Core\Type;
  18301.  
  18302. use Symfony\Component\Form\AbstractType;
  18303. use Symfony\Component\Form\Extension\Core\DataTransformer\BooleanToStringTransformer;
  18304. use Symfony\Component\Form\FormBuilderInterface;
  18305. use Symfony\Component\Form\FormInterface;
  18306. use Symfony\Component\Form\FormView;
  18307. use Symfony\Component\OptionsResolver\OptionsResolver;
  18308.  
  18309. class 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 <[email protected]>
  18362.  *
  18363.  * For the full copyright and license information, please view the LICENSE
  18364.  * file that was distributed with this source code.
  18365.  */
  18366.  
  18367. namespace Symfony\Component\Form\Extension\Core\Type;
  18368.  
  18369. use Symfony\Component\Form\AbstractType;
  18370. use Symfony\Component\Form\FormInterface;
  18371. use Symfony\Component\Form\FormView;
  18372.  
  18373. class 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 <[email protected]>
  18401.  *
  18402.  * For the full copyright and license information, please view the LICENSE
  18403.  * file that was distributed with this source code.
  18404.  */
  18405.  
  18406. namespace Symfony\Component\Form\Extension\Core\Type;
  18407.  
  18408. use Symfony\Component\Form\AbstractType;
  18409. use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
  18410. use Symfony\Component\Form\FormBuilderInterface;
  18411. use Symfony\Component\Form\FormInterface;
  18412. use Symfony\Component\Form\FormView;
  18413. use Symfony\Component\OptionsResolver\Options;
  18414. use Symfony\Component\OptionsResolver\OptionsResolver;
  18415.  
  18416. class 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 <[email protected]>
  18537.  *
  18538.  * For the full copyright and license information, please view the LICENSE
  18539.  * file that was distributed with this source code.
  18540.  */
  18541.  
  18542. namespace Symfony\Component\Form\Extension\Core\Type;
  18543.  
  18544. use Symfony\Component\Form\AbstractType;
  18545. use Symfony\Component\Form\Extension\Core\DataTransformer\UuidToStringTransformer;
  18546. use Symfony\Component\Form\FormBuilderInterface;
  18547. use Symfony\Component\OptionsResolver\OptionsResolver;
  18548.  
  18549. /**
  18550.  * @author Pavel Dyakonov <[email protected]>
  18551.  */
  18552. class 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 <[email protected]>
  18579.  *
  18580.  * For the full copyright and license information, please view the LICENSE
  18581.  * file that was distributed with this source code.
  18582.  */
  18583.  
  18584. namespace Symfony\Component\Form\Extension\Core\DataMapper;
  18585.  
  18586. use Symfony\Component\Form\DataMapperInterface;
  18587. use 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 <[email protected]>
  18597.  */
  18598. class 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 <[email protected]>
  18643.  *
  18644.  * For the full copyright and license information, please view the LICENSE
  18645.  * file that was distributed with this source code.
  18646.  */
  18647.  
  18648. namespace Symfony\Component\Form\Extension\Core\DataMapper;
  18649.  
  18650. use Symfony\Component\Form\DataAccessorInterface;
  18651. use Symfony\Component\Form\DataMapperInterface;
  18652. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  18653. use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor;
  18654. use Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor;
  18655. use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
  18656.  
  18657. /**
  18658.  * Maps arrays/objects to/from forms using data accessors.
  18659.  *
  18660.  * @author Bernhard Schussek <[email protected]>
  18661.  */
  18662. class 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 <[email protected]>
  18732.  *
  18733.  * For the full copyright and license information, please view the LICENSE
  18734.  * file that was distributed with this source code.
  18735.  */
  18736.  
  18737. namespace Symfony\Component\Form\Extension\Core\DataMapper;
  18738.  
  18739. use Symfony\Component\Form\DataMapperInterface;
  18740. use 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 <[email protected]>
  18750.  */
  18751. class 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 <[email protected]>
  18793.  *
  18794.  * For the full copyright and license information, please view the LICENSE
  18795.  * file that was distributed with this source code.
  18796.  */
  18797.  
  18798. namespace Symfony\Component\Form\Extension\Core\DataAccessor;
  18799.  
  18800. use Symfony\Component\Form\DataAccessorInterface;
  18801. use Symfony\Component\Form\Exception\AccessException;
  18802. use 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 <[email protected]>
  18808.  */
  18809. class 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 <[email protected]>
  18849.  *
  18850.  * For the full copyright and license information, please view the LICENSE
  18851.  * file that was distributed with this source code.
  18852.  */
  18853.  
  18854. namespace Symfony\Component\Form\Extension\Core\DataAccessor;
  18855.  
  18856. use Symfony\Component\Form\DataAccessorInterface;
  18857. use Symfony\Component\Form\DataMapperInterface;
  18858. use Symfony\Component\Form\Exception\AccessException;
  18859. use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
  18860. use Symfony\Component\Form\FormInterface;
  18861. use Symfony\Component\PropertyAccess\Exception\AccessException as PropertyAccessException;
  18862. use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
  18863. use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
  18864. use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
  18865. use Symfony\Component\PropertyAccess\PropertyAccess;
  18866. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  18867. use 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 <[email protected]>
  18873.  * @author Bernhard Schussek <[email protected]>
  18874.  */
  18875. class 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 <[email protected]>
  18975.  *
  18976.  * For the full copyright and license information, please view the LICENSE
  18977.  * file that was distributed with this source code.
  18978.  */
  18979.  
  18980. namespace Symfony\Component\Form\Extension\Core\DataAccessor;
  18981.  
  18982. use Symfony\Component\Form\DataAccessorInterface;
  18983. use Symfony\Component\Form\Exception\AccessException;
  18984. use Symfony\Component\Form\FormInterface;
  18985.  
  18986. /**
  18987.  * @author Yonel Ceruto <[email protected]>
  18988.  */
  18989. class 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 <[email protected]>
  19055.  *
  19056.  * For the full copyright and license information, please view the LICENSE
  19057.  * file that was distributed with this source code.
  19058.  */
  19059.  
  19060. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  19061.  
  19062. use Symfony\Component\Form\DataTransformerInterface;
  19063. use Symfony\Component\Form\Exception\TransformationFailedException;
  19064.  
  19065. /**
  19066.  * Transforms between an ISO 8601 week date string and an array.
  19067.  *
  19068.  * @author Damien Fayet <[email protected]>
  19069.  *
  19070.  * @implements DataTransformerInterface<string, array{year: int|null, week: int|null}>
  19071.  */
  19072. class 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 <[email protected]>
  19166.  *
  19167.  * For the full copyright and license information, please view the LICENSE
  19168.  * file that was distributed with this source code.
  19169.  */
  19170.  
  19171. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  19172.  
  19173. use Symfony\Component\Form\DataTransformerInterface;
  19174. use Symfony\Component\Form\Exception\TransformationFailedException;
  19175.  
  19176. /**
  19177.  * Passes a value through multiple value transformers.
  19178.  *
  19179.  * @author Bernhard Schussek <[email protected]>
  19180.  */
  19181. class 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 <[email protected]>
  19254.  *
  19255.  * For the full copyright and license information, please view the LICENSE
  19256.  * file that was distributed with this source code.
  19257.  */
  19258.  
  19259. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  19260.  
  19261. use Symfony\Component\Form\DataTransformerInterface;
  19262. use Symfony\Component\Form\Exception\InvalidArgumentException;
  19263.  
  19264. /**
  19265.  * @template TTransformedValue
  19266.  *
  19267.  * @implements DataTransformerInterface<\DateTimeInterface, TTransformedValue>
  19268.  */
  19269. abstract 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 <[email protected]>
  19317.  *
  19318.  * For the full copyright and license information, please view the LICENSE
  19319.  * file that was distributed with this source code.
  19320.  */
  19321.  
  19322. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  19323.  
  19324. use Symfony\Component\Form\Exception\TransformationFailedException;
  19325.  
  19326. /**
  19327.  * @author Bernhard Schussek <[email protected]>
  19328.  *
  19329.  * @extends BaseDateTimeTransformer<string>
  19330.  */
  19331. class 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 <[email protected]>
  19407.  *
  19408.  * For the full copyright and license information, please view the LICENSE
  19409.  * file that was distributed with this source code.
  19410.  */
  19411.  
  19412. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  19413.  
  19414. use Symfony\Component\Form\DataTransformerInterface;
  19415. use Symfony\Component\Form\Exception\TransformationFailedException;
  19416. use 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 <[email protected]>
  19422.  *
  19423.  * @implements DataTransformerInterface<\DateInterval, array>
  19424.  */
  19425. class 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 <[email protected]>
  19579.  *
  19580.  * For the full copyright and license information, please view the LICENSE
  19581.  * file that was distributed with this source code.
  19582.  */
  19583.  
  19584. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  19585.  
  19586. use Symfony\Component\Form\Exception\TransformationFailedException;
  19587.  
  19588. /**
  19589.  * Transforms between a timestamp and a DateTime object.
  19590.  *
  19591.  * @author Bernhard Schussek <[email protected]>
  19592.  * @author Florian Eckerstorfer <[email protected]>
  19593.  *
  19594.  * @extends BaseDateTimeTransformer<int|numeric-string>
  19595.  */
  19596. class 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 <[email protected]>
  19661.  *
  19662.  * For the full copyright and license information, please view the LICENSE
  19663.  * file that was distributed with this source code.
  19664.  */
  19665.  
  19666. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  19667.  
  19668. use Symfony\Component\Form\Exception\TransformationFailedException;
  19669.  
  19670. /**
  19671.  * Transforms between a normalized time and a localized time string/array.
  19672.  *
  19673.  * @author Bernhard Schussek <[email protected]>
  19674.  * @author Florian Eckerstorfer <[email protected]>
  19675.  *
  19676.  * @extends BaseDateTimeTransformer<array>
  19677.  */
  19678. class 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 <[email protected]>
  19847.  *
  19848.  * For the full copyright and license information, please view the LICENSE
  19849.  * file that was distributed with this source code.
  19850.  */
  19851.  
  19852. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  19853.  
  19854. use Symfony\Component\Form\DataTransformerInterface;
  19855. use Symfony\Component\Form\Exception\TransformationFailedException;
  19856.  
  19857. /**
  19858.  * @author Bernhard Schussek <[email protected]>
  19859.  *
  19860.  * @implements DataTransformerInterface<mixed, array>
  19861.  */
  19862. class 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 <[email protected]>
  19930.  *
  19931.  * For the full copyright and license information, please view the LICENSE
  19932.  * file that was distributed with this source code.
  19933.  */
  19934.  
  19935. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  19936.  
  19937. use 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 <[email protected]>
  19944.  */
  19945. class 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 <[email protected]>
  19990.  *
  19991.  * For the full copyright and license information, please view the LICENSE
  19992.  * file that was distributed with this source code.
  19993.  */
  19994.  
  19995. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  19996.  
  19997. use Symfony\Component\Form\DataTransformerInterface;
  19998. use Symfony\Component\Form\Exception\InvalidArgumentException;
  19999. use Symfony\Component\Form\Exception\TransformationFailedException;
  20000.  
  20001. /**
  20002.  * Transforms between a Boolean and a string.
  20003.  *
  20004.  * @author Bernhard Schussek <[email protected]>
  20005.  * @author Florian Eckerstorfer <[email protected]>
  20006.  *
  20007.  * @implements DataTransformerInterface<bool, string>
  20008.  */
  20009. class 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 <[email protected]>
  20073.  *
  20074.  * For the full copyright and license information, please view the LICENSE
  20075.  * file that was distributed with this source code.
  20076.  */
  20077.  
  20078. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  20079.  
  20080. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  20081. use Symfony\Component\Form\DataTransformerInterface;
  20082. use Symfony\Component\Form\Exception\TransformationFailedException;
  20083.  
  20084. /**
  20085.  * @author Bernhard Schussek <[email protected]>
  20086.  *
  20087.  * @implements DataTransformerInterface<array, array>
  20088.  */
  20089. class 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 <[email protected]>
  20146.  *
  20147.  * For the full copyright and license information, please view the LICENSE
  20148.  * file that was distributed with this source code.
  20149.  */
  20150.  
  20151. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  20152.  
  20153. use Symfony\Component\Form\Exception\TransformationFailedException;
  20154.  
  20155. /**
  20156.  * @author Franz Wilding <[email protected]>
  20157.  * @author Bernhard Schussek <[email protected]>
  20158.  * @author Fred Cox <[email protected]>
  20159.  *
  20160.  * @extends BaseDateTimeTransformer<string>
  20161.  */
  20162. class 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 <[email protected]>
  20257.  *
  20258.  * For the full copyright and license information, please view the LICENSE
  20259.  * file that was distributed with this source code.
  20260.  */
  20261.  
  20262. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  20263.  
  20264. use Symfony\Component\Form\DataTransformerInterface;
  20265. use Symfony\Component\Form\Exception\TransformationFailedException;
  20266. use Symfony\Component\Uid\Ulid;
  20267.  
  20268. /**
  20269.  * Transforms between a ULID string and a Ulid object.
  20270.  *
  20271.  * @author Pavel Dyakonov <[email protected]>
  20272.  *
  20273.  * @implements DataTransformerInterface<Ulid, string>
  20274.  */
  20275. class 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 <[email protected]>
  20334.  *
  20335.  * For the full copyright and license information, please view the LICENSE
  20336.  * file that was distributed with this source code.
  20337.  */
  20338.  
  20339. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  20340.  
  20341. use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
  20342. use Symfony\Component\Form\DataTransformerInterface;
  20343. use Symfony\Component\Form\Exception\TransformationFailedException;
  20344.  
  20345. /**
  20346.  * @author Bernhard Schussek <[email protected]>
  20347.  *
  20348.  * @implements DataTransformerInterface<mixed, string>
  20349.  */
  20350. class 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 <[email protected]>
  20391.  *
  20392.  * For the full copyright and license information, please view the LICENSE
  20393.  * file that was distributed with this source code.
  20394.  */
  20395.  
  20396. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  20397.  
  20398. use Symfony\Component\Form\Exception\TransformationFailedException;
  20399.  
  20400. /**
  20401.  * Transforms between a date string and a DateTime object.
  20402.  *
  20403.  * @author Bernhard Schussek <[email protected]>
  20404.  * @author Florian Eckerstorfer <[email protected]>
  20405.  *
  20406.  * @extends BaseDateTimeTransformer<string>
  20407.  */
  20408. class 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 <[email protected]>
  20530.  *
  20531.  * For the full copyright and license information, please view the LICENSE
  20532.  * file that was distributed with this source code.
  20533.  */
  20534.  
  20535. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  20536.  
  20537. use Symfony\Component\Form\DataTransformerInterface;
  20538. use Symfony\Component\Form\Exception\TransformationFailedException;
  20539.  
  20540. /**
  20541.  * @implements DataTransformerInterface<numeric-string, float>
  20542.  */
  20543. class 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 <[email protected]>
  20590.  *
  20591.  * For the full copyright and license information, please view the LICENSE
  20592.  * file that was distributed with this source code.
  20593.  */
  20594.  
  20595. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  20596.  
  20597. use Symfony\Component\Form\DataTransformerInterface;
  20598. use Symfony\Component\Form\Exception\TransformationFailedException;
  20599.  
  20600. /**
  20601.  * @author Bernhard Schussek <[email protected]>
  20602.  *
  20603.  * @implements DataTransformerInterface<array, array>
  20604.  */
  20605. class 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 <[email protected]>
  20674.  *
  20675.  * For the full copyright and license information, please view the LICENSE
  20676.  * file that was distributed with this source code.
  20677.  */
  20678.  
  20679. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  20680.  
  20681. use Symfony\Component\Form\Exception\InvalidArgumentException;
  20682. use Symfony\Component\Form\Exception\TransformationFailedException;
  20683. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  20684.  
  20685. /**
  20686.  * Transforms between a normalized time and a localized time string.
  20687.  *
  20688.  * @author Bernhard Schussek <[email protected]>
  20689.  * @author Florian Eckerstorfer <[email protected]>
  20690.  *
  20691.  * @extends BaseDateTimeTransformer<string>
  20692.  */
  20693. class 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 <[email protected]>
  20874.  *
  20875.  * For the full copyright and license information, please view the LICENSE
  20876.  * file that was distributed with this source code.
  20877.  */
  20878.  
  20879. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  20880.  
  20881. use Symfony\Component\Form\Exception\TransformationFailedException;
  20882.  
  20883. /**
  20884.  * Transforms between a normalized format and a localized money string.
  20885.  *
  20886.  * @author Bernhard Schussek <[email protected]>
  20887.  * @author Florian Eckerstorfer <[email protected]>
  20888.  */
  20889. class 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 <[email protected]>
  20964.  *
  20965.  * For the full copyright and license information, please view the LICENSE
  20966.  * file that was distributed with this source code.
  20967.  */
  20968.  
  20969. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  20970.  
  20971. use Symfony\Component\Form\DataTransformerInterface;
  20972. use Symfony\Component\Form\Exception\TransformationFailedException;
  20973. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  20974.  
  20975. /**
  20976.  * Transforms between a date string and a DateInterval object.
  20977.  *
  20978.  * @author Steffen Roßkamp <[email protected]>
  20979.  *
  20980.  * @implements DataTransformerInterface<\DateInterval, string>
  20981.  */
  20982. class 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 <[email protected]>
  21065.  *
  21066.  * For the full copyright and license information, please view the LICENSE
  21067.  * file that was distributed with this source code.
  21068.  */
  21069.  
  21070. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  21071.  
  21072. use Symfony\Component\Form\DataTransformerInterface;
  21073. use Symfony\Component\Form\Exception\TransformationFailedException;
  21074. use 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 <[email protected]>
  21080.  * @author Florian Eckerstorfer <[email protected]>
  21081.  *
  21082.  * @implements DataTransformerInterface<int|float, string>
  21083.  */
  21084. class 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 <[email protected]>
  21288.  *
  21289.  * For the full copyright and license information, please view the LICENSE
  21290.  * file that was distributed with this source code.
  21291.  */
  21292.  
  21293. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  21294.  
  21295. use Symfony\Component\Form\DataTransformerInterface;
  21296. use Symfony\Component\Form\Exception\TransformationFailedException;
  21297.  
  21298. /**
  21299.  * Transforms between a DateTimeImmutable object and a DateTime object.
  21300.  *
  21301.  * @author Valentin Udaltsov <[email protected]>
  21302.  *
  21303.  * @implements DataTransformerInterface<\DateTimeImmutable, \DateTime>
  21304.  */
  21305. final 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 <[email protected]>
  21357.  *
  21358.  * For the full copyright and license information, please view the LICENSE
  21359.  * file that was distributed with this source code.
  21360.  */
  21361.  
  21362. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  21363.  
  21364. use Symfony\Component\Form\DataTransformerInterface;
  21365. use Symfony\Component\Form\Exception\TransformationFailedException;
  21366.  
  21367. /**
  21368.  * Transforms between a timezone identifier string and a DateTimeZone object.
  21369.  *
  21370.  * @author Roland Franssen <[email protected]>
  21371.  *
  21372.  * @implements DataTransformerInterface<\DateTimeZone|array<\DateTimeZone>, string|array<string>>
  21373.  */
  21374. class 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 <[email protected]>
  21437.  *
  21438.  * For the full copyright and license information, please view the LICENSE
  21439.  * file that was distributed with this source code.
  21440.  */
  21441.  
  21442. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  21443.  
  21444. use Symfony\Component\Form\DataTransformerInterface;
  21445. use 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 <[email protected]>
  21452.  * @author Florian Eckerstorfer <[email protected]>
  21453.  *
  21454.  * @implements DataTransformerInterface<int|float, string>
  21455.  */
  21456. class 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 <[email protected]>
  21650.  *
  21651.  * For the full copyright and license information, please view the LICENSE
  21652.  * file that was distributed with this source code.
  21653.  */
  21654.  
  21655. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  21656.  
  21657. use Symfony\Component\Form\DataTransformerInterface;
  21658. use Symfony\Component\Form\Exception\TransformationFailedException;
  21659.  
  21660. /**
  21661.  * Transforms between a timezone identifier string and a IntlTimeZone object.
  21662.  *
  21663.  * @author Roland Franssen <[email protected]>
  21664.  *
  21665.  * @implements DataTransformerInterface<\IntlTimeZone|array<\IntlTimeZone>, string|array<string>>
  21666.  */
  21667. class 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 <[email protected]>
  21732.  *
  21733.  * For the full copyright and license information, please view the LICENSE
  21734.  * file that was distributed with this source code.
  21735.  */
  21736.  
  21737. namespace Symfony\Component\Form\Extension\Core\DataTransformer;
  21738.  
  21739. use Symfony\Component\Form\DataTransformerInterface;
  21740. use Symfony\Component\Form\Exception\TransformationFailedException;
  21741. use Symfony\Component\Uid\Uuid;
  21742.  
  21743. /**
  21744.  * Transforms between a UUID string and a Uuid object.
  21745.  *
  21746.  * @author Pavel Dyakonov <[email protected]>
  21747.  *
  21748.  * @implements DataTransformerInterface<Uuid, string>
  21749.  */
  21750. class 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 <[email protected]>
  21811.  *
  21812.  * For the full copyright and license information, please view the LICENSE
  21813.  * file that was distributed with this source code.
  21814.  */
  21815.  
  21816. namespace Symfony\Component\Form\Extension\Validator;
  21817.  
  21818. use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
  21819. use Symfony\Component\Form\Extension\Core\Type\CollectionType;
  21820. use Symfony\Component\Form\Extension\Core\Type\CountryType;
  21821. use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
  21822. use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
  21823. use Symfony\Component\Form\Extension\Core\Type\DateType;
  21824. use Symfony\Component\Form\Extension\Core\Type\EmailType;
  21825. use Symfony\Component\Form\Extension\Core\Type\FileType;
  21826. use Symfony\Component\Form\Extension\Core\Type\IntegerType;
  21827. use Symfony\Component\Form\Extension\Core\Type\LanguageType;
  21828. use Symfony\Component\Form\Extension\Core\Type\LocaleType;
  21829. use Symfony\Component\Form\Extension\Core\Type\NumberType;
  21830. use Symfony\Component\Form\Extension\Core\Type\TextType;
  21831. use Symfony\Component\Form\Extension\Core\Type\TimeType;
  21832. use Symfony\Component\Form\Extension\Core\Type\UrlType;
  21833. use Symfony\Component\Form\FormTypeGuesserInterface;
  21834. use Symfony\Component\Form\Guess\Guess;
  21835. use Symfony\Component\Form\Guess\TypeGuess;
  21836. use Symfony\Component\Form\Guess\ValueGuess;
  21837. use Symfony\Component\Validator\Constraint;
  21838. use Symfony\Component\Validator\Constraints\Count;
  21839. use Symfony\Component\Validator\Constraints\Country;
  21840. use Symfony\Component\Validator\Constraints\Currency;
  21841. use Symfony\Component\Validator\Constraints\Date;
  21842. use Symfony\Component\Validator\Constraints\DateTime;
  21843. use Symfony\Component\Validator\Constraints\Email;
  21844. use Symfony\Component\Validator\Constraints\File;
  21845. use Symfony\Component\Validator\Constraints\Image;
  21846. use Symfony\Component\Validator\Constraints\Ip;
  21847. use Symfony\Component\Validator\Constraints\IsFalse;
  21848. use Symfony\Component\Validator\Constraints\IsTrue;
  21849. use Symfony\Component\Validator\Constraints\Language;
  21850. use Symfony\Component\Validator\Constraints\Length;
  21851. use Symfony\Component\Validator\Constraints\Locale;
  21852. use Symfony\Component\Validator\Constraints\NotBlank;
  21853. use Symfony\Component\Validator\Constraints\NotNull;
  21854. use Symfony\Component\Validator\Constraints\Range;
  21855. use Symfony\Component\Validator\Constraints\Regex;
  21856. use Symfony\Component\Validator\Constraints\Time;
  21857. use Symfony\Component\Validator\Constraints\Type;
  21858. use Symfony\Component\Validator\Constraints\Url;
  21859. use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
  21860. use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
  21861.  
  21862. class 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 <[email protected]>
  22106.  *
  22107.  * For the full copyright and license information, please view the LICENSE
  22108.  * file that was distributed with this source code.
  22109.  */
  22110.  
  22111. namespace Symfony\Component\Form\Extension\Validator;
  22112.  
  22113. use Symfony\Component\Form\AbstractExtension;
  22114. use Symfony\Component\Form\Extension\Validator\Constraints\Form;
  22115. use Symfony\Component\Form\FormRendererInterface;
  22116. use Symfony\Component\Form\FormTypeGuesserInterface;
  22117. use Symfony\Component\Validator\Constraints\Traverse;
  22118. use Symfony\Component\Validator\Mapping\ClassMetadata;
  22119. use Symfony\Component\Validator\Validator\ValidatorInterface;
  22120. use Symfony\Contracts\Translation\TranslatorInterface;
  22121.  
  22122. /**
  22123.  * Extension supporting the Symfony Validator component in forms.
  22124.  *
  22125.  * @author Bernhard Schussek <[email protected]>
  22126.  */
  22127. class 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 <[email protected]>
  22171.  *
  22172.  * For the full copyright and license information, please view the LICENSE
  22173.  * file that was distributed with this source code.
  22174.  */
  22175.  
  22176. namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
  22177.  
  22178. use Symfony\Component\Form\Exception\OutOfBoundsException;
  22179. use Symfony\Component\PropertyAccess\PropertyPath;
  22180. use Symfony\Component\PropertyAccess\PropertyPathInterface;
  22181.  
  22182. /**
  22183.  * @author Bernhard Schussek <[email protected]>
  22184.  *
  22185.  * @implements \IteratorAggregate<int, string>
  22186.  */
  22187. class 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 <[email protected]>
  22392.  *
  22393.  * For the full copyright and license information, please view the LICENSE
  22394.  * file that was distributed with this source code.
  22395.  */
  22396.  
  22397. namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
  22398.  
  22399. use Symfony\Component\PropertyAccess\PropertyPathIterator;
  22400.  
  22401. /**
  22402.  * @author Bernhard Schussek <[email protected]>
  22403.  */
  22404. class 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 <[email protected]>
  22426.  *
  22427.  * For the full copyright and license information, please view the LICENSE
  22428.  * file that was distributed with this source code.
  22429.  */
  22430.  
  22431. namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
  22432.  
  22433. use Symfony\Component\Form\Exception\ErrorMappingException;
  22434. use Symfony\Component\Form\FormInterface;
  22435.  
  22436. /**
  22437.  * @author Bernhard Schussek <[email protected]>
  22438.  */
  22439. class 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 <[email protected]>
  22504.  *
  22505.  * For the full copyright and license information, please view the LICENSE
  22506.  * file that was distributed with this source code.
  22507.  */
  22508.  
  22509. namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
  22510.  
  22511. use Symfony\Component\Form\FormInterface;
  22512. use Symfony\Component\PropertyAccess\PropertyPath;
  22513.  
  22514. /**
  22515.  * @author Bernhard Schussek <[email protected]>
  22516.  */
  22517. class 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 <[email protected]>
  22541.  *
  22542.  * For the full copyright and license information, please view the LICENSE
  22543.  * file that was distributed with this source code.
  22544.  */
  22545.  
  22546. namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
  22547.  
  22548. use Symfony\Component\Form\FormInterface;
  22549. use Symfony\Component\Validator\ConstraintViolation;
  22550.  
  22551. /**
  22552.  * @author Bernhard Schussek <[email protected]>
  22553.  */
  22554. interface 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 <[email protected]>
  22574.  *
  22575.  * For the full copyright and license information, please view the LICENSE
  22576.  * file that was distributed with this source code.
  22577.  */
  22578.  
  22579. namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
  22580.  
  22581. use Symfony\Component\Form\FileUploadError;
  22582. use Symfony\Component\Form\FormError;
  22583. use Symfony\Component\Form\FormInterface;
  22584. use Symfony\Component\Form\FormRendererInterface;
  22585. use Symfony\Component\Form\Util\InheritDataAwareIterator;
  22586. use Symfony\Component\PropertyAccess\PropertyPathBuilder;
  22587. use Symfony\Component\PropertyAccess\PropertyPathIterator;
  22588. use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
  22589. use Symfony\Component\Validator\Constraints\File;
  22590. use Symfony\Component\Validator\ConstraintViolation;
  22591. use Symfony\Contracts\Translation\TranslatorInterface;
  22592.  
  22593. /**
  22594.  * @author Bernhard Schussek <[email protected]>
  22595.  */
  22596. class 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 <[email protected]>
  22916.  *
  22917.  * For the full copyright and license information, please view the LICENSE
  22918.  * file that was distributed with this source code.
  22919.  */
  22920.  
  22921. namespace Symfony\Component\Form\Extension\Validator\Constraints;
  22922.  
  22923. use Symfony\Component\Form\FormInterface;
  22924. use Symfony\Component\Validator\Constraint;
  22925. use Symfony\Component\Validator\Constraints\Composite;
  22926. use Symfony\Component\Validator\Constraints\GroupSequence;
  22927. use Symfony\Component\Validator\Constraints\Valid;
  22928. use Symfony\Component\Validator\ConstraintValidator;
  22929. use Symfony\Component\Validator\Exception\UnexpectedTypeException;
  22930.  
  22931. /**
  22932.  * @author Bernhard Schussek <[email protected]>
  22933.  */
  22934. class 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 <[email protected]>
  23196.  *
  23197.  * For the full copyright and license information, please view the LICENSE
  23198.  * file that was distributed with this source code.
  23199.  */
  23200.  
  23201. namespace Symfony\Component\Form\Extension\Validator\Constraints;
  23202.  
  23203. use Symfony\Component\Validator\Constraint;
  23204.  
  23205. /**
  23206.  * @author Bernhard Schussek <[email protected]>
  23207.  */
  23208. class 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 <[email protected]>
  23233.  *
  23234.  * For the full copyright and license information, please view the LICENSE
  23235.  * file that was distributed with this source code.
  23236.  */
  23237.  
  23238. namespace Symfony\Component\Form\Extension\Validator\EventListener;
  23239.  
  23240. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  23241. use Symfony\Component\Form\Extension\Validator\Constraints\Form;
  23242. use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface;
  23243. use Symfony\Component\Form\FormEvent;
  23244. use Symfony\Component\Form\FormEvents;
  23245. use Symfony\Component\Validator\Validator\ValidatorInterface;
  23246.  
  23247. /**
  23248.  * @author Bernhard Schussek <[email protected]>
  23249.  */
  23250. class 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 <[email protected]>
  23289.  *
  23290.  * For the full copyright and license information, please view the LICENSE
  23291.  * file that was distributed with this source code.
  23292.  */
  23293.  
  23294. namespace Symfony\Component\Form\Extension\Validator\Type;
  23295.  
  23296. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  23297.  
  23298. /**
  23299.  * @author Bernhard Schussek <[email protected]>
  23300.  */
  23301. class 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 <[email protected]>
  23318.  *
  23319.  * For the full copyright and license information, please view the LICENSE
  23320.  * file that was distributed with this source code.
  23321.  */
  23322.  
  23323. namespace Symfony\Component\Form\Extension\Validator\Type;
  23324.  
  23325. use Symfony\Component\Form\Extension\Core\Type\FormType;
  23326. use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
  23327. use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
  23328. use Symfony\Component\Form\FormBuilderInterface;
  23329. use Symfony\Component\Form\FormRendererInterface;
  23330. use Symfony\Component\OptionsResolver\Options;
  23331. use Symfony\Component\OptionsResolver\OptionsResolver;
  23332. use Symfony\Component\Validator\Constraint;
  23333. use Symfony\Component\Validator\Validator\ValidatorInterface;
  23334. use Symfony\Contracts\Translation\TranslatorInterface;
  23335.  
  23336. /**
  23337.  * @author Bernhard Schussek <[email protected]>
  23338.  */
  23339. class 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 <[email protected]>
  23391.  *
  23392.  * For the full copyright and license information, please view the LICENSE
  23393.  * file that was distributed with this source code.
  23394.  */
  23395.  
  23396. namespace Symfony\Component\Form\Extension\Validator\Type;
  23397.  
  23398. use Symfony\Component\Form\AbstractTypeExtension;
  23399. use Symfony\Component\OptionsResolver\Options;
  23400. use Symfony\Component\OptionsResolver\OptionsResolver;
  23401. use Symfony\Component\Validator\Constraints\GroupSequence;
  23402.  
  23403. /**
  23404.  * Encapsulates common logic of {@link FormTypeValidatorExtension} and
  23405.  * {@link SubmitTypeValidatorExtension}.
  23406.  *
  23407.  * @author Bernhard Schussek <[email protected]>
  23408.  */
  23409. abstract 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 <[email protected]>
  23451.  *
  23452.  * For the full copyright and license information, please view the LICENSE
  23453.  * file that was distributed with this source code.
  23454.  */
  23455.  
  23456. namespace Symfony\Component\Form\Extension\Validator\Type;
  23457.  
  23458. use Symfony\Component\Form\AbstractTypeExtension;
  23459. use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
  23460. use Symfony\Component\OptionsResolver\Options;
  23461. use Symfony\Component\OptionsResolver\OptionsResolver;
  23462.  
  23463. /**
  23464.  * @author Bernhard Schussek <[email protected]>
  23465.  */
  23466. class 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 <[email protected]>
  23493.  *
  23494.  * For the full copyright and license information, please view the LICENSE
  23495.  * file that was distributed with this source code.
  23496.  */
  23497.  
  23498. namespace Symfony\Component\Form\Extension\Validator\Type;
  23499.  
  23500. use Symfony\Component\Form\AbstractTypeExtension;
  23501. use Symfony\Component\Form\Extension\Core\Type\FormType;
  23502. use Symfony\Component\OptionsResolver\Options;
  23503. use Symfony\Component\OptionsResolver\OptionsResolver;
  23504. use Symfony\Contracts\Translation\TranslatorInterface;
  23505.  
  23506. /**
  23507.  * @author Abdellatif Ait boudad <[email protected]>
  23508.  * @author David Badura <[email protected]>
  23509.  */
  23510. class 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 <[email protected]>
  23540.  *
  23541.  * For the full copyright and license information, please view the LICENSE
  23542.  * file that was distributed with this source code.
  23543.  */
  23544.  
  23545. namespace Symfony\Component\Form\Extension\PasswordHasher\Type;
  23546.  
  23547. use Symfony\Component\Form\AbstractTypeExtension;
  23548. use Symfony\Component\Form\Extension\Core\Type\PasswordType;
  23549. use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
  23550. use Symfony\Component\Form\FormBuilderInterface;
  23551. use Symfony\Component\Form\FormEvents;
  23552. use Symfony\Component\OptionsResolver\OptionsResolver;
  23553. use Symfony\Component\PropertyAccess\PropertyPath;
  23554.  
  23555. /**
  23556.  * @author Sébastien Alfaiate <[email protected]>
  23557.  */
  23558. class 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 <[email protected]>
  23598.  *
  23599.  * For the full copyright and license information, please view the LICENSE
  23600.  * file that was distributed with this source code.
  23601.  */
  23602.  
  23603. namespace Symfony\Component\Form\Extension\PasswordHasher\Type;
  23604.  
  23605. use Symfony\Component\Form\AbstractTypeExtension;
  23606. use Symfony\Component\Form\Extension\Core\Type\FormType;
  23607. use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
  23608. use Symfony\Component\Form\FormBuilderInterface;
  23609. use Symfony\Component\Form\FormEvents;
  23610.  
  23611. /**
  23612.  * @author Sébastien Alfaiate <[email protected]>
  23613.  */
  23614. class 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 <[email protected]>
  23641.  *
  23642.  * For the full copyright and license information, please view the LICENSE
  23643.  * file that was distributed with this source code.
  23644.  */
  23645.  
  23646. namespace Symfony\Component\Form\Extension\PasswordHasher\EventListener;
  23647.  
  23648. use Symfony\Component\Form\Exception\InvalidConfigurationException;
  23649. use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
  23650. use Symfony\Component\Form\FormEvent;
  23651. use Symfony\Component\Form\FormInterface;
  23652. use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
  23653. use Symfony\Component\PropertyAccess\PropertyAccess;
  23654. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  23655. use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
  23656.  
  23657. /**
  23658.  * @author Sébastien Alfaiate <[email protected]>
  23659.  * @author Gábor Egyed <[email protected]>
  23660.  */
  23661. class 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 <[email protected]>
  23755.  *
  23756.  * For the full copyright and license information, please view the LICENSE
  23757.  * file that was distributed with this source code.
  23758.  */
  23759.  
  23760. namespace Symfony\Component\Form\Extension\PasswordHasher;
  23761.  
  23762. use Symfony\Component\Form\AbstractExtension;
  23763. use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
  23764.  
  23765. /**
  23766.  * Integrates the PasswordHasher component with the Form library.
  23767.  *
  23768.  * @author Sébastien Alfaiate <[email protected]>
  23769.  */
  23770. class 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 <[email protected]>
  23795.  *
  23796.  * For the full copyright and license information, please view the LICENSE
  23797.  * file that was distributed with this source code.
  23798.  */
  23799.  
  23800. namespace Symfony\Component\Form;
  23801.  
  23802. use Symfony\Component\OptionsResolver\OptionsResolver;
  23803.  
  23804. /**
  23805.  * @author Bernhard Schussek <[email protected]>
  23806.  */
  23807. abstract 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 <[email protected]>
  23835.  *
  23836.  * For the full copyright and license information, please view the LICENSE
  23837.  * file that was distributed with this source code.
  23838.  */
  23839.  
  23840. namespace Symfony\Component\Form;
  23841.  
  23842. use Symfony\Component\Form\Extension\Core\Type\FormType;
  23843. use 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 <[email protected]>
  23849.  */
  23850. interface 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 <[email protected]>
  23929.  *
  23930.  * For the full copyright and license information, please view the LICENSE
  23931.  * file that was distributed with this source code.
  23932.  */
  23933.  
  23934. namespace Symfony\Component\Form;
  23935.  
  23936. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  23937. use Symfony\Component\Form\Exception\BadMethodCallException;
  23938. use Symfony\Component\Form\Exception\InvalidArgumentException;
  23939. use Symfony\Component\Form\Extension\Core\Type\TextType;
  23940.  
  23941. /**
  23942.  * A builder for creating {@link Form} instances.
  23943.  *
  23944.  * @author Bernhard Schussek <[email protected]>
  23945.  *
  23946.  * @implements \IteratorAggregate<string, FormBuilderInterface>
  23947.  */
  23948. class 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 <[email protected]>
  24145.  *
  24146.  * For the full copyright and license information, please view the LICENSE
  24147.  * file that was distributed with this source code.
  24148.  */
  24149.  
  24150. namespace Symfony\Component\Form;
  24151.  
  24152. use Symfony\Component\Form\Exception\BadMethodCallException;
  24153. use Symfony\Component\Form\Exception\LogicException;
  24154. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  24155. use Twig\Environment;
  24156.  
  24157. /**
  24158.  * Renders a form into HTML using a rendering engine.
  24159.  *
  24160.  * @author Bernhard Schussek <[email protected]>
  24161.  */
  24162. class 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 <[email protected]>
  24432.  *
  24433.  * For the full copyright and license information, please view the LICENSE
  24434.  * file that was distributed with this source code.
  24435.  */
  24436.  
  24437. namespace Symfony\Component\Form;
  24438.  
  24439. /**
  24440.  * @author Bernhard Schussek <[email protected]>
  24441.  */
  24442. class 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 <[email protected]>
  24459.  *
  24460.  * For the full copyright and license information, please view the LICENSE
  24461.  * file that was distributed with this source code.
  24462.  */
  24463.  
  24464. namespace Symfony\Component\Form;
  24465.  
  24466. /**
  24467.  * A builder for {@link SubmitButton} instances.
  24468.  *
  24469.  * @author Bernhard Schussek <[email protected]>
  24470.  */
  24471. class 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 <[email protected]>
  24491.  *
  24492.  * For the full copyright and license information, please view the LICENSE
  24493.  * file that was distributed with this source code.
  24494.  */
  24495.  
  24496. namespace Symfony\Component\Form;
  24497.  
  24498. /**
  24499.  * Interface for extensions which provide types, type extensions and a guesser.
  24500.  */
  24501. interface 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 <[email protected]>
  24550.  *
  24551.  * For the full copyright and license information, please view the LICENSE
  24552.  * file that was distributed with this source code.
  24553.  */
  24554.  
  24555. namespace Symfony\Component\Form;
  24556.  
  24557. /**
  24558.  * Renders a form into HTML.
  24559.  *
  24560.  * @author Bernhard Schussek <[email protected]>
  24561.  */
  24562. interface 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 <[email protected]>
  24639.  *
  24640.  * For the full copyright and license information, please view the LICENSE
  24641.  * file that was distributed with this source code.
  24642.  */
  24643.  
  24644. namespace Symfony\Component\Form;
  24645.  
  24646. /**
  24647.  * @internal
  24648.  */
  24649. class 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 <[email protected]>
  24662.  *
  24663.  * For the full copyright and license information, please view the LICENSE
  24664.  * file that was distributed with this source code.
  24665.  */
  24666.  
  24667. namespace Symfony\Component\Form;
  24668.  
  24669. class 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.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement