Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- ------------------------------------------------------------------------------------------------------------------------
- ./AbstractRendererEngine.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Contracts\Service\ResetInterface;
- /**
- * Default implementation of {@link FormRendererEngineInterface}.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- abstract class AbstractRendererEngine implements FormRendererEngineInterface, ResetInterface
- {
- /**
- * The variable in {@link FormView} used as cache key.
- */
- public const CACHE_KEY_VAR = 'cache_key';
- /**
- * @var array[]
- */
- protected array $themes = [];
- /**
- * @var bool[]
- */
- protected array $useDefaultThemes = [];
- /**
- * @var array[]
- */
- protected array $resources = [];
- /**
- * @var array<array<int|false>>
- */
- private array $resourceHierarchyLevels = [];
- /**
- * Creates a new renderer engine.
- *
- * @param array $defaultThemes The default themes. The type of these
- * themes is open to the implementation.
- */
- public function __construct(
- protected array $defaultThemes = [],
- ) {
- }
- public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void
- {
- $cacheKey = $view->vars[self::CACHE_KEY_VAR];
- // Do not cast, as casting turns objects into arrays of properties
- $this->themes[$cacheKey] = \is_array($themes) ? $themes : [$themes];
- $this->useDefaultThemes[$cacheKey] = $useDefaultThemes;
- // Unset instead of resetting to an empty array, in order to allow
- // implementations (like TwigRendererEngine) to check whether $cacheKey
- // is set at all.
- unset($this->resources[$cacheKey], $this->resourceHierarchyLevels[$cacheKey]);
- }
- public function getResourceForBlockName(FormView $view, string $blockName): mixed
- {
- $cacheKey = $view->vars[self::CACHE_KEY_VAR];
- if (!isset($this->resources[$cacheKey][$blockName])) {
- $this->loadResourceForBlockName($cacheKey, $view, $blockName);
- }
- return $this->resources[$cacheKey][$blockName];
- }
- public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): mixed
- {
- $cacheKey = $view->vars[self::CACHE_KEY_VAR];
- $blockName = $blockNameHierarchy[$hierarchyLevel];
- if (!isset($this->resources[$cacheKey][$blockName])) {
- $this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $hierarchyLevel);
- }
- return $this->resources[$cacheKey][$blockName];
- }
- public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): int|false
- {
- $cacheKey = $view->vars[self::CACHE_KEY_VAR];
- $blockName = $blockNameHierarchy[$hierarchyLevel];
- if (!isset($this->resources[$cacheKey][$blockName])) {
- $this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $hierarchyLevel);
- }
- // If $block was previously rendered loaded with loadTemplateForBlock(), the template
- // is cached but the hierarchy level is not. In this case, we know that the block
- // exists at this very hierarchy level, so we can just set it.
- if (!isset($this->resourceHierarchyLevels[$cacheKey][$blockName])) {
- $this->resourceHierarchyLevels[$cacheKey][$blockName] = $hierarchyLevel;
- }
- return $this->resourceHierarchyLevels[$cacheKey][$blockName];
- }
- /**
- * Loads the cache with the resource for a given block name.
- *
- * @see getResourceForBlock()
- */
- abstract protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName): bool;
- /**
- * Loads the cache with the resource for a specific level of a block hierarchy.
- *
- * @see getResourceForBlockHierarchy()
- */
- private function loadResourceForBlockNameHierarchy(string $cacheKey, FormView $view, array $blockNameHierarchy, int $hierarchyLevel): bool
- {
- $blockName = $blockNameHierarchy[$hierarchyLevel];
- // Try to find a template for that block
- if ($this->loadResourceForBlockName($cacheKey, $view, $blockName)) {
- // If loadTemplateForBlock() returns true, it was able to populate the
- // cache. The only missing thing is to set the hierarchy level at which
- // the template was found.
- $this->resourceHierarchyLevels[$cacheKey][$blockName] = $hierarchyLevel;
- return true;
- }
- if ($hierarchyLevel > 0) {
- $parentLevel = $hierarchyLevel - 1;
- $parentBlockName = $blockNameHierarchy[$parentLevel];
- // The next two if statements contain slightly duplicated code. This is by intention
- // and tries to avoid execution of unnecessary checks in order to increase performance.
- if (isset($this->resources[$cacheKey][$parentBlockName])) {
- // It may happen that the parent block is already loaded, but its level is not.
- // In this case, the parent block must have been loaded by loadResourceForBlock(),
- // which does not check the hierarchy of the block. Subsequently the block must have
- // been found directly on the parent level.
- if (!isset($this->resourceHierarchyLevels[$cacheKey][$parentBlockName])) {
- $this->resourceHierarchyLevels[$cacheKey][$parentBlockName] = $parentLevel;
- }
- // Cache the shortcuts for further accesses
- $this->resources[$cacheKey][$blockName] = $this->resources[$cacheKey][$parentBlockName];
- $this->resourceHierarchyLevels[$cacheKey][$blockName] = $this->resourceHierarchyLevels[$cacheKey][$parentBlockName];
- return true;
- }
- if ($this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $parentLevel)) {
- // Cache the shortcuts for further accesses
- $this->resources[$cacheKey][$blockName] = $this->resources[$cacheKey][$parentBlockName];
- $this->resourceHierarchyLevels[$cacheKey][$blockName] = $this->resourceHierarchyLevels[$cacheKey][$parentBlockName];
- return true;
- }
- }
- // Cache the result for further accesses
- $this->resources[$cacheKey][$blockName] = false;
- $this->resourceHierarchyLevels[$cacheKey][$blockName] = false;
- return false;
- }
- public function reset(): void
- {
- $this->themes = [];
- $this->useDefaultThemes = [];
- $this->resources = [];
- $this->resourceHierarchyLevels = [];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormFactory.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Extension\Core\Type\FormType;
- use Symfony\Component\Form\Extension\Core\Type\TextType;
- class FormFactory implements FormFactoryInterface
- {
- public function __construct(
- private FormRegistryInterface $registry,
- ) {
- }
- public function create(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
- {
- return $this->createBuilder($type, $data, $options)->getForm();
- }
- public function createNamed(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
- {
- return $this->createNamedBuilder($name, $type, $data, $options)->getForm();
- }
- public function createForProperty(string $class, string $property, mixed $data = null, array $options = []): FormInterface
- {
- return $this->createBuilderForProperty($class, $property, $data, $options)->getForm();
- }
- public function createBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface
- {
- return $this->createNamedBuilder($this->registry->getType($type)->getBlockPrefix(), $type, $data, $options);
- }
- public function createNamedBuilder(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface
- {
- if (null !== $data && !\array_key_exists('data', $options)) {
- $options['data'] = $data;
- }
- $type = $this->registry->getType($type);
- $builder = $type->createBuilder($this, $name, $options);
- // Explicitly call buildForm() in order to be able to override either
- // createBuilder() or buildForm() in the resolved form type
- $type->buildForm($builder, $builder->getOptions());
- return $builder;
- }
- public function createBuilderForProperty(string $class, string $property, mixed $data = null, array $options = []): FormBuilderInterface
- {
- if (null === $guesser = $this->registry->getTypeGuesser()) {
- return $this->createNamedBuilder($property, TextType::class, $data, $options);
- }
- $typeGuess = $guesser->guessType($class, $property);
- $maxLengthGuess = $guesser->guessMaxLength($class, $property);
- $requiredGuess = $guesser->guessRequired($class, $property);
- $patternGuess = $guesser->guessPattern($class, $property);
- $type = $typeGuess ? $typeGuess->getType() : TextType::class;
- $maxLength = $maxLengthGuess?->getValue();
- $pattern = $patternGuess?->getValue();
- if (null !== $pattern) {
- $options = array_replace_recursive(['attr' => ['pattern' => $pattern]], $options);
- }
- if (null !== $maxLength) {
- $options = array_replace_recursive(['attr' => ['maxlength' => $maxLength]], $options);
- }
- if ($requiredGuess) {
- $options = array_merge(['required' => $requiredGuess->getValue()], $options);
- }
- // user options may override guessed options
- if ($typeGuess) {
- $attrs = [];
- $typeGuessOptions = $typeGuess->getOptions();
- if (isset($typeGuessOptions['attr']) && isset($options['attr'])) {
- $attrs = ['attr' => array_merge($typeGuessOptions['attr'], $options['attr'])];
- }
- $options = array_merge($typeGuessOptions, $options, $attrs);
- }
- return $this->createNamedBuilder($property, $type, $data, $options);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./AbstractType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Extension\Core\Type\FormType;
- use Symfony\Component\Form\Util\StringUtil;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- abstract class AbstractType implements FormTypeInterface
- {
- /**
- * @return string|null
- */
- public function getParent()
- {
- return FormType::class;
- }
- /**
- * @return void
- */
- public function configureOptions(OptionsResolver $resolver)
- {
- }
- /**
- * @return void
- */
- public function buildForm(FormBuilderInterface $builder, array $options)
- {
- }
- /**
- * @return void
- */
- public function buildView(FormView $view, FormInterface $form, array $options)
- {
- }
- /**
- * @return void
- */
- public function finishView(FormView $view, FormInterface $form, array $options)
- {
- }
- /**
- * @return string
- */
- public function getBlockPrefix()
- {
- return StringUtil::fqcnToBlockPrefix(static::class) ?: '';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ResolvedFormTypeInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * A wrapper for a form type and its extensions.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface ResolvedFormTypeInterface
- {
- /**
- * Returns the prefix of the template block name for this type.
- */
- public function getBlockPrefix(): string;
- /**
- * Returns the parent type.
- */
- public function getParent(): ?self;
- /**
- * Returns the wrapped form type.
- */
- public function getInnerType(): FormTypeInterface;
- /**
- * Returns the extensions of the wrapped form type.
- *
- * @return FormTypeExtensionInterface[]
- */
- public function getTypeExtensions(): array;
- /**
- * Creates a new form builder for this type.
- *
- * @param string $name The name for the builder
- */
- public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface;
- /**
- * Creates a new form view for a form of this type.
- */
- public function createView(FormInterface $form, ?FormView $parent = null): FormView;
- /**
- * Configures a form builder for the type hierarchy.
- */
- public function buildForm(FormBuilderInterface $builder, array $options): void;
- /**
- * Configures a form view for the type hierarchy.
- *
- * It is called before the children of the view are built.
- */
- public function buildView(FormView $view, FormInterface $form, array $options): void;
- /**
- * Finishes a form view for the type hierarchy.
- *
- * It is called after the children of the view have been built.
- */
- public function finishView(FormView $view, FormInterface $form, array $options): void;
- /**
- * Returns the configured options resolver used for this type.
- */
- public function getOptionsResolver(): OptionsResolver;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ClearableErrorsInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * A form element whose errors can be cleared.
- *
- * @author Colin O'Dell <[email protected]>
- */
- interface ClearableErrorsInterface
- {
- /**
- * Removes all the errors of this form.
- *
- * @param bool $deep Whether to remove errors from child forms as well
- *
- * @return $this
- */
- public function clearErrors(bool $deep = false): static;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormFactoryBuilderInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * A builder for FormFactoryInterface objects.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormFactoryBuilderInterface
- {
- /**
- * Sets the factory for creating ResolvedFormTypeInterface instances.
- *
- * @return $this
- */
- public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory): static;
- /**
- * Adds an extension to be loaded by the factory.
- *
- * @return $this
- */
- public function addExtension(FormExtensionInterface $extension): static;
- /**
- * Adds a list of extensions to be loaded by the factory.
- *
- * @param FormExtensionInterface[] $extensions The extensions
- *
- * @return $this
- */
- public function addExtensions(array $extensions): static;
- /**
- * Adds a form type to the factory.
- *
- * @return $this
- */
- public function addType(FormTypeInterface $type): static;
- /**
- * Adds a list of form types to the factory.
- *
- * @param FormTypeInterface[] $types The form types
- *
- * @return $this
- */
- public function addTypes(array $types): static;
- /**
- * Adds a form type extension to the factory.
- *
- * @return $this
- */
- public function addTypeExtension(FormTypeExtensionInterface $typeExtension): static;
- /**
- * Adds a list of form type extensions to the factory.
- *
- * @param FormTypeExtensionInterface[] $typeExtensions The form type extensions
- *
- * @return $this
- */
- public function addTypeExtensions(array $typeExtensions): static;
- /**
- * Adds a type guesser to the factory.
- *
- * @return $this
- */
- public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser): static;
- /**
- * Adds a list of type guessers to the factory.
- *
- * @param FormTypeGuesserInterface[] $typeGuessers The type guessers
- *
- * @return $this
- */
- public function addTypeGuessers(array $typeGuessers): static;
- /**
- * Builds and returns the factory.
- */
- public function getFormFactory(): FormFactoryInterface;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ResolvedFormType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\EventDispatcher\EventDispatcher;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- use Symfony\Component\OptionsResolver\Exception\ExceptionInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * A wrapper for a form type and its extensions.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ResolvedFormType implements ResolvedFormTypeInterface
- {
- /**
- * @var FormTypeExtensionInterface[]
- */
- private array $typeExtensions;
- private OptionsResolver $optionsResolver;
- /**
- * @param FormTypeExtensionInterface[] $typeExtensions
- */
- public function __construct(
- private FormTypeInterface $innerType,
- array $typeExtensions = [],
- private ?ResolvedFormTypeInterface $parent = null,
- ) {
- foreach ($typeExtensions as $extension) {
- if (!$extension instanceof FormTypeExtensionInterface) {
- throw new UnexpectedTypeException($extension, FormTypeExtensionInterface::class);
- }
- }
- $this->typeExtensions = $typeExtensions;
- }
- public function getBlockPrefix(): string
- {
- return $this->innerType->getBlockPrefix();
- }
- public function getParent(): ?ResolvedFormTypeInterface
- {
- return $this->parent;
- }
- public function getInnerType(): FormTypeInterface
- {
- return $this->innerType;
- }
- public function getTypeExtensions(): array
- {
- return $this->typeExtensions;
- }
- public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface
- {
- try {
- $options = $this->getOptionsResolver()->resolve($options);
- } catch (ExceptionInterface $e) {
- 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);
- }
- // Should be decoupled from the specific option at some point
- $dataClass = $options['data_class'] ?? null;
- $builder = $this->newBuilder($name, $dataClass, $factory, $options);
- $builder->setType($this);
- return $builder;
- }
- public function createView(FormInterface $form, ?FormView $parent = null): FormView
- {
- return $this->newView($parent);
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $this->parent?->buildForm($builder, $options);
- $this->innerType->buildForm($builder, $options);
- foreach ($this->typeExtensions as $extension) {
- $extension->buildForm($builder, $options);
- }
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $this->parent?->buildView($view, $form, $options);
- $this->innerType->buildView($view, $form, $options);
- foreach ($this->typeExtensions as $extension) {
- $extension->buildView($view, $form, $options);
- }
- }
- public function finishView(FormView $view, FormInterface $form, array $options): void
- {
- $this->parent?->finishView($view, $form, $options);
- $this->innerType->finishView($view, $form, $options);
- foreach ($this->typeExtensions as $extension) {
- /* @var FormTypeExtensionInterface $extension */
- $extension->finishView($view, $form, $options);
- }
- }
- public function getOptionsResolver(): OptionsResolver
- {
- if (!isset($this->optionsResolver)) {
- if (null !== $this->parent) {
- $this->optionsResolver = clone $this->parent->getOptionsResolver();
- } else {
- $this->optionsResolver = new OptionsResolver();
- }
- $this->innerType->configureOptions($this->optionsResolver);
- foreach ($this->typeExtensions as $extension) {
- $extension->configureOptions($this->optionsResolver);
- }
- }
- return $this->optionsResolver;
- }
- /**
- * Creates a new builder instance.
- *
- * Override this method if you want to customize the builder class.
- */
- protected function newBuilder(string $name, ?string $dataClass, FormFactoryInterface $factory, array $options): FormBuilderInterface
- {
- if ($this->innerType instanceof ButtonTypeInterface) {
- return new ButtonBuilder($name, $options);
- }
- if ($this->innerType instanceof SubmitButtonTypeInterface) {
- return new SubmitButtonBuilder($name, $options);
- }
- return new FormBuilder($name, $dataClass, new EventDispatcher(), $factory, $options);
- }
- /**
- * Creates a new view instance.
- *
- * Override this method if you want to customize the view class.
- */
- protected function newView(?FormView $parent = null): FormView
- {
- return new FormView($parent);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Forms.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * Entry point of the Form component.
- *
- * Use this class to conveniently create new form factories:
- *
- * use Symfony\Component\Form\Forms;
- *
- * $formFactory = Forms::createFormFactory();
- *
- * $form = $formFactory->createBuilder()
- * ->add('firstName', 'Symfony\Component\Form\Extension\Core\Type\TextType')
- * ->add('lastName', 'Symfony\Component\Form\Extension\Core\Type\TextType')
- * ->add('age', 'Symfony\Component\Form\Extension\Core\Type\IntegerType')
- * ->add('color', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [
- * 'choices' => ['Red' => 'r', 'Blue' => 'b'],
- * ])
- * ->getForm();
- *
- * You can also add custom extensions to the form factory:
- *
- * $formFactory = Forms::createFormFactoryBuilder()
- * ->addExtension(new AcmeExtension())
- * ->getFormFactory();
- *
- * If you create custom form types or type extensions, it is
- * generally recommended to create your own extensions that lazily
- * load these types and type extensions. In projects where performance
- * does not matter that much, you can also pass them directly to the
- * form factory:
- *
- * $formFactory = Forms::createFormFactoryBuilder()
- * ->addType(new PersonType())
- * ->addType(new PhoneNumberType())
- * ->addTypeExtension(new FormTypeHelpTextExtension())
- * ->getFormFactory();
- *
- * Support for the Validator component is provided by ValidatorExtension.
- * This extension needs a validator object to function properly:
- *
- * use Symfony\Component\Validator\Validation;
- * use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
- *
- * $validator = Validation::createValidator();
- * $formFactory = Forms::createFormFactoryBuilder()
- * ->addExtension(new ValidatorExtension($validator))
- * ->getFormFactory();
- *
- * @author Bernhard Schussek <[email protected]>
- */
- final class Forms
- {
- /**
- * Creates a form factory with the default configuration.
- */
- public static function createFormFactory(): FormFactoryInterface
- {
- return self::createFormFactoryBuilder()->getFormFactory();
- }
- /**
- * Creates a form factory builder with the default configuration.
- */
- public static function createFormFactoryBuilder(): FormFactoryBuilderInterface
- {
- return new FormFactoryBuilder(true);
- }
- /**
- * This class cannot be instantiated.
- */
- private function __construct()
- {
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormConfigBuilderInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\PropertyAccess\PropertyPathInterface;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormConfigBuilderInterface extends FormConfigInterface
- {
- /**
- * Adds an event listener to an event on this form.
- *
- * @param int $priority The priority of the listener. Listeners
- * with a higher priority are called before
- * listeners with a lower priority.
- *
- * @return $this
- */
- public function addEventListener(string $eventName, callable $listener, int $priority = 0): static;
- /**
- * Adds an event subscriber for events on this form.
- *
- * @return $this
- */
- public function addEventSubscriber(EventSubscriberInterface $subscriber): static;
- /**
- * Appends / prepends a transformer to the view transformer chain.
- *
- * The transform method of the transformer is used to convert data from the
- * normalized to the view format.
- * The reverseTransform method of the transformer is used to convert from the
- * view to the normalized format.
- *
- * @param bool $forcePrepend If set to true, prepend instead of appending
- *
- * @return $this
- */
- public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): static;
- /**
- * Clears the view transformers.
- *
- * @return $this
- */
- public function resetViewTransformers(): static;
- /**
- * Prepends / appends a transformer to the normalization transformer chain.
- *
- * The transform method of the transformer is used to convert data from the
- * model to the normalized format.
- * The reverseTransform method of the transformer is used to convert from the
- * normalized to the model format.
- *
- * @param bool $forceAppend If set to true, append instead of prepending
- *
- * @return $this
- */
- public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): static;
- /**
- * Clears the normalization transformers.
- *
- * @return $this
- */
- public function resetModelTransformers(): static;
- /**
- * Sets the value for an attribute.
- *
- * @param mixed $value The value of the attribute
- *
- * @return $this
- */
- public function setAttribute(string $name, mixed $value): static;
- /**
- * Sets the attributes.
- *
- * @return $this
- */
- public function setAttributes(array $attributes): static;
- /**
- * Sets the data mapper used by the form.
- *
- * @return $this
- */
- public function setDataMapper(?DataMapperInterface $dataMapper): static;
- /**
- * Sets whether the form is disabled.
- *
- * @return $this
- */
- public function setDisabled(bool $disabled): static;
- /**
- * Sets the data used for the client data when no value is submitted.
- *
- * @param mixed $emptyData The empty data
- *
- * @return $this
- */
- public function setEmptyData(mixed $emptyData): static;
- /**
- * Sets whether errors bubble up to the parent.
- *
- * @return $this
- */
- public function setErrorBubbling(bool $errorBubbling): static;
- /**
- * Sets whether this field is required to be filled out when submitted.
- *
- * @return $this
- */
- public function setRequired(bool $required): static;
- /**
- * Sets the property path that the form should be mapped to.
- *
- * @param string|PropertyPathInterface|null $propertyPath The property path or null if the path should be set
- * automatically based on the form's name
- *
- * @return $this
- */
- public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): static;
- /**
- * Sets whether the form should be mapped to an element of its
- * parent's data.
- *
- * @return $this
- */
- public function setMapped(bool $mapped): static;
- /**
- * Sets whether the form's data should be modified by reference.
- *
- * @return $this
- */
- public function setByReference(bool $byReference): static;
- /**
- * Sets whether the form should read and write the data of its parent.
- *
- * @return $this
- */
- public function setInheritData(bool $inheritData): static;
- /**
- * Sets whether the form should be compound.
- *
- * @return $this
- *
- * @see FormConfigInterface::getCompound()
- */
- public function setCompound(bool $compound): static;
- /**
- * Sets the resolved type.
- *
- * @return $this
- */
- public function setType(ResolvedFormTypeInterface $type): static;
- /**
- * Sets the initial data of the form.
- *
- * @param mixed $data The data of the form in model format
- *
- * @return $this
- */
- public function setData(mixed $data): static;
- /**
- * Locks the form's data to the data passed in the configuration.
- *
- * A form with locked data is restricted to the data passed in
- * this configuration. The data can only be modified then by
- * submitting the form or using PRE_SET_DATA event.
- *
- * It means data passed to a factory method or mapped from the
- * parent will be ignored.
- *
- * @return $this
- */
- public function setDataLocked(bool $locked): static;
- /**
- * Sets the form factory used for creating new forms.
- *
- * @return $this
- */
- public function setFormFactory(FormFactoryInterface $formFactory): static;
- /**
- * Sets the target URL of the form.
- *
- * @return $this
- */
- public function setAction(string $action): static;
- /**
- * Sets the HTTP method used by the form.
- *
- * @return $this
- */
- public function setMethod(string $method): static;
- /**
- * Sets the request handler used by the form.
- *
- * @return $this
- */
- public function setRequestHandler(RequestHandlerInterface $requestHandler): static;
- /**
- * Sets whether the form should be initialized automatically.
- *
- * Should be set to true only for root forms.
- *
- * @param bool $initialize True to initialize the form automatically,
- * false to suppress automatic initialization.
- * In the second case, you need to call
- * {@link FormInterface::initialize()} manually.
- *
- * @return $this
- */
- public function setAutoInitialize(bool $initialize): static;
- /**
- * Builds and returns the form configuration.
- */
- public function getFormConfig(): FormConfigInterface;
- /**
- * Sets the callback that will be called to determine if the model
- * data of the form is empty or not.
- *
- * @return $this
- */
- public function setIsEmptyCallback(?callable $isEmptyCallback): static;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormBuilderInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * @author Bernhard Schussek <[email protected]>
- *
- * @extends \Traversable<string, FormBuilderInterface>
- */
- interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuilderInterface
- {
- /**
- * Adds a new field to this group. A field must have a unique name within
- * the group. Otherwise the existing field is overwritten.
- *
- * If you add a nested group, this group should also be represented in the
- * object hierarchy.
- *
- * @param array<string, mixed> $options
- */
- public function add(string|self $child, ?string $type = null, array $options = []): static;
- /**
- * Creates a form builder.
- *
- * @param string $name The name of the form or the name of the property
- * @param string|null $type The type of the form or null if name is a property
- * @param array<string, mixed> $options
- */
- public function create(string $name, ?string $type = null, array $options = []): self;
- /**
- * Returns a child by name.
- *
- * @throws Exception\InvalidArgumentException if the given child does not exist
- */
- public function get(string $name): self;
- /**
- * Removes the field with the given name.
- */
- public function remove(string $name): static;
- /**
- * Returns whether a field with the given name exists.
- */
- public function has(string $name): bool;
- /**
- * Returns the children.
- *
- * @return array<string, self>
- */
- public function all(): array;
- /**
- * Creates the form.
- */
- public function getForm(): FormInterface;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Command/DebugCommand.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Command;
- use Symfony\Component\Console\Attribute\AsCommand;
- use Symfony\Component\Console\Command\Command;
- use Symfony\Component\Console\Completion\CompletionInput;
- use Symfony\Component\Console\Completion\CompletionSuggestions;
- use Symfony\Component\Console\Exception\InvalidArgumentException;
- use Symfony\Component\Console\Input\InputArgument;
- use Symfony\Component\Console\Input\InputInterface;
- use Symfony\Component\Console\Input\InputOption;
- use Symfony\Component\Console\Output\OutputInterface;
- use Symfony\Component\Console\Style\SymfonyStyle;
- use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
- use Symfony\Component\Form\Console\Helper\DescriptorHelper;
- use Symfony\Component\Form\Extension\Core\CoreExtension;
- use Symfony\Component\Form\FormRegistryInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A console command for retrieving information about form types.
- *
- * @author Yonel Ceruto <[email protected]>
- */
- #[AsCommand(name: 'debug:form', description: 'Display form type information')]
- class DebugCommand extends Command
- {
- public function __construct(
- private FormRegistryInterface $formRegistry,
- private array $namespaces = ['Symfony\Component\Form\Extension\Core\Type'],
- private array $types = [],
- private array $extensions = [],
- private array $guessers = [],
- private ?FileLinkFormatter $fileLinkFormatter = null,
- ) {
- parent::__construct();
- }
- protected function configure(): void
- {
- $this
- ->setDefinition([
- new InputArgument('class', InputArgument::OPTIONAL, 'The form type class'),
- new InputArgument('option', InputArgument::OPTIONAL, 'The form type option'),
- new InputOption('show-deprecated', null, InputOption::VALUE_NONE, 'Display deprecated options in form types'),
- new InputOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'txt'),
- ])
- ->setHelp(<<<'EOF'
- The <info>%command.name%</info> command displays information about form types.
- <info>php %command.full_name%</info>
- The command lists all built-in types, services types, type extensions and
- guessers currently available.
- <info>php %command.full_name% Symfony\Component\Form\Extension\Core\Type\ChoiceType</info>
- <info>php %command.full_name% ChoiceType</info>
- The command lists all defined options that contains the given form type,
- as well as their parents and type extensions.
- <info>php %command.full_name% ChoiceType choice_value</info>
- Use the <info>--show-deprecated</info> option to display form types with
- deprecated options or the deprecated options of the given form type:
- <info>php %command.full_name% --show-deprecated</info>
- <info>php %command.full_name% ChoiceType --show-deprecated</info>
- The command displays the definition of the given option name.
- <info>php %command.full_name% --format=json</info>
- The command lists everything in a machine readable json format.
- EOF
- )
- ;
- }
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- $io = new SymfonyStyle($input, $output);
- if (null === $class = $input->getArgument('class')) {
- $object = null;
- $options['core_types'] = $this->getCoreTypes();
- $options['service_types'] = array_values(array_diff($this->types, $options['core_types']));
- if ($input->getOption('show-deprecated')) {
- $options['core_types'] = $this->filterTypesByDeprecated($options['core_types']);
- $options['service_types'] = $this->filterTypesByDeprecated($options['service_types']);
- }
- $options['extensions'] = $this->extensions;
- $options['guessers'] = $this->guessers;
- foreach ($options as $k => $list) {
- sort($options[$k]);
- }
- } else {
- if (!class_exists($class) || !is_subclass_of($class, FormTypeInterface::class)) {
- $class = $this->getFqcnTypeClass($input, $io, $class);
- }
- $resolvedType = $this->formRegistry->getType($class);
- if ($option = $input->getArgument('option')) {
- $object = $resolvedType->getOptionsResolver();
- if (!$object->isDefined($option)) {
- $message = \sprintf('Option "%s" is not defined in "%s".', $option, $resolvedType->getInnerType()::class);
- if ($alternatives = $this->findAlternatives($option, $object->getDefinedOptions())) {
- if (1 === \count($alternatives)) {
- $message .= "\n\nDid you mean this?\n ";
- } else {
- $message .= "\n\nDid you mean one of these?\n ";
- }
- $message .= implode("\n ", $alternatives);
- }
- throw new InvalidArgumentException($message);
- }
- $options['type'] = $resolvedType->getInnerType();
- $options['option'] = $option;
- } else {
- $object = $resolvedType;
- }
- }
- $helper = new DescriptorHelper($this->fileLinkFormatter);
- $options['format'] = $input->getOption('format');
- $options['show_deprecated'] = $input->getOption('show-deprecated');
- $helper->describe($io, $object, $options);
- return 0;
- }
- private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, string $shortClassName): string
- {
- $classes = $this->getFqcnTypeClasses($shortClassName);
- if (0 === $count = \count($classes)) {
- $message = \sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces));
- $allTypes = array_merge($this->getCoreTypes(), $this->types);
- if ($alternatives = $this->findAlternatives($shortClassName, $allTypes)) {
- if (1 === \count($alternatives)) {
- $message .= "\n\nDid you mean this?\n ";
- } else {
- $message .= "\n\nDid you mean one of these?\n ";
- }
- $message .= implode("\n ", $alternatives);
- }
- throw new InvalidArgumentException($message);
- }
- if (1 === $count) {
- return $classes[0];
- }
- if (!$input->isInteractive()) {
- throw new InvalidArgumentException(\sprintf("The type \"%s\" is ambiguous.\n\nDid you mean one of these?\n %s.", $shortClassName, implode("\n ", $classes)));
- }
- 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]);
- }
- private function getFqcnTypeClasses(string $shortClassName): array
- {
- $classes = [];
- sort($this->namespaces);
- foreach ($this->namespaces as $namespace) {
- if (class_exists($fqcn = $namespace.'\\'.$shortClassName)) {
- $classes[] = $fqcn;
- } elseif (class_exists($fqcn = $namespace.'\\'.ucfirst($shortClassName))) {
- $classes[] = $fqcn;
- } elseif (class_exists($fqcn = $namespace.'\\'.ucfirst($shortClassName).'Type')) {
- $classes[] = $fqcn;
- } elseif (str_ends_with($shortClassName, 'type') && class_exists($fqcn = $namespace.'\\'.ucfirst(substr($shortClassName, 0, -4).'Type'))) {
- $classes[] = $fqcn;
- }
- }
- return $classes;
- }
- private function getCoreTypes(): array
- {
- $coreExtension = new CoreExtension();
- $loadTypesRefMethod = (new \ReflectionObject($coreExtension))->getMethod('loadTypes');
- $coreTypes = $loadTypesRefMethod->invoke($coreExtension);
- $coreTypes = array_map(static fn (FormTypeInterface $type) => $type::class, $coreTypes);
- sort($coreTypes);
- return $coreTypes;
- }
- private function filterTypesByDeprecated(array $types): array
- {
- $typesWithDeprecatedOptions = [];
- foreach ($types as $class) {
- $optionsResolver = $this->formRegistry->getType($class)->getOptionsResolver();
- foreach ($optionsResolver->getDefinedOptions() as $option) {
- if ($optionsResolver->isDeprecated($option)) {
- $typesWithDeprecatedOptions[] = $class;
- break;
- }
- }
- }
- return $typesWithDeprecatedOptions;
- }
- private function findAlternatives(string $name, array $collection): array
- {
- $alternatives = [];
- foreach ($collection as $item) {
- $lev = levenshtein($name, $item);
- if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) {
- $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
- }
- }
- $threshold = 1e3;
- $alternatives = array_filter($alternatives, static fn ($lev) => $lev < 2 * $threshold);
- ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);
- return array_keys($alternatives);
- }
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- if ($input->mustSuggestArgumentValuesFor('class')) {
- $suggestions->suggestValues(array_merge($this->getCoreTypes(), $this->types));
- return;
- }
- if ($input->mustSuggestArgumentValuesFor('option') && null !== $class = $input->getArgument('class')) {
- $this->completeOptions($class, $suggestions);
- return;
- }
- if ($input->mustSuggestOptionValuesFor('format')) {
- $suggestions->suggestValues($this->getAvailableFormatOptions());
- }
- }
- private function completeOptions(string $class, CompletionSuggestions $suggestions): void
- {
- if (!class_exists($class) || !is_subclass_of($class, FormTypeInterface::class)) {
- $classes = $this->getFqcnTypeClasses($class);
- if (1 === \count($classes)) {
- $class = $classes[0];
- }
- }
- if (!$this->formRegistry->hasType($class)) {
- return;
- }
- $resolvedType = $this->formRegistry->getType($class);
- $suggestions->suggestValues($resolvedType->getOptionsResolver()->getDefinedOptions());
- }
- /** @return string[] */
- private function getAvailableFormatOptions(): array
- {
- return (new DescriptorHelper())->getFormats();
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormTypeExtensionInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormTypeExtensionInterface
- {
- /**
- * Gets the extended types.
- *
- * @return string[]
- */
- public static function getExtendedTypes(): iterable;
- public function configureOptions(OptionsResolver $resolver): void;
- /**
- * Builds the form.
- *
- * This method is called after the extended type has built the form to
- * further modify it.
- *
- * @param array<string, mixed> $options
- *
- * @see FormTypeInterface::buildForm()
- */
- public function buildForm(FormBuilderInterface $builder, array $options): void;
- /**
- * Builds the view.
- *
- * This method is called after the extended type has built the view to
- * further modify it.
- *
- * @param array<string, mixed> $options
- *
- * @see FormTypeInterface::buildView()
- */
- public function buildView(FormView $view, FormInterface $form, array $options): void;
- /**
- * Finishes the view.
- *
- * This method is called after the extended type has finished the view to
- * further modify it.
- *
- * @param array<string, mixed> $options
- *
- * @see FormTypeInterface::finishView()
- */
- public function finishView(FormView $view, FormInterface $form, array $options): void;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Form.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Event\PostSetDataEvent;
- use Symfony\Component\Form\Event\PostSubmitEvent;
- use Symfony\Component\Form\Event\PreSetDataEvent;
- use Symfony\Component\Form\Event\PreSubmitEvent;
- use Symfony\Component\Form\Event\SubmitEvent;
- use Symfony\Component\Form\Exception\AlreadySubmittedException;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Exception\OutOfBoundsException;
- use Symfony\Component\Form\Exception\RuntimeException;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- use Symfony\Component\Form\Extension\Core\Type\TextType;
- use Symfony\Component\Form\Util\FormUtil;
- use Symfony\Component\Form\Util\InheritDataAwareIterator;
- use Symfony\Component\Form\Util\OrderedHashMap;
- use Symfony\Component\PropertyAccess\PropertyPath;
- use Symfony\Component\PropertyAccess\PropertyPathInterface;
- /**
- * Form represents a form.
- *
- * To implement your own form fields, you need to have a thorough understanding
- * of the data flow within a form. A form stores its data in three different
- * representations:
- *
- * (1) the "model" format required by the form's object
- * (2) the "normalized" format for internal processing
- * (3) the "view" format used for display simple fields
- * or map children model data for compound fields
- *
- * A date field, for example, may store a date as "Y-m-d" string (1) in the
- * object. To facilitate processing in the field, this value is normalized
- * to a DateTime object (2). In the HTML representation of your form, a
- * localized string (3) may be presented to and modified by the user, or it could be an array of values
- * to be mapped to choices fields.
- *
- * In most cases, format (1) and format (2) will be the same. For example,
- * a checkbox field uses a Boolean value for both internal processing and
- * storage in the object. In these cases you need to set a view transformer
- * to convert between formats (2) and (3). You can do this by calling
- * addViewTransformer().
- *
- * In some cases though it makes sense to make format (1) configurable. To
- * demonstrate this, let's extend our above date field to store the value
- * either as "Y-m-d" string or as timestamp. Internally we still want to
- * use a DateTime object for processing. To convert the data from string/integer
- * to DateTime you can set a model transformer by calling
- * addModelTransformer(). The normalized data is then converted to the displayed
- * data as described before.
- *
- * The conversions (1) -> (2) -> (3) use the transform methods of the transformers.
- * The conversions (3) -> (2) -> (1) use the reverseTransform methods of the transformers.
- *
- * @author Fabien Potencier <[email protected]>
- * @author Bernhard Schussek <[email protected]>
- *
- * @implements \IteratorAggregate<string, FormInterface>
- */
- class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterface
- {
- private ?FormInterface $parent = null;
- /**
- * A map of FormInterface instances.
- *
- * @var OrderedHashMap<FormInterface>
- */
- private OrderedHashMap $children;
- /**
- * @var FormError[]
- */
- private array $errors = [];
- private bool $submitted = false;
- /**
- * The button that was used to submit the form.
- */
- private FormInterface|ClickableInterface|null $clickedButton = null;
- private mixed $modelData = null;
- private mixed $normData = null;
- private mixed $viewData = null;
- /**
- * The submitted values that don't belong to any children.
- */
- private array $extraData = [];
- /**
- * The transformation failure generated during submission, if any.
- */
- private ?TransformationFailedException $transformationFailure = null;
- /**
- * Whether the form's data has been initialized.
- *
- * When the data is initialized with its default value, that default value
- * is passed through the transformer chain in order to synchronize the
- * model, normalized and view format for the first time. This is done
- * lazily in order to save performance when {@link setData()} is called
- * manually, making the initialization with the configured default value
- * superfluous.
- */
- private bool $defaultDataSet = false;
- /**
- * Whether setData() is currently being called.
- */
- private bool $lockSetData = false;
- private string $name = '';
- /**
- * Whether the form inherits its underlying data from its parent.
- */
- private bool $inheritData;
- private ?PropertyPathInterface $propertyPath = null;
- /**
- * @throws LogicException if a data mapper is not provided for a compound form
- */
- public function __construct(
- private FormConfigInterface $config,
- ) {
- // Compound forms always need a data mapper, otherwise calls to
- // `setData` and `add` will not lead to the correct population of
- // the child forms.
- if ($config->getCompound() && !$config->getDataMapper()) {
- throw new LogicException('Compound forms need a data mapper.');
- }
- // If the form inherits the data from its parent, it is not necessary
- // to call setData() with the default data.
- if ($this->inheritData = $config->getInheritData()) {
- $this->defaultDataSet = true;
- }
- $this->children = new OrderedHashMap();
- $this->name = $config->getName();
- }
- public function __clone()
- {
- $this->children = clone $this->children;
- foreach ($this->children as $key => $child) {
- $this->children[$key] = clone $child;
- }
- }
- public function getConfig(): FormConfigInterface
- {
- return $this->config;
- }
- public function getName(): string
- {
- return $this->name;
- }
- public function getPropertyPath(): ?PropertyPathInterface
- {
- if ($this->propertyPath || $this->propertyPath = $this->config->getPropertyPath()) {
- return $this->propertyPath;
- }
- if ('' === $this->name) {
- return null;
- }
- $parent = $this->parent;
- while ($parent?->getConfig()->getInheritData()) {
- $parent = $parent->getParent();
- }
- if ($parent && null === $parent->getConfig()->getDataClass()) {
- $this->propertyPath = new PropertyPath('['.$this->name.']');
- } else {
- $this->propertyPath = new PropertyPath($this->name);
- }
- return $this->propertyPath;
- }
- public function isRequired(): bool
- {
- if (null === $this->parent || $this->parent->isRequired()) {
- return $this->config->getRequired();
- }
- return false;
- }
- public function isDisabled(): bool
- {
- if (null === $this->parent || !$this->parent->isDisabled()) {
- return $this->config->getDisabled();
- }
- return true;
- }
- public function setParent(?FormInterface $parent): static
- {
- if ($this->submitted) {
- throw new AlreadySubmittedException('You cannot set the parent of a submitted form.');
- }
- if (null !== $parent && '' === $this->name) {
- throw new LogicException('A form with an empty name cannot have a parent form.');
- }
- $this->parent = $parent;
- return $this;
- }
- public function getParent(): ?FormInterface
- {
- return $this->parent;
- }
- public function getRoot(): FormInterface
- {
- return $this->parent ? $this->parent->getRoot() : $this;
- }
- public function isRoot(): bool
- {
- return null === $this->parent;
- }
- public function setData(mixed $modelData): static
- {
- // If the form is submitted while disabled, it is set to submitted, but the data is not
- // changed. In such cases (i.e. when the form is not initialized yet) don't
- // abort this method.
- if ($this->submitted && $this->defaultDataSet) {
- throw new AlreadySubmittedException('You cannot change the data of a submitted form.');
- }
- // If the form inherits its parent's data, disallow data setting to
- // prevent merge conflicts
- if ($this->inheritData) {
- throw new RuntimeException('You cannot change the data of a form inheriting its parent data.');
- }
- // Don't allow modifications of the configured data if the data is locked
- if ($this->config->getDataLocked() && $modelData !== $this->config->getData()) {
- return $this;
- }
- if (\is_object($modelData) && !$this->config->getByReference()) {
- $modelData = clone $modelData;
- }
- if ($this->lockSetData) {
- 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.');
- }
- $this->lockSetData = true;
- $dispatcher = $this->config->getEventDispatcher();
- // Hook to change content of the model data before transformation and mapping children
- if ($dispatcher->hasListeners(FormEvents::PRE_SET_DATA)) {
- $event = new PreSetDataEvent($this, $modelData);
- $dispatcher->dispatch($event, FormEvents::PRE_SET_DATA);
- $modelData = $event->getData();
- }
- // Treat data as strings unless a transformer exists
- if (\is_scalar($modelData) && !$this->config->getViewTransformers() && !$this->config->getModelTransformers()) {
- $modelData = (string) $modelData;
- }
- // Synchronize representations - must not change the content!
- // Transformation exceptions are not caught on initialization
- $normData = $this->modelToNorm($modelData);
- $viewData = $this->normToView($normData);
- // Validate if view data matches data class (unless empty)
- if (!FormUtil::isEmpty($viewData)) {
- $dataClass = $this->config->getDataClass();
- if (null !== $dataClass && !$viewData instanceof $dataClass) {
- $actualType = get_debug_type($viewData);
- 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.'".');
- }
- }
- $this->modelData = $modelData;
- $this->normData = $normData;
- $this->viewData = $viewData;
- $this->defaultDataSet = true;
- $this->lockSetData = false;
- // Compound forms don't need to invoke this method if they don't have children
- if (\count($this->children) > 0) {
- // Update child forms from the data (unless their config data is locked)
- $this->config->getDataMapper()->mapDataToForms($viewData, new \RecursiveIteratorIterator(new InheritDataAwareIterator($this->children)));
- }
- if ($dispatcher->hasListeners(FormEvents::POST_SET_DATA)) {
- $event = new PostSetDataEvent($this, $modelData);
- $dispatcher->dispatch($event, FormEvents::POST_SET_DATA);
- }
- return $this;
- }
- public function getData(): mixed
- {
- if ($this->inheritData) {
- if (!$this->parent) {
- throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
- }
- return $this->parent->getData();
- }
- if (!$this->defaultDataSet) {
- if ($this->lockSetData) {
- 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.');
- }
- $this->setData($this->config->getData());
- }
- return $this->modelData;
- }
- public function getNormData(): mixed
- {
- if ($this->inheritData) {
- if (!$this->parent) {
- throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
- }
- return $this->parent->getNormData();
- }
- if (!$this->defaultDataSet) {
- if ($this->lockSetData) {
- 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.');
- }
- $this->setData($this->config->getData());
- }
- return $this->normData;
- }
- public function getViewData(): mixed
- {
- if ($this->inheritData) {
- if (!$this->parent) {
- throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
- }
- return $this->parent->getViewData();
- }
- if (!$this->defaultDataSet) {
- if ($this->lockSetData) {
- 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.');
- }
- $this->setData($this->config->getData());
- }
- return $this->viewData;
- }
- public function getExtraData(): array
- {
- return $this->extraData;
- }
- public function initialize(): static
- {
- if (null !== $this->parent) {
- throw new RuntimeException('Only root forms should be initialized.');
- }
- // Guarantee that the *_SET_DATA events have been triggered once the
- // form is initialized. This makes sure that dynamically added or
- // removed fields are already visible after initialization.
- if (!$this->defaultDataSet) {
- $this->setData($this->config->getData());
- }
- return $this;
- }
- public function handleRequest(mixed $request = null): static
- {
- $this->config->getRequestHandler()->handleRequest($this, $request);
- return $this;
- }
- public function submit(mixed $submittedData, bool $clearMissing = true): static
- {
- if ($this->submitted) {
- throw new AlreadySubmittedException('A form can only be submitted once.');
- }
- // Initialize errors in the very beginning so we're sure
- // they are collectable during submission only
- $this->errors = [];
- // Obviously, a disabled form should not change its data upon submission.
- if ($this->isDisabled()) {
- $this->submitted = true;
- return $this;
- }
- // The data must be initialized if it was not initialized yet.
- // This is necessary to guarantee that the *_SET_DATA listeners
- // are always invoked before submit() takes place.
- if (!$this->defaultDataSet) {
- $this->setData($this->config->getData());
- }
- // Treat false as NULL to support binding false to checkboxes.
- // Don't convert NULL to a string here in order to determine later
- // whether an empty value has been submitted or whether no value has
- // been submitted at all. This is important for processing checkboxes
- // and radio buttons with empty values.
- if (false === $submittedData) {
- $submittedData = null;
- } elseif (\is_scalar($submittedData)) {
- $submittedData = (string) $submittedData;
- } elseif ($this->config->getRequestHandler()->isFileUpload($submittedData)) {
- if (!$this->config->getOption('allow_file_upload')) {
- $submittedData = null;
- $this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, file upload given.');
- }
- } elseif (\is_array($submittedData) && !$this->config->getCompound() && !$this->config->getOption('multiple', false)) {
- $submittedData = null;
- $this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, array given.');
- }
- $dispatcher = $this->config->getEventDispatcher();
- $modelData = null;
- $normData = null;
- $viewData = null;
- try {
- if (null !== $this->transformationFailure) {
- throw $this->transformationFailure;
- }
- // Hook to change content of the data submitted by the browser
- if ($dispatcher->hasListeners(FormEvents::PRE_SUBMIT)) {
- $event = new PreSubmitEvent($this, $submittedData);
- $dispatcher->dispatch($event, FormEvents::PRE_SUBMIT);
- $submittedData = $event->getData();
- }
- // Check whether the form is compound.
- // This check is preferable over checking the number of children,
- // since forms without children may also be compound.
- // (think of empty collection forms)
- if ($this->config->getCompound()) {
- if (!\is_array($submittedData ??= [])) {
- throw new TransformationFailedException('Compound forms expect an array or NULL on submission.');
- }
- foreach ($this->children as $name => $child) {
- $isSubmitted = \array_key_exists($name, $submittedData);
- if ($isSubmitted || $clearMissing) {
- $child->submit($isSubmitted ? $submittedData[$name] : null, $clearMissing);
- unset($submittedData[$name]);
- if (null !== $this->clickedButton) {
- continue;
- }
- if ($child instanceof ClickableInterface && $child->isClicked()) {
- $this->clickedButton = $child;
- continue;
- }
- if (method_exists($child, 'getClickedButton') && null !== $child->getClickedButton()) {
- $this->clickedButton = $child->getClickedButton();
- }
- }
- }
- $this->extraData = $submittedData;
- }
- // Forms that inherit their parents' data also are not processed,
- // because then it would be too difficult to merge the changes in
- // the child and the parent form. Instead, the parent form also takes
- // changes in the grandchildren (i.e. children of the form that inherits
- // its parent's data) into account.
- // (see InheritDataAwareIterator below)
- if (!$this->inheritData) {
- // If the form is compound, the view data is merged with the data
- // of the children using the data mapper.
- // If the form is not compound, the view data is assigned to the submitted data.
- $viewData = $this->config->getCompound() ? $this->viewData : $submittedData;
- if (FormUtil::isEmpty($viewData)) {
- $emptyData = $this->config->getEmptyData();
- if ($emptyData instanceof \Closure) {
- $emptyData = $emptyData($this, $viewData);
- }
- $viewData = $emptyData;
- }
- // Merge form data from children into existing view data
- // It is not necessary to invoke this method if the form has no children,
- // even if it is compound.
- if (\count($this->children) > 0) {
- // Use InheritDataAwareIterator to process children of
- // descendants that inherit this form's data.
- // These descendants will not be submitted normally (see the check
- // for $this->config->getInheritData() above)
- $this->config->getDataMapper()->mapFormsToData(
- new \RecursiveIteratorIterator(new InheritDataAwareIterator($this->children)),
- $viewData
- );
- }
- // Normalize data to unified representation
- $normData = $this->viewToNorm($viewData);
- // Hook to change content of the data in the normalized
- // representation
- if ($dispatcher->hasListeners(FormEvents::SUBMIT)) {
- $event = new SubmitEvent($this, $normData);
- $dispatcher->dispatch($event, FormEvents::SUBMIT);
- $normData = $event->getData();
- }
- // Synchronize representations - must not change the content!
- $modelData = $this->normToModel($normData);
- $viewData = $this->normToView($normData);
- }
- } catch (TransformationFailedException $e) {
- $this->transformationFailure = $e;
- // If $viewData was not yet set, set it to $submittedData so that
- // the erroneous data is accessible on the form.
- // Forms that inherit data never set any data, because the getters
- // forward to the parent form's getters anyway.
- if (null === $viewData && !$this->inheritData) {
- $viewData = $submittedData;
- }
- }
- $this->submitted = true;
- $this->modelData = $modelData;
- $this->normData = $normData;
- $this->viewData = $viewData;
- if ($dispatcher->hasListeners(FormEvents::POST_SUBMIT)) {
- $event = new PostSubmitEvent($this, $viewData);
- $dispatcher->dispatch($event, FormEvents::POST_SUBMIT);
- }
- return $this;
- }
- public function addError(FormError $error): static
- {
- if (null === $error->getOrigin()) {
- $error->setOrigin($this);
- }
- if ($this->parent && $this->config->getErrorBubbling()) {
- $this->parent->addError($error);
- } else {
- $this->errors[] = $error;
- }
- return $this;
- }
- public function isSubmitted(): bool
- {
- return $this->submitted;
- }
- public function isSynchronized(): bool
- {
- return null === $this->transformationFailure;
- }
- public function getTransformationFailure(): ?TransformationFailedException
- {
- return $this->transformationFailure;
- }
- public function isEmpty(): bool
- {
- foreach ($this->children as $child) {
- if (!$child->isEmpty()) {
- return false;
- }
- }
- if (null !== $isEmptyCallback = $this->config->getIsEmptyCallback()) {
- return $isEmptyCallback($this->modelData);
- }
- return FormUtil::isEmpty($this->modelData)
- // arrays, countables
- || (is_countable($this->modelData) && 0 === \count($this->modelData))
- // traversables that are not countable
- || ($this->modelData instanceof \Traversable && 0 === iterator_count($this->modelData));
- }
- public function isValid(): bool
- {
- if (!$this->submitted) {
- throw new LogicException('Cannot check if an unsubmitted form is valid. Call Form::isSubmitted() and ensure that it\'s true before calling Form::isValid().');
- }
- if ($this->isDisabled()) {
- return true;
- }
- return 0 === \count($this->getErrors(true));
- }
- /**
- * Returns the button that was used to submit the form.
- */
- public function getClickedButton(): FormInterface|ClickableInterface|null
- {
- if ($this->clickedButton) {
- return $this->clickedButton;
- }
- return $this->parent && method_exists($this->parent, 'getClickedButton') ? $this->parent->getClickedButton() : null;
- }
- public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator
- {
- $errors = $this->errors;
- // Copy the errors of nested forms to the $errors array
- if ($deep) {
- foreach ($this as $child) {
- /** @var FormInterface $child */
- if ($child->isSubmitted() && $child->isValid()) {
- continue;
- }
- $iterator = $child->getErrors(true, $flatten);
- if (0 === \count($iterator)) {
- continue;
- }
- if ($flatten) {
- foreach ($iterator as $error) {
- $errors[] = $error;
- }
- } else {
- $errors[] = $iterator;
- }
- }
- }
- return new FormErrorIterator($this, $errors);
- }
- public function clearErrors(bool $deep = false): static
- {
- $this->errors = [];
- if ($deep) {
- // Clear errors from children
- foreach ($this as $child) {
- if ($child instanceof ClearableErrorsInterface) {
- $child->clearErrors(true);
- }
- }
- }
- return $this;
- }
- public function all(): array
- {
- return iterator_to_array($this->children);
- }
- public function add(FormInterface|string $child, ?string $type = null, array $options = []): static
- {
- if ($this->submitted) {
- throw new AlreadySubmittedException('You cannot add children to a submitted form.');
- }
- if (!$this->config->getCompound()) {
- throw new LogicException('You cannot add children to a simple form. Maybe you should set the option "compound" to true?');
- }
- if (!$child instanceof FormInterface) {
- // Never initialize child forms automatically
- $options['auto_initialize'] = false;
- if (null === $type && null === $this->config->getDataClass()) {
- $type = TextType::class;
- }
- if (null === $type) {
- $child = $this->config->getFormFactory()->createForProperty($this->config->getDataClass(), $child, null, $options);
- } else {
- $child = $this->config->getFormFactory()->createNamed($child, $type, null, $options);
- }
- } elseif ($child->getConfig()->getAutoInitialize()) {
- 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()));
- }
- $this->children[$child->getName()] = $child;
- $child->setParent($this);
- // If setData() is currently being called, there is no need to call
- // mapDataToForms() here, as mapDataToForms() is called at the end
- // of setData() anyway. Not doing this check leads to an endless
- // recursion when initializing the form lazily and an event listener
- // (such as ResizeFormListener) adds fields depending on the data:
- //
- // * setData() is called, the form is not initialized yet
- // * add() is called by the listener (setData() is not complete, so
- // the form is still not initialized)
- // * getViewData() is called
- // * setData() is called since the form is not initialized yet
- // * ... endless recursion ...
- //
- // Also skip data mapping if setData() has not been called yet.
- // setData() will be called upon form initialization and data mapping
- // will take place by then.
- if (!$this->lockSetData && $this->defaultDataSet && !$this->inheritData) {
- $viewData = $this->getViewData();
- $this->config->getDataMapper()->mapDataToForms(
- $viewData,
- new \RecursiveIteratorIterator(new InheritDataAwareIterator(new \ArrayIterator([$child->getName() => $child])))
- );
- }
- return $this;
- }
- public function remove(string $name): static
- {
- if ($this->submitted) {
- throw new AlreadySubmittedException('You cannot remove children from a submitted form.');
- }
- if (isset($this->children[$name])) {
- if (!$this->children[$name]->isSubmitted()) {
- $this->children[$name]->setParent(null);
- }
- unset($this->children[$name]);
- }
- return $this;
- }
- public function has(string $name): bool
- {
- return isset($this->children[$name]);
- }
- public function get(string $name): FormInterface
- {
- if (isset($this->children[$name])) {
- return $this->children[$name];
- }
- throw new OutOfBoundsException(\sprintf('Child "%s" does not exist.', $name));
- }
- /**
- * Returns whether a child with the given name exists (implements the \ArrayAccess interface).
- *
- * @param string $name The name of the child
- */
- public function offsetExists(mixed $name): bool
- {
- return $this->has($name);
- }
- /**
- * Returns the child with the given name (implements the \ArrayAccess interface).
- *
- * @param string $name The name of the child
- *
- * @throws OutOfBoundsException if the named child does not exist
- */
- public function offsetGet(mixed $name): FormInterface
- {
- return $this->get($name);
- }
- /**
- * Adds a child to the form (implements the \ArrayAccess interface).
- *
- * @param string $name Ignored. The name of the child is used
- * @param FormInterface $child The child to be added
- *
- * @throws AlreadySubmittedException if the form has already been submitted
- * @throws LogicException when trying to add a child to a non-compound form
- *
- * @see self::add()
- */
- public function offsetSet(mixed $name, mixed $child): void
- {
- $this->add($child);
- }
- /**
- * Removes the child with the given name from the form (implements the \ArrayAccess interface).
- *
- * @param string $name The name of the child to remove
- *
- * @throws AlreadySubmittedException if the form has already been submitted
- */
- public function offsetUnset(mixed $name): void
- {
- $this->remove($name);
- }
- /**
- * Returns the iterator for this group.
- *
- * @return \Traversable<string, FormInterface>
- */
- public function getIterator(): \Traversable
- {
- return $this->children;
- }
- /**
- * Returns the number of form children (implements the \Countable interface).
- */
- public function count(): int
- {
- return \count($this->children);
- }
- public function createView(?FormView $parent = null): FormView
- {
- if (null === $parent && $this->parent) {
- $parent = $this->parent->createView();
- }
- $type = $this->config->getType();
- $options = $this->config->getOptions();
- // The methods createView(), buildView() and finishView() are called
- // explicitly here in order to be able to override either of them
- // in a custom resolved form type.
- $view = $type->createView($this, $parent);
- $type->buildView($view, $this, $options);
- foreach ($this->children as $name => $child) {
- $view->children[$name] = $child->createView($view);
- }
- $this->sort($view->children);
- $type->finishView($view, $this, $options);
- return $view;
- }
- /**
- * Sorts view fields based on their priority value.
- */
- private function sort(array &$children): void
- {
- $c = [];
- $i = 0;
- $needsSorting = false;
- foreach ($children as $name => $child) {
- $c[$name] = ['p' => $child->vars['priority'] ?? 0, 'i' => $i++];
- if (0 !== $c[$name]['p']) {
- $needsSorting = true;
- }
- }
- if (!$needsSorting) {
- return;
- }
- uksort($children, static fn ($a, $b): int => [$c[$b]['p'], $c[$a]['i']] <=> [$c[$a]['p'], $c[$b]['i']]);
- }
- /**
- * Normalizes the underlying data if a model transformer is set.
- *
- * @throws TransformationFailedException If the underlying data cannot be transformed to "normalized" format
- */
- private function modelToNorm(mixed $value): mixed
- {
- try {
- foreach ($this->config->getModelTransformers() as $transformer) {
- $value = $transformer->transform($value);
- }
- } catch (TransformationFailedException $exception) {
- throw new TransformationFailedException(\sprintf('Unable to transform data for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
- }
- return $value;
- }
- /**
- * Reverse transforms a value if a model transformer is set.
- *
- * @throws TransformationFailedException If the value cannot be transformed to "model" format
- */
- private function normToModel(mixed $value): mixed
- {
- try {
- $transformers = $this->config->getModelTransformers();
- for ($i = \count($transformers) - 1; $i >= 0; --$i) {
- $value = $transformers[$i]->reverseTransform($value);
- }
- } catch (TransformationFailedException $exception) {
- throw new TransformationFailedException(\sprintf('Unable to reverse value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
- }
- return $value;
- }
- /**
- * Transforms the value if a view transformer is set.
- *
- * @throws TransformationFailedException If the normalized value cannot be transformed to "view" format
- */
- private function normToView(mixed $value): mixed
- {
- // Scalar values should be converted to strings to
- // facilitate differentiation between empty ("") and zero (0).
- // Only do this for simple forms, as the resulting value in
- // compound forms is passed to the data mapper and thus should
- // not be converted to a string before.
- if (!($transformers = $this->config->getViewTransformers()) && !$this->config->getCompound()) {
- return null === $value || \is_scalar($value) ? (string) $value : $value;
- }
- try {
- foreach ($transformers as $transformer) {
- $value = $transformer->transform($value);
- }
- } catch (TransformationFailedException $exception) {
- throw new TransformationFailedException(\sprintf('Unable to transform value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
- }
- return $value;
- }
- /**
- * Reverse transforms a value if a view transformer is set.
- *
- * @throws TransformationFailedException If the submitted value cannot be transformed to "normalized" format
- */
- private function viewToNorm(mixed $value): mixed
- {
- if (!$transformers = $this->config->getViewTransformers()) {
- return '' === $value ? null : $value;
- }
- try {
- for ($i = \count($transformers) - 1; $i >= 0; --$i) {
- $value = $transformers[$i]->reverseTransform($value);
- }
- } catch (TransformationFailedException $exception) {
- throw new TransformationFailedException(\sprintf('Unable to reverse value for property path "%s": ', $this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception, $exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
- }
- return $value;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\PropertyAccess\PropertyPathInterface;
- /**
- * A form group bundling multiple forms in a hierarchical structure.
- *
- * @author Bernhard Schussek <[email protected]>
- *
- * @extends \ArrayAccess<string, FormInterface>
- * @extends \Traversable<string, FormInterface>
- */
- interface FormInterface extends \ArrayAccess, \Traversable, \Countable
- {
- /**
- * Sets the parent form.
- *
- * @param FormInterface|null $parent The parent form or null if it's the root
- *
- * @return $this
- *
- * @throws Exception\AlreadySubmittedException if the form has already been submitted
- * @throws Exception\LogicException when trying to set a parent for a form with
- * an empty name
- */
- public function setParent(?self $parent): static;
- /**
- * Returns the parent form.
- */
- public function getParent(): ?self;
- /**
- * Adds or replaces a child to the form.
- *
- * @param FormInterface|string $child The FormInterface instance or the name of the child
- * @param string|null $type The child's type, if a name was passed
- * @param array $options The child's options, if a name was passed
- *
- * @return $this
- *
- * @throws Exception\AlreadySubmittedException if the form has already been submitted
- * @throws Exception\LogicException when trying to add a child to a non-compound form
- * @throws Exception\UnexpectedTypeException if $child or $type has an unexpected type
- */
- public function add(self|string $child, ?string $type = null, array $options = []): static;
- /**
- * Returns the child with the given name.
- *
- * @throws Exception\OutOfBoundsException if the named child does not exist
- */
- public function get(string $name): self;
- /**
- * Returns whether a child with the given name exists.
- */
- public function has(string $name): bool;
- /**
- * Removes a child from the form.
- *
- * @return $this
- *
- * @throws Exception\AlreadySubmittedException if the form has already been submitted
- */
- public function remove(string $name): static;
- /**
- * Returns all children in this group.
- *
- * @return self[]
- */
- public function all(): array;
- /**
- * Returns the errors of this form.
- *
- * @param bool $deep Whether to include errors of child forms as well
- * @param bool $flatten Whether to flatten the list of errors in case
- * $deep is set to true
- */
- public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator;
- /**
- * Updates the form with default model data.
- *
- * @param mixed $modelData The data formatted as expected for the underlying object
- *
- * @return $this
- *
- * @throws Exception\AlreadySubmittedException If the form has already been submitted
- * @throws Exception\LogicException if the view data does not match the expected type
- * according to {@link FormConfigInterface::getDataClass}
- * @throws Exception\RuntimeException If listeners try to call setData in a cycle or if
- * the form inherits data from its parent
- * @throws Exception\TransformationFailedException if the synchronization failed
- */
- public function setData(mixed $modelData): static;
- /**
- * Returns the model data in the format needed for the underlying object.
- *
- * @return mixed When the field is not submitted, the default data is returned.
- * When the field is submitted, the default data has been bound
- * to the submitted view data.
- *
- * @throws Exception\RuntimeException If the form inherits data but has no parent
- */
- public function getData(): mixed;
- /**
- * Returns the normalized data of the field, used as internal bridge
- * between model data and view data.
- *
- * @return mixed When the field is not submitted, the default data is returned.
- * When the field is submitted, the normalized submitted data
- * is returned if the field is synchronized with the view data,
- * null otherwise.
- *
- * @throws Exception\RuntimeException If the form inherits data but has no parent
- */
- public function getNormData(): mixed;
- /**
- * Returns the view data of the field.
- *
- * It may be defined by {@link FormConfigInterface::getDataClass}.
- *
- * There are two cases:
- *
- * - When the form is compound the view data is mapped to the children.
- * Each child will use its mapped data as model data.
- * It can be an array, an object or null.
- *
- * - When the form is simple its view data is used to be bound
- * to the submitted data.
- * It can be a string or an array.
- *
- * In both cases the view data is the actual altered data on submission.
- *
- * @throws Exception\RuntimeException If the form inherits data but has no parent
- */
- public function getViewData(): mixed;
- /**
- * Returns the extra submitted data.
- *
- * @return array The submitted data which do not belong to a child
- */
- public function getExtraData(): array;
- /**
- * Returns the form's configuration.
- */
- public function getConfig(): FormConfigInterface;
- /**
- * Returns whether the form is submitted.
- */
- public function isSubmitted(): bool;
- /**
- * Returns the name by which the form is identified in forms.
- *
- * Only root forms are allowed to have an empty name.
- */
- public function getName(): string;
- /**
- * Returns the property path that the form is mapped to.
- */
- public function getPropertyPath(): ?PropertyPathInterface;
- /**
- * Adds an error to this form.
- *
- * @return $this
- */
- public function addError(FormError $error): static;
- /**
- * Returns whether the form and all children are valid.
- *
- * @throws Exception\LogicException if the form is not submitted
- */
- public function isValid(): bool;
- /**
- * Returns whether the form is required to be filled out.
- *
- * If the form has a parent and the parent is not required, this method
- * will always return false. Otherwise the value set with setRequired()
- * is returned.
- */
- public function isRequired(): bool;
- /**
- * Returns whether this form is disabled.
- *
- * The content of a disabled form is displayed, but not allowed to be
- * modified. The validation of modified disabled forms should fail.
- *
- * Forms whose parents are disabled are considered disabled regardless of
- * their own state.
- */
- public function isDisabled(): bool;
- /**
- * Returns whether the form is empty.
- */
- public function isEmpty(): bool;
- /**
- * Returns whether the data in the different formats is synchronized.
- *
- * If the data is not synchronized, you can get the transformation failure
- * by calling {@link getTransformationFailure()}.
- *
- * If the form is not submitted, this method always returns true.
- */
- public function isSynchronized(): bool;
- /**
- * Returns the data transformation failure, if any, during submission.
- */
- public function getTransformationFailure(): ?Exception\TransformationFailedException;
- /**
- * Initializes the form tree.
- *
- * Should be called on the root form after constructing the tree.
- *
- * @return $this
- *
- * @throws Exception\RuntimeException If the form is not the root
- */
- public function initialize(): static;
- /**
- * Inspects the given request and calls {@link submit()} if the form was
- * submitted.
- *
- * Internally, the request is forwarded to the configured
- * {@link RequestHandlerInterface} instance, which determines whether to
- * submit the form or not.
- *
- * @return $this
- */
- public function handleRequest(mixed $request = null): static;
- /**
- * Submits data to the form.
- *
- * @param string|array|null $submittedData The submitted data
- * @param bool $clearMissing Whether to set fields to NULL
- * when they are missing in the
- * submitted data. This argument
- * is only used in compound form
- *
- * @return $this
- *
- * @throws Exception\AlreadySubmittedException if the form has already been submitted
- */
- public function submit(string|array|null $submittedData, bool $clearMissing = true): static;
- /**
- * Returns the root of the form tree.
- */
- public function getRoot(): self;
- /**
- * Returns whether the field is the root of the form tree.
- */
- public function isRoot(): bool;
- public function createView(?FormView $parent = null): FormView;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormError.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Exception\BadMethodCallException;
- /**
- * Wraps errors in forms.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class FormError
- {
- protected string $messageTemplate;
- /**
- * The form that spawned this error.
- */
- private ?FormInterface $origin = null;
- /**
- * Any array key in $messageParameters will be used as a placeholder in
- * $messageTemplate.
- *
- * @param string $message The translated error message
- * @param string|null $messageTemplate The template for the error message
- * @param array $messageParameters The parameters that should be
- * substituted in the message template
- * @param int|null $messagePluralization The value for error message pluralization
- * @param mixed $cause The cause of the error
- *
- * @see \Symfony\Component\Translation\Translator
- */
- public function __construct(
- private string $message,
- ?string $messageTemplate = null,
- protected array $messageParameters = [],
- protected ?int $messagePluralization = null,
- private mixed $cause = null,
- ) {
- $this->messageTemplate = $messageTemplate ?: $message;
- }
- /**
- * Returns the error message.
- */
- public function getMessage(): string
- {
- return $this->message;
- }
- /**
- * Returns the error message template.
- */
- public function getMessageTemplate(): string
- {
- return $this->messageTemplate;
- }
- /**
- * Returns the parameters to be inserted in the message template.
- */
- public function getMessageParameters(): array
- {
- return $this->messageParameters;
- }
- /**
- * Returns the value for error message pluralization.
- */
- public function getMessagePluralization(): ?int
- {
- return $this->messagePluralization;
- }
- /**
- * Returns the cause of this error.
- */
- public function getCause(): mixed
- {
- return $this->cause;
- }
- /**
- * Sets the form that caused this error.
- *
- * This method must only be called once.
- *
- * @throws BadMethodCallException If the method is called more than once
- */
- public function setOrigin(FormInterface $origin): void
- {
- if (null !== $this->origin) {
- throw new BadMethodCallException('setOrigin() must only be called once.');
- }
- $this->origin = $origin;
- }
- /**
- * Returns the form that caused this error.
- */
- public function getOrigin(): ?FormInterface
- {
- return $this->origin;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormEvent.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Contracts\EventDispatcher\Event;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class FormEvent extends Event
- {
- public function __construct(
- private FormInterface $form,
- protected mixed $data,
- ) {
- }
- /**
- * Returns the form at the source of the event.
- */
- public function getForm(): FormInterface
- {
- return $this->form;
- }
- /**
- * Returns the data associated with this event.
- */
- public function getData(): mixed
- {
- return $this->data;
- }
- /**
- * Allows updating with some filtered data.
- */
- public function setData(mixed $data): void
- {
- $this->data = $data;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./DependencyInjection/FormPass.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\DependencyInjection;
- use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
- use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
- use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
- use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
- use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
- use Symfony\Component\DependencyInjection\ContainerBuilder;
- use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
- use Symfony\Component\DependencyInjection\Reference;
- /**
- * Adds all services with the tags "form.type", "form.type_extension" and
- * "form.type_guesser" as arguments of the "form.extension" service.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class FormPass implements CompilerPassInterface
- {
- use PriorityTaggedServiceTrait;
- public function process(ContainerBuilder $container): void
- {
- if (!$container->hasDefinition('form.extension')) {
- return;
- }
- $definition = $container->getDefinition('form.extension');
- $definition->replaceArgument(0, $this->processFormTypes($container));
- $definition->replaceArgument(1, $this->processFormTypeExtensions($container));
- $definition->replaceArgument(2, $this->processFormTypeGuessers($container));
- }
- private function processFormTypes(ContainerBuilder $container): Reference
- {
- // Get service locator argument
- $servicesMap = [];
- $namespaces = ['Symfony\Component\Form\Extension\Core\Type' => true];
- $csrfTokenIds = [];
- // Builds an array with fully-qualified type class names as keys and service IDs as values
- foreach ($container->findTaggedServiceIds('form.type', true) as $serviceId => $tag) {
- // Add form type service to the service locator
- $serviceDefinition = $container->getDefinition($serviceId);
- $servicesMap[$formType = $serviceDefinition->getClass()] = new Reference($serviceId);
- $namespaces[substr($formType, 0, strrpos($formType, '\\'))] = true;
- if (isset($tag[0]['csrf_token_id'])) {
- $csrfTokenIds[$formType] = $tag[0]['csrf_token_id'];
- }
- }
- if ($container->hasDefinition('console.command.form_debug')) {
- $commandDefinition = $container->getDefinition('console.command.form_debug');
- $commandDefinition->setArgument(1, array_keys($namespaces));
- $commandDefinition->setArgument(2, array_keys($servicesMap));
- }
- if ($csrfTokenIds && $container->hasDefinition('form.type_extension.csrf')) {
- $csrfExtension = $container->getDefinition('form.type_extension.csrf');
- if (8 <= \count($csrfExtension->getArguments())) {
- $csrfExtension->replaceArgument(7, $csrfTokenIds);
- }
- }
- return ServiceLocatorTagPass::register($container, $servicesMap);
- }
- private function processFormTypeExtensions(ContainerBuilder $container): array
- {
- $typeExtensions = [];
- $typeExtensionsClasses = [];
- foreach ($this->findAndSortTaggedServices('form.type_extension', $container) as $reference) {
- $serviceId = (string) $reference;
- $serviceDefinition = $container->getDefinition($serviceId);
- $tag = $serviceDefinition->getTag('form.type_extension');
- $typeExtensionClass = $container->getParameterBag()->resolveValue($serviceDefinition->getClass());
- if (isset($tag[0]['extended_type'])) {
- $typeExtensions[$tag[0]['extended_type']][] = new Reference($serviceId);
- $typeExtensionsClasses[] = $typeExtensionClass;
- } else {
- $extendsTypes = false;
- $typeExtensionsClasses[] = $typeExtensionClass;
- foreach ($typeExtensionClass::getExtendedTypes() as $extendedType) {
- $typeExtensions[$extendedType][] = new Reference($serviceId);
- $extendsTypes = true;
- }
- if (!$extendsTypes) {
- throw new InvalidArgumentException(\sprintf('The getExtendedTypes() method for service "%s" does not return any extended types.', $serviceId));
- }
- }
- }
- foreach ($typeExtensions as $extendedType => $extensions) {
- $typeExtensions[$extendedType] = new IteratorArgument($extensions);
- }
- if ($container->hasDefinition('console.command.form_debug')) {
- $commandDefinition = $container->getDefinition('console.command.form_debug');
- $commandDefinition->setArgument(3, $typeExtensionsClasses);
- }
- return $typeExtensions;
- }
- private function processFormTypeGuessers(ContainerBuilder $container): ArgumentInterface
- {
- $guessers = [];
- $guessersClasses = [];
- foreach ($container->findTaggedServiceIds('form.type_guesser', true) as $serviceId => $tags) {
- $guessers[] = new Reference($serviceId);
- $serviceDefinition = $container->getDefinition($serviceId);
- $guessersClasses[] = $serviceDefinition->getClass();
- }
- if ($container->hasDefinition('console.command.form_debug')) {
- $commandDefinition = $container->getDefinition('console.command.form_debug');
- $commandDefinition->setArgument(4, $guessersClasses);
- }
- return new IteratorArgument($guessers);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormTypeGuesserChain.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- use Symfony\Component\Form\Guess\Guess;
- use Symfony\Component\Form\Guess\TypeGuess;
- use Symfony\Component\Form\Guess\ValueGuess;
- class FormTypeGuesserChain implements FormTypeGuesserInterface
- {
- private array $guessers = [];
- /**
- * @param FormTypeGuesserInterface[] $guessers
- *
- * @throws UnexpectedTypeException if any guesser does not implement FormTypeGuesserInterface
- */
- public function __construct(iterable $guessers)
- {
- $tmpGuessers = [];
- foreach ($guessers as $guesser) {
- if (!$guesser instanceof FormTypeGuesserInterface) {
- throw new UnexpectedTypeException($guesser, FormTypeGuesserInterface::class);
- }
- if ($guesser instanceof self) {
- $tmpGuessers[] = $guesser->guessers;
- } else {
- $tmpGuessers[] = [$guesser];
- }
- }
- $this->guessers = array_merge([], ...$tmpGuessers);
- }
- public function guessType(string $class, string $property): ?TypeGuess
- {
- return $this->guess(static fn ($guesser) => $guesser->guessType($class, $property));
- }
- public function guessRequired(string $class, string $property): ?ValueGuess
- {
- return $this->guess(static fn ($guesser) => $guesser->guessRequired($class, $property));
- }
- public function guessMaxLength(string $class, string $property): ?ValueGuess
- {
- return $this->guess(static fn ($guesser) => $guesser->guessMaxLength($class, $property));
- }
- public function guessPattern(string $class, string $property): ?ValueGuess
- {
- return $this->guess(static fn ($guesser) => $guesser->guessPattern($class, $property));
- }
- /**
- * Executes a closure for each guesser and returns the best guess from the
- * return values.
- *
- * @param \Closure $closure The closure to execute. Accepts a guesser
- * as argument and should return a Guess instance
- */
- private function guess(\Closure $closure): ?Guess
- {
- $guesses = [];
- foreach ($this->guessers as $guesser) {
- if ($guess = $closure($guesser)) {
- $guesses[] = $guess;
- }
- }
- return Guess::getBestGuess($guesses);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormEvents.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Event\PostSetDataEvent;
- use Symfony\Component\Form\Event\PostSubmitEvent;
- use Symfony\Component\Form\Event\PreSetDataEvent;
- use Symfony\Component\Form\Event\PreSubmitEvent;
- use Symfony\Component\Form\Event\SubmitEvent;
- /**
- * To learn more about how form events work check the documentation
- * entry at {@link https://symfony.com/doc/any/components/form/form_events.html}.
- *
- * To learn how to dynamically modify forms using events check the cookbook
- * entry at {@link https://symfony.com/doc/any/cookbook/form/dynamic_form_modification.html}.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- final class FormEvents
- {
- /**
- * The PRE_SUBMIT event is dispatched at the beginning of the Form::submit() method.
- *
- * It can be used to:
- * - Change data from the request, before submitting the data to the form.
- * - Add or remove form fields, before submitting the data to the form.
- *
- * @Event("Symfony\Component\Form\Event\PreSubmitEvent")
- */
- public const PRE_SUBMIT = 'form.pre_submit';
- /**
- * The SUBMIT event is dispatched after the Form::submit() method
- * has changed the view data by the request data, or submitted and mapped
- * the children if the form is compound, and after reverse transformation
- * to normalized representation.
- *
- * It's also dispatched just before the Form::submit() method transforms back
- * the normalized data to the model and view data.
- *
- * So at this stage children of compound forms are submitted and synchronized, unless
- * their transformation failed, but a parent would still be at the PRE_SUBMIT level.
- *
- * Since the current form is not synchronized yet, it is still possible to add and
- * remove fields.
- *
- * @Event("Symfony\Component\Form\Event\SubmitEvent")
- */
- public const SUBMIT = 'form.submit';
- /**
- * The FormEvents::POST_SUBMIT event is dispatched at the very end of the Form::submit().
- *
- * It this stage the model and view data may have been denormalized. Otherwise the form
- * is desynchronized because transformation failed during submission.
- *
- * It can be used to fetch data after denormalization.
- *
- * The event attaches the current view data. To know whether this is the renormalized data
- * or the invalid request data, call Form::isSynchronized() first.
- *
- * @Event("Symfony\Component\Form\Event\PostSubmitEvent")
- */
- public const POST_SUBMIT = 'form.post_submit';
- /**
- * The FormEvents::PRE_SET_DATA event is dispatched at the beginning of the Form::setData() method.
- *
- * It can be used to:
- * - Modify the data given during pre-population;
- * - Keep synchronized the form depending on the data (adding or removing fields dynamically).
- *
- * @Event("Symfony\Component\Form\Event\PreSetDataEvent")
- */
- public const PRE_SET_DATA = 'form.pre_set_data';
- /**
- * The FormEvents::POST_SET_DATA event is dispatched at the end of the Form::setData() method.
- *
- * This event can be used to modify the form depending on the final state of the underlying data
- * accessible in every representation: model, normalized and view.
- *
- * @Event("Symfony\Component\Form\Event\PostSetDataEvent")
- */
- public const POST_SET_DATA = 'form.post_set_data';
- /**
- * Event aliases.
- *
- * These aliases can be consumed by RegisterListenersPass.
- */
- public const ALIASES = [
- PreSubmitEvent::class => self::PRE_SUBMIT,
- SubmitEvent::class => self::SUBMIT,
- PostSubmitEvent::class => self::POST_SUBMIT,
- PreSetDataEvent::class => self::PRE_SET_DATA,
- PostSetDataEvent::class => self::POST_SET_DATA,
- ];
- private function __construct()
- {
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormRegistryInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * The central registry of the Form component.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormRegistryInterface
- {
- /**
- * Returns a form type by name.
- *
- * This method registers the type extensions from the form extensions.
- *
- * @throws Exception\InvalidArgumentException if the type cannot be retrieved from any extension
- */
- public function getType(string $name): ResolvedFormTypeInterface;
- /**
- * Returns whether the given form type is supported.
- */
- public function hasType(string $name): bool;
- /**
- * Returns the guesser responsible for guessing types.
- */
- public function getTypeGuesser(): ?FormTypeGuesserInterface;
- /**
- * Returns the extensions loaded by the framework.
- *
- * @return FormExtensionInterface[]
- */
- public function getExtensions(): array;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormTypeInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormTypeInterface
- {
- /**
- * Returns the name of the parent type.
- *
- * The parent type and its extensions will configure the form with the
- * following methods before the current implementation.
- *
- * @return string|null
- */
- public function getParent();
- /**
- * Configures the options for this type.
- *
- * @return void
- */
- public function configureOptions(OptionsResolver $resolver);
- /**
- * Builds the form.
- *
- * This method is called for each type in the hierarchy starting from the
- * top most type. Type extensions can further modify the form.
- *
- * @param array<string, mixed> $options
- *
- * @return void
- *
- * @see FormTypeExtensionInterface::buildForm()
- */
- public function buildForm(FormBuilderInterface $builder, array $options);
- /**
- * Builds the form view.
- *
- * This method is called for each type in the hierarchy starting from the
- * top most type. Type extensions can further modify the view.
- *
- * A view of a form is built before the views of the child forms are built.
- * This means that you cannot access child views in this method. If you need
- * to do so, move your logic to {@link finishView()} instead.
- *
- * @param array<string, mixed> $options
- *
- * @return void
- *
- * @see FormTypeExtensionInterface::buildView()
- */
- public function buildView(FormView $view, FormInterface $form, array $options);
- /**
- * Finishes the form view.
- *
- * This method gets called for each type in the hierarchy starting from the
- * top most type. Type extensions can further modify the view.
- *
- * When this method is called, views of the form's children have already
- * been built and finished and can be accessed. You should only implement
- * such logic in this method that actually accesses child views. For everything
- * else you are recommended to implement {@link buildView()} instead.
- *
- * @param array<string, mixed> $options
- *
- * @return void
- *
- * @see FormTypeExtensionInterface::finishView()
- */
- public function finishView(FormView $view, FormInterface $form, array $options);
- /**
- * Returns the prefix of the template block name for this type.
- *
- * The block prefix defaults to the underscored short class name with
- * the "Type" suffix removed (e.g. "UserProfileType" => "user_profile").
- *
- * @return string
- */
- public function getBlockPrefix();
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/OutOfBoundsException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- /**
- * Base OutOfBoundsException for Form component.
- *
- * @author Alexander Kotynia <[email protected]>
- */
- class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/AccessException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- class AccessException extends RuntimeException
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/TransformationFailedException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- /**
- * Indicates a value transformation error.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class TransformationFailedException extends RuntimeException
- {
- private ?string $invalidMessage;
- private array $invalidMessageParameters;
- public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, ?string $invalidMessage = null, array $invalidMessageParameters = [])
- {
- parent::__construct($message, $code, $previous);
- $this->setInvalidMessage($invalidMessage, $invalidMessageParameters);
- }
- /**
- * Sets the message that will be shown to the user.
- *
- * @param string|null $invalidMessage The message or message key
- * @param array $invalidMessageParameters Data to be passed into the translator
- */
- public function setInvalidMessage(?string $invalidMessage, array $invalidMessageParameters = []): void
- {
- $this->invalidMessage = $invalidMessage;
- $this->invalidMessageParameters = $invalidMessageParameters;
- }
- public function getInvalidMessage(): ?string
- {
- return $this->invalidMessage;
- }
- public function getInvalidMessageParameters(): array
- {
- return $this->invalidMessageParameters;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/RuntimeException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- /**
- * Base RuntimeException for the Form component.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class RuntimeException extends \RuntimeException implements ExceptionInterface
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/ExceptionInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- /**
- * Base ExceptionInterface for the Form component.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface ExceptionInterface extends \Throwable
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/BadMethodCallException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- /**
- * Base BadMethodCallException for the Form component.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/InvalidArgumentException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- /**
- * Base InvalidArgumentException for the Form component.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/LogicException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- /**
- * Base LogicException for Form component.
- *
- * @author Alexander Kotynia <[email protected]>
- */
- class LogicException extends \LogicException implements ExceptionInterface
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/InvalidConfigurationException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- class InvalidConfigurationException extends InvalidArgumentException
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/ErrorMappingException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- class ErrorMappingException extends RuntimeException
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/StringCastException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- class StringCastException extends RuntimeException
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/AlreadySubmittedException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- /**
- * Thrown when an operation is called that is not acceptable after submitting
- * a form.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class AlreadySubmittedException extends LogicException
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Exception/UnexpectedTypeException.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Exception;
- class UnexpectedTypeException extends InvalidArgumentException
- {
- public function __construct(mixed $value, string $expectedType)
- {
- parent::__construct(\sprintf('Expected argument of type "%s", "%s" given', $expectedType, get_debug_type($value)));
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Event/PreSetDataEvent.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Event;
- use Symfony\Component\Form\FormEvent;
- /**
- * This event is dispatched at the beginning of the Form::setData() method.
- *
- * It can be used to modify the data given during pre-population.
- */
- final class PreSetDataEvent extends FormEvent
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Event/PreSubmitEvent.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Event;
- use Symfony\Component\Form\FormEvent;
- /**
- * This event is dispatched at the beginning of the Form::submit() method.
- *
- * It can be used to:
- * - Change data from the request, before submitting the data to the form.
- * - Add or remove form fields, before submitting the data to the form.
- */
- final class PreSubmitEvent extends FormEvent
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Event/PostSetDataEvent.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Event;
- use Symfony\Component\Form\Exception\BadMethodCallException;
- use Symfony\Component\Form\FormEvent;
- /**
- * This event is dispatched at the end of the Form::setData() method.
- *
- * It can be used to modify a form depending on the populated data (adding or
- * removing fields dynamically).
- */
- final class PostSetDataEvent extends FormEvent
- {
- public function setData(mixed $data): never
- {
- throw new BadMethodCallException('Form data cannot be changed during "form.post_set_data", you should use "form.pre_set_data" instead.');
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Event/PostSubmitEvent.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Event;
- use Symfony\Component\Form\Exception\BadMethodCallException;
- use Symfony\Component\Form\FormEvent;
- /**
- * This event is dispatched after the Form::submit()
- * once the model and view data have been denormalized.
- *
- * It can be used to fetch data after denormalization.
- */
- final class PostSubmitEvent extends FormEvent
- {
- public function setData(mixed $data): never
- {
- throw new BadMethodCallException('Form data cannot be changed during "form.post_submit", you should use "form.pre_submit" or "form.submit" instead.');
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Event/SubmitEvent.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Event;
- use Symfony\Component\Form\FormEvent;
- /**
- * This event is dispatched just before the Form::submit() method
- * transforms back the normalized data to the model and view data.
- *
- * It can be used to change data from the normalized representation of the data.
- */
- final class SubmitEvent extends FormEvent
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Util/OrderedHashMapIterator.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Util;
- /**
- * Iterator for {@link OrderedHashMap} objects.
- *
- * @author Bernhard Schussek <[email protected]>
- *
- * @internal
- *
- * @template-covariant TValue
- *
- * @implements \Iterator<string, TValue>
- */
- class OrderedHashMapIterator implements \Iterator
- {
- private int $cursor = 0;
- private int $cursorId;
- private ?string $key = null;
- /** @var TValue|null */
- private mixed $current = null;
- /**
- * @param TValue[] $elements The elements of the map, indexed by their
- * keys
- * @param list<string> $orderedKeys The keys of the map in the order in which
- * they should be iterated
- * @param array<int, int> $managedCursors An array from which to reference the
- * iterator's cursor as long as it is alive.
- * This array is managed by the corresponding
- * {@link OrderedHashMap} instance to support
- * recognizing the deletion of elements.
- */
- public function __construct(
- private array &$elements,
- private array &$orderedKeys,
- private array &$managedCursors,
- ) {
- $this->cursorId = \count($managedCursors);
- $this->managedCursors[$this->cursorId] = &$this->cursor;
- }
- public function __sleep(): array
- {
- throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
- }
- public function __wakeup(): void
- {
- throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
- }
- /**
- * Removes the iterator's cursors from the managed cursors of the
- * corresponding {@link OrderedHashMap} instance.
- */
- public function __destruct()
- {
- // Use array_splice() instead of unset() to prevent holes in the
- // array indices, which would break the initialization of $cursorId
- array_splice($this->managedCursors, $this->cursorId, 1);
- }
- public function current(): mixed
- {
- return $this->current;
- }
- public function next(): void
- {
- ++$this->cursor;
- if (isset($this->orderedKeys[$this->cursor])) {
- $this->key = $this->orderedKeys[$this->cursor];
- $this->current = $this->elements[$this->key];
- } else {
- $this->key = null;
- $this->current = null;
- }
- }
- public function key(): mixed
- {
- return $this->key;
- }
- public function valid(): bool
- {
- return null !== $this->key;
- }
- public function rewind(): void
- {
- $this->cursor = 0;
- if (isset($this->orderedKeys[0])) {
- $this->key = $this->orderedKeys[0];
- $this->current = $this->elements[$this->key];
- } else {
- $this->key = null;
- $this->current = null;
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Util/ServerParams.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Util;
- use Symfony\Component\HttpFoundation\RequestStack;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class ServerParams
- {
- public function __construct(
- private ?RequestStack $requestStack = null,
- ) {
- }
- /**
- * Returns true if the POST max size has been exceeded in the request.
- */
- public function hasPostMaxSizeBeenExceeded(): bool
- {
- $contentLength = $this->getContentLength();
- $maxContentLength = $this->getPostMaxSize();
- return $maxContentLength && $contentLength > $maxContentLength;
- }
- /**
- * Returns maximum post size in bytes.
- */
- public function getPostMaxSize(): int|float|null
- {
- $iniMax = strtolower($this->getNormalizedIniPostMaxSize());
- if ('' === $iniMax) {
- return null;
- }
- $max = ltrim($iniMax, '+');
- if (str_starts_with($max, '0x')) {
- $max = \intval($max, 16);
- } elseif (str_starts_with($max, '0')) {
- $max = \intval($max, 8);
- } else {
- $max = (int) $max;
- }
- switch (substr($iniMax, -1)) {
- case 't': $max *= 1024;
- // no break
- case 'g': $max *= 1024;
- // no break
- case 'm': $max *= 1024;
- // no break
- case 'k': $max *= 1024;
- }
- return $max;
- }
- /**
- * Returns the normalized "post_max_size" ini setting.
- */
- public function getNormalizedIniPostMaxSize(): string
- {
- return strtoupper(trim(\ini_get('post_max_size')));
- }
- /**
- * Returns the content length of the request.
- */
- public function getContentLength(): mixed
- {
- if (null !== $this->requestStack && null !== $request = $this->requestStack->getCurrentRequest()) {
- return $request->server->get('CONTENT_LENGTH');
- }
- return isset($_SERVER['CONTENT_LENGTH'])
- ? (int) $_SERVER['CONTENT_LENGTH']
- : null;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Util/InheritDataAwareIterator.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Util;
- /**
- * Iterator that traverses an array of forms.
- *
- * Contrary to \ArrayIterator, this iterator recognizes changes in the original
- * array during iteration.
- *
- * You can wrap the iterator into a {@link \RecursiveIteratorIterator} in order to
- * enter any child form that inherits its parent's data and iterate the children
- * of that form as well.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class InheritDataAwareIterator extends \IteratorIterator implements \RecursiveIterator
- {
- public function getChildren(): static
- {
- return new static($this->current());
- }
- public function hasChildren(): bool
- {
- return (bool) $this->current()->getConfig()->getInheritData();
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Util/OptionsResolverWrapper.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Util;
- use Symfony\Component\OptionsResolver\Exception\AccessException;
- use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Yonel Ceruto <[email protected]>
- *
- * @internal
- */
- class OptionsResolverWrapper extends OptionsResolver
- {
- private array $undefined = [];
- /**
- * @return $this
- */
- public function setNormalizer(string $option, \Closure $normalizer): static
- {
- try {
- parent::setNormalizer($option, $normalizer);
- } catch (UndefinedOptionsException) {
- $this->undefined[$option] = true;
- }
- return $this;
- }
- /**
- * @return $this
- */
- public function setAllowedValues(string $option, mixed $allowedValues): static
- {
- try {
- parent::setAllowedValues($option, $allowedValues);
- } catch (UndefinedOptionsException) {
- $this->undefined[$option] = true;
- }
- return $this;
- }
- /**
- * @return $this
- */
- public function addAllowedValues(string $option, mixed $allowedValues): static
- {
- try {
- parent::addAllowedValues($option, $allowedValues);
- } catch (UndefinedOptionsException) {
- $this->undefined[$option] = true;
- }
- return $this;
- }
- /**
- * @param string|array $allowedTypes
- *
- * @return $this
- */
- public function setAllowedTypes(string $option, $allowedTypes): static
- {
- try {
- parent::setAllowedTypes($option, $allowedTypes);
- } catch (UndefinedOptionsException) {
- $this->undefined[$option] = true;
- }
- return $this;
- }
- /**
- * @param string|array $allowedTypes
- *
- * @return $this
- */
- public function addAllowedTypes(string $option, $allowedTypes): static
- {
- try {
- parent::addAllowedTypes($option, $allowedTypes);
- } catch (UndefinedOptionsException) {
- $this->undefined[$option] = true;
- }
- return $this;
- }
- public function resolve(array $options = []): array
- {
- throw new AccessException('Resolve options is not supported.');
- }
- public function getUndefinedOptions(): array
- {
- return array_keys($this->undefined);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Util/OrderedHashMap.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Util;
- /**
- * A hash map which keeps track of deletions and additions.
- *
- * Like in associative arrays, elements can be mapped to integer or string keys.
- * Unlike associative arrays, the map keeps track of the order in which keys
- * were added and removed. This order is reflected during iteration.
- *
- * The map supports concurrent modification during iteration. That means that
- * you can insert and remove elements from within a foreach loop and the
- * iterator will reflect those changes accordingly.
- *
- * While elements that are added during the loop are recognized by the iterator,
- * changed elements are not. Otherwise the loop could be infinite if each loop
- * changes the current element:
- *
- * $map = new OrderedHashMap();
- * $map[1] = 1;
- * $map[2] = 2;
- * $map[3] = 3;
- *
- * foreach ($map as $index => $value) {
- * echo "$index: $value\n"
- * if (1 === $index) {
- * $map[1] = 4;
- * $map[] = 5;
- * }
- * }
- *
- * print_r(iterator_to_array($map));
- *
- * // => 1: 1
- * // 2: 2
- * // 3: 3
- * // 4: 5
- * // Array
- * // (
- * // [1] => 4
- * // [2] => 2
- * // [3] => 3
- * // [4] => 5
- * // )
- *
- * The map also supports multiple parallel iterators. That means that you can
- * nest foreach loops without affecting each other's iteration:
- *
- * foreach ($map as $index => $value) {
- * foreach ($map as $index2 => $value2) {
- * // ...
- * }
- * }
- *
- * @author Bernhard Schussek <[email protected]>
- *
- * @template TValue
- *
- * @implements \ArrayAccess<string, TValue>
- * @implements \IteratorAggregate<string, TValue>
- */
- class OrderedHashMap implements \ArrayAccess, \IteratorAggregate, \Countable
- {
- /**
- * The keys of the map in the order in which they were inserted or changed.
- *
- * @var list<string>
- */
- private array $orderedKeys = [];
- /**
- * References to the cursors of all open iterators.
- *
- * @var array<int, int>
- */
- private array $managedCursors = [];
- /**
- * Creates a new map.
- *
- * @param TValue[] $elements The initial elements of the map, indexed by their keys
- */
- public function __construct(
- private array $elements = [],
- ) {
- // the explicit string type-cast is necessary as digit-only keys would be returned as integers otherwise
- $this->orderedKeys = array_map(strval(...), array_keys($elements));
- }
- public function offsetExists(mixed $key): bool
- {
- return isset($this->elements[$key]);
- }
- public function offsetGet(mixed $key): mixed
- {
- if (!isset($this->elements[$key])) {
- throw new \OutOfBoundsException(\sprintf('The offset "%s" does not exist.', $key));
- }
- return $this->elements[$key];
- }
- public function offsetSet(mixed $key, mixed $value): void
- {
- if (null === $key || !isset($this->elements[$key])) {
- if (null === $key) {
- $key = [] === $this->orderedKeys
- // If the array is empty, use 0 as key
- ? 0
- // Imitate PHP behavior of generating a key that equals
- // the highest existing integer key + 1
- : 1 + (int) max($this->orderedKeys);
- }
- $this->orderedKeys[] = (string) $key;
- }
- $this->elements[$key] = $value;
- }
- public function offsetUnset(mixed $key): void
- {
- if (false !== ($position = array_search((string) $key, $this->orderedKeys))) {
- array_splice($this->orderedKeys, $position, 1);
- unset($this->elements[$key]);
- foreach ($this->managedCursors as $i => $cursor) {
- if ($cursor >= $position) {
- --$this->managedCursors[$i];
- }
- }
- }
- }
- public function getIterator(): \Traversable
- {
- return new OrderedHashMapIterator($this->elements, $this->orderedKeys, $this->managedCursors);
- }
- public function count(): int
- {
- return \count($this->elements);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Util/StringUtil.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Util;
- /**
- * @author Issei Murasawa <[email protected]>
- * @author Bernhard Schussek <[email protected]>
- */
- class StringUtil
- {
- /**
- * This class should not be instantiated.
- */
- private function __construct()
- {
- }
- /**
- * Returns the trimmed data.
- */
- public static function trim(string $string): string
- {
- if (null !== $result = @preg_replace('/^[\pZ\p{Cc}\p{Cf}]+|[\pZ\p{Cc}\p{Cf}]+$/u', '', $string)) {
- return $result;
- }
- return trim($string);
- }
- /**
- * Converts a fully-qualified class name to a block prefix.
- *
- * @param string $fqcn The fully-qualified class name
- */
- public static function fqcnToBlockPrefix(string $fqcn): ?string
- {
- // Non-greedy ("+?") to match "type" suffix, if present
- if (preg_match('~([^\\\\]+?)(type)?$~i', $fqcn, $matches)) {
- return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $matches[1]));
- }
- return null;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Util/FormUtil.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Util;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class FormUtil
- {
- /**
- * This class should not be instantiated.
- */
- private function __construct()
- {
- }
- /**
- * Returns whether the given data is empty.
- *
- * This logic is reused multiple times throughout the processing of
- * a form and needs to be consistent. PHP keyword `empty` cannot
- * be used as it also considers 0 and "0" to be empty.
- */
- public static function isEmpty(mixed $data): bool
- {
- // Should not do a check for [] === $data!!!
- // This method is used in occurrences where arrays are
- // not considered to be empty, ever.
- return null === $data || '' === $data;
- }
- /**
- * Recursively replaces or appends elements of the first array with elements
- * of second array. If the key is an integer, the values will be appended to
- * the new array; otherwise, the value from the second array will replace
- * the one from the first array.
- */
- public static function mergeParamsAndFiles(array $params, array $files): array
- {
- $isFilesList = array_is_list($files);
- foreach ($params as $key => $value) {
- if (\is_array($value) && \is_array($files[$key] ?? null)) {
- $params[$key] = self::mergeParamsAndFiles($value, $files[$key]);
- unset($files[$key]);
- }
- }
- if (!$isFilesList) {
- return array_replace($params, $files);
- }
- foreach ($files as $value) {
- $params[] = $value;
- }
- return $params;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormTypeGuesserInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormTypeGuesserInterface
- {
- /**
- * Returns a field guess for a property name of a class.
- */
- public function guessType(string $class, string $property): ?Guess\TypeGuess;
- /**
- * Returns a guess whether a property of a class is required.
- */
- public function guessRequired(string $class, string $property): ?Guess\ValueGuess;
- /**
- * Returns a guess about the field's maximum length.
- */
- public function guessMaxLength(string $class, string $property): ?Guess\ValueGuess;
- /**
- * Returns a guess about the field's pattern.
- */
- public function guessPattern(string $class, string $property): ?Guess\ValueGuess;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Loader/IntlCallbackChoiceLoader.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Loader;
- /**
- * Callback choice loader optimized for Intl choice types.
- *
- * @author Jules Pietri <[email protected]>
- * @author Yonel Ceruto <[email protected]>
- */
- class IntlCallbackChoiceLoader extends CallbackChoiceLoader
- {
- public function loadChoicesForValues(array $values, ?callable $value = null): array
- {
- return parent::loadChoicesForValues(array_filter($values), $value);
- }
- public function loadValuesForChoices(array $choices, ?callable $value = null): array
- {
- $choices = array_filter($choices);
- // If no callable is set, choices are the same as values
- if (null === $value) {
- return $choices;
- }
- return parent::loadValuesForChoices($choices, $value);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Loader/AbstractChoiceLoader.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Loader;
- use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
- use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
- /**
- * @author Jules Pietri <[email protected]>
- */
- abstract class AbstractChoiceLoader implements ChoiceLoaderInterface
- {
- private ?iterable $choices;
- /**
- * @final
- */
- public function loadChoiceList(?callable $value = null): ChoiceListInterface
- {
- return new ArrayChoiceList($this->choices ??= $this->loadChoices(), $value);
- }
- public function loadChoicesForValues(array $values, ?callable $value = null): array
- {
- if (!$values) {
- return [];
- }
- return $this->doLoadChoicesForValues($values, $value);
- }
- public function loadValuesForChoices(array $choices, ?callable $value = null): array
- {
- if (!$choices) {
- return [];
- }
- if ($value) {
- // if a value callback exists, use it
- return array_map(fn ($item) => (string) $value($item), $choices);
- }
- return $this->doLoadValuesForChoices($choices);
- }
- abstract protected function loadChoices(): iterable;
- protected function doLoadChoicesForValues(array $values, ?callable $value): array
- {
- return $this->loadChoiceList($value)->getChoicesForValues($values);
- }
- protected function doLoadValuesForChoices(array $choices): array
- {
- return $this->loadChoiceList()->getValuesForChoices($choices);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Loader/FilterChoiceLoaderDecorator.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Loader;
- /**
- * A decorator to filter choices only when they are loaded or partially loaded.
- *
- * @author Jules Pietri <[email protected]>
- */
- class FilterChoiceLoaderDecorator extends AbstractChoiceLoader
- {
- private ChoiceLoaderInterface $decoratedLoader;
- private \Closure $filter;
- public function __construct(ChoiceLoaderInterface $loader, callable $filter)
- {
- $this->decoratedLoader = $loader;
- $this->filter = $filter(...);
- }
- protected function loadChoices(): iterable
- {
- $list = $this->decoratedLoader->loadChoiceList();
- if (array_values($list->getValues()) === array_values($structuredValues = $list->getStructuredValues())) {
- return array_filter(array_combine($list->getOriginalKeys(), $list->getChoices()), $this->filter);
- }
- foreach ($structuredValues as $group => $values) {
- if (\is_array($values)) {
- if ($values && $filtered = array_filter($list->getChoicesForValues($values), $this->filter)) {
- $choices[$group] = $filtered;
- }
- continue;
- // filter empty groups
- }
- if ($filtered = array_filter($list->getChoicesForValues([$values]), $this->filter)) {
- $choices[$group] = $filtered[0];
- }
- }
- return $choices ?? [];
- }
- public function loadChoicesForValues(array $values, ?callable $value = null): array
- {
- return array_filter($this->decoratedLoader->loadChoicesForValues($values, $value), $this->filter);
- }
- public function loadValuesForChoices(array $choices, ?callable $value = null): array
- {
- return $this->decoratedLoader->loadValuesForChoices(array_filter($choices, $this->filter), $value);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Loader/LazyChoiceLoader.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Loader;
- use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
- use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
- /**
- * A choice loader that loads its choices and values lazily, only when necessary.
- *
- * @author Yonel Ceruto <[email protected]>
- */
- class LazyChoiceLoader implements ChoiceLoaderInterface
- {
- private ?ChoiceListInterface $choiceList = null;
- public function __construct(
- private readonly ChoiceLoaderInterface $loader,
- ) {
- }
- public function loadChoiceList(?callable $value = null): ChoiceListInterface
- {
- return $this->choiceList ??= new ArrayChoiceList([], $value);
- }
- public function loadChoicesForValues(array $values, ?callable $value = null): array
- {
- $choices = $this->loader->loadChoicesForValues($values, $value);
- $this->choiceList = new ArrayChoiceList($choices, $value);
- return $choices;
- }
- public function loadValuesForChoices(array $choices, ?callable $value = null): array
- {
- $values = $this->loader->loadValuesForChoices($choices, $value);
- if ($this->choiceList?->getValuesForChoices($choices) !== $values) {
- $this->loadChoicesForValues($values, $value);
- }
- return $values;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Loader/CallbackChoiceLoader.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Loader;
- /**
- * Loads an {@link ArrayChoiceList} instance from a callable returning iterable choices.
- *
- * @author Jules Pietri <[email protected]>
- */
- class CallbackChoiceLoader extends AbstractChoiceLoader
- {
- private \Closure $callback;
- /**
- * @param callable $callback The callable returning iterable choices
- */
- public function __construct(callable $callback)
- {
- $this->callback = $callback(...);
- }
- protected function loadChoices(): iterable
- {
- return ($this->callback)();
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Loader/ChoiceLoaderInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Loader;
- use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
- /**
- * Loads a choice list.
- *
- * The methods {@link loadChoicesForValues()} and {@link loadValuesForChoices()}
- * can be used to load the list only partially in cases where a fully-loaded
- * list is not necessary.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface ChoiceLoaderInterface
- {
- /**
- * Loads a list of choices.
- *
- * Optionally, a callable can be passed for generating the choice values.
- * The callable receives the choice as only argument.
- * Null may be passed when the choice list contains the empty value.
- *
- * @param callable|null $value The callable which generates the values
- * from choices
- */
- public function loadChoiceList(?callable $value = null): ChoiceListInterface;
- /**
- * Loads the choices corresponding to the given values.
- *
- * The choices are returned with the same keys and in the same order as the
- * corresponding values in the given array.
- *
- * Optionally, a callable can be passed for generating the choice values.
- * The callable receives the choice as only argument.
- * Null may be passed when the choice list contains the empty value.
- *
- * @param string[] $values An array of choice values. Non-existing
- * values in this array are ignored
- * @param callable|null $value The callable generating the choice values
- */
- public function loadChoicesForValues(array $values, ?callable $value = null): array;
- /**
- * Loads the values corresponding to the given choices.
- *
- * The values are returned with the same keys and in the same order as the
- * corresponding choices in the given array.
- *
- * Optionally, a callable can be passed for generating the choice values.
- * The callable receives the choice as only argument.
- * Null may be passed when the choice list contains the empty value.
- *
- * @param array $choices An array of choices. Non-existing choices in
- * this array are ignored
- * @param callable|null $value The callable generating the choice values
- *
- * @return string[]
- */
- public function loadValuesForChoices(array $choices, ?callable $value = null): array;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/ChoiceList.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFilter;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceTranslationParameters;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice;
- use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
- use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A set of convenient static methods to create cacheable choice list options.
- *
- * @author Jules Pietri <[email protected]>
- */
- final class ChoiceList
- {
- /**
- * Creates a cacheable loader from any callable providing iterable choices.
- *
- * @param callable $choices A callable that must return iterable choices or grouped choices
- * @param mixed $vary Dynamic data used to compute a unique hash when caching the loader
- */
- public static function lazy(FormTypeInterface|FormTypeExtensionInterface $formType, callable $choices, mixed $vary = null): ChoiceLoader
- {
- return self::loader($formType, new CallbackChoiceLoader($choices), $vary);
- }
- /**
- * Decorates a loader to make it cacheable.
- *
- * @param ChoiceLoaderInterface $loader A loader responsible for creating loading choices or grouped choices
- * @param mixed $vary Dynamic data used to compute a unique hash when caching the loader
- */
- public static function loader(FormTypeInterface|FormTypeExtensionInterface $formType, ChoiceLoaderInterface $loader, mixed $vary = null): ChoiceLoader
- {
- return new ChoiceLoader($formType, $loader, $vary);
- }
- /**
- * Decorates a "choice_value" callback to make it cacheable.
- *
- * @param callable|array $value Any pseudo callable to create a unique string value from a choice
- * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback
- */
- public static function value(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $value, mixed $vary = null): ChoiceValue
- {
- return new ChoiceValue($formType, $value, $vary);
- }
- /**
- * @param callable|array $filter Any pseudo callable to filter a choice list
- * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback
- */
- public static function filter(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $filter, mixed $vary = null): ChoiceFilter
- {
- return new ChoiceFilter($formType, $filter, $vary);
- }
- /**
- * Decorates a "choice_label" option to make it cacheable.
- *
- * @param callable|false $label Any pseudo callable to create a label from a choice or false to discard it
- * @param mixed $vary Dynamic data used to compute a unique hash when caching the option
- */
- public static function label(FormTypeInterface|FormTypeExtensionInterface $formType, callable|false $label, mixed $vary = null): ChoiceLabel
- {
- return new ChoiceLabel($formType, $label, $vary);
- }
- /**
- * Decorates a "choice_name" callback to make it cacheable.
- *
- * @param callable|array $fieldName Any pseudo callable to create a field name from a choice
- * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback
- */
- public static function fieldName(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $fieldName, mixed $vary = null): ChoiceFieldName
- {
- return new ChoiceFieldName($formType, $fieldName, $vary);
- }
- /**
- * Decorates a "choice_attr" option to make it cacheable.
- *
- * @param callable|array $attr Any pseudo callable or array to create html attributes from a choice
- * @param mixed $vary Dynamic data used to compute a unique hash when caching the option
- */
- public static function attr(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $attr, mixed $vary = null): ChoiceAttr
- {
- return new ChoiceAttr($formType, $attr, $vary);
- }
- /**
- * Decorates a "choice_translation_parameters" option to make it cacheable.
- *
- * @param callable|array $translationParameters Any pseudo callable or array to create translation parameters from a choice
- * @param mixed $vary Dynamic data used to compute a unique hash when caching the option
- */
- public static function translationParameters(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $translationParameters, mixed $vary = null): ChoiceTranslationParameters
- {
- return new ChoiceTranslationParameters($formType, $translationParameters, $vary);
- }
- /**
- * Decorates a "group_by" callback to make it cacheable.
- *
- * @param callable|array $groupBy Any pseudo callable to return a group name from a choice
- * @param mixed $vary Dynamic data used to compute a unique hash when caching the callback
- */
- public static function groupBy(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $groupBy, mixed $vary = null): GroupBy
- {
- return new GroupBy($formType, $groupBy, $vary);
- }
- /**
- * Decorates a "preferred_choices" option to make it cacheable.
- *
- * @param callable|array $preferred Any pseudo callable or array to return a group name from a choice
- * @param mixed $vary Dynamic data used to compute a unique hash when caching the option
- */
- public static function preferred(FormTypeInterface|FormTypeExtensionInterface $formType, callable|array $preferred, mixed $vary = null): PreferredChoice
- {
- return new PreferredChoice($formType, $preferred, $vary);
- }
- /**
- * Should not be instantiated.
- */
- private function __construct()
- {
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/ChoiceListInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList;
- /**
- * A list of choices that can be selected in a choice field.
- *
- * A choice list assigns unique string values to each of a list of choices.
- * These string values are displayed in the "value" attributes in HTML and
- * submitted back to the server.
- *
- * The acceptable data types for the choices depend on the implementation.
- * Values must always be strings and (within the list) free of duplicates.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface ChoiceListInterface
- {
- /**
- * Returns all selectable choices.
- *
- * @return array The selectable choices indexed by the corresponding values
- */
- public function getChoices(): array;
- /**
- * Returns the values for the choices.
- *
- * The values are strings that do not contain duplicates:
- *
- * $form->add('field', 'choice', [
- * 'choices' => [
- * 'Decided' => ['Yes' => true, 'No' => false],
- * 'Undecided' => ['Maybe' => null],
- * ],
- * ]);
- *
- * In this example, the result of this method is:
- *
- * [
- * 'Yes' => '0',
- * 'No' => '1',
- * 'Maybe' => '2',
- * ]
- *
- * Null and false MUST NOT conflict when being casted to string.
- * For this some default incremented values SHOULD be computed.
- *
- * @return string[]
- */
- public function getValues(): array;
- /**
- * Returns the values in the structure originally passed to the list.
- *
- * Contrary to {@link getValues()}, the result is indexed by the original
- * keys of the choices. If the original array contained nested arrays, these
- * nested arrays are represented here as well:
- *
- * $form->add('field', 'choice', [
- * 'choices' => [
- * 'Decided' => ['Yes' => true, 'No' => false],
- * 'Undecided' => ['Maybe' => null],
- * ],
- * ]);
- *
- * In this example, the result of this method is:
- *
- * [
- * 'Decided' => ['Yes' => '0', 'No' => '1'],
- * 'Undecided' => ['Maybe' => '2'],
- * ]
- *
- * Nested arrays do not make sense in a view format unless
- * they are used as a convenient way of grouping.
- * If the implementation does not intend to support grouped choices,
- * this method SHOULD be equivalent to {@link getValues()}.
- * The $groupBy callback parameter SHOULD be used instead.
- *
- * @return string[]
- */
- public function getStructuredValues(): array;
- /**
- * Returns the original keys of the choices.
- *
- * The original keys are the keys of the choice array that was passed in the
- * "choice" option of the choice type. Note that this array may contain
- * duplicates if the "choice" option contained choice groups:
- *
- * $form->add('field', 'choice', [
- * 'choices' => [
- * 'Decided' => [true, false],
- * 'Undecided' => [null],
- * ],
- * ]);
- *
- * In this example, the original key 0 appears twice, once for `true` and
- * once for `null`.
- *
- * @return int[]|string[] The original choice keys indexed by the
- * corresponding choice values
- */
- public function getOriginalKeys(): array;
- /**
- * Returns the choices corresponding to the given values.
- *
- * The choices are returned with the same keys and in the same order as the
- * corresponding values in the given array.
- *
- * @param string[] $values An array of choice values. Non-existing values in
- * this array are ignored
- */
- public function getChoicesForValues(array $values): array;
- /**
- * Returns the values corresponding to the given choices.
- *
- * The values are returned with the same keys and in the same order as the
- * corresponding choices in the given array.
- *
- * @param array $choices An array of choices. Non-existing choices in this
- * array are ignored
- *
- * @return string[]
- */
- public function getValuesForChoices(array $choices): array;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/LazyChoiceList.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList;
- use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
- /**
- * A choice list that loads its choices lazily.
- *
- * The choices are fetched using a {@link ChoiceLoaderInterface} instance.
- * If only {@link getChoicesForValues()} or {@link getValuesForChoices()} is
- * called, the choice list is only loaded partially for improved performance.
- *
- * Once {@link getChoices()} or {@link getValues()} is called, the list is
- * loaded fully.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class LazyChoiceList implements ChoiceListInterface
- {
- /**
- * The callable creating string values for each choice.
- *
- * If null, choices are cast to strings.
- */
- private ?\Closure $value;
- /**
- * Creates a lazily-loaded list using the given loader.
- *
- * Optionally, a callable can be passed for generating the choice values.
- * The callable receives the choice as first and the array key as the second
- * argument.
- *
- * @param callable|null $value The callable creating string values for each choice.
- * If null, choices are cast to strings.
- */
- public function __construct(
- private ChoiceLoaderInterface $loader,
- ?callable $value = null,
- ) {
- $this->value = null === $value ? null : $value(...);
- }
- public function getChoices(): array
- {
- return $this->loader->loadChoiceList($this->value)->getChoices();
- }
- public function getValues(): array
- {
- return $this->loader->loadChoiceList($this->value)->getValues();
- }
- public function getStructuredValues(): array
- {
- return $this->loader->loadChoiceList($this->value)->getStructuredValues();
- }
- public function getOriginalKeys(): array
- {
- return $this->loader->loadChoiceList($this->value)->getOriginalKeys();
- }
- public function getChoicesForValues(array $values): array
- {
- return $this->loader->loadChoicesForValues($values, $this->value);
- }
- public function getValuesForChoices(array $choices): array
- {
- return $this->loader->loadValuesForChoices($choices, $this->value);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/ArrayChoiceList.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList;
- /**
- * A list of choices with arbitrary data types.
- *
- * The user of this class is responsible for assigning string values to the
- * choices and for their uniqueness.
- * Both the choices and their values are passed to the constructor.
- * Each choice must have a corresponding value (with the same key) in
- * the values array.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ArrayChoiceList implements ChoiceListInterface
- {
- protected array $choices;
- /**
- * The values indexed by the original keys.
- */
- protected array $structuredValues;
- /**
- * The original keys of the choices array.
- */
- protected array $originalKeys;
- protected ?\Closure $valueCallback = null;
- /**
- * Creates a list with the given choices and values.
- *
- * The given choice array must have the same array keys as the value array.
- *
- * @param iterable $choices The selectable choices
- * @param callable|null $value The callable for creating the value
- * for a choice. If `null` is passed,
- * incrementing integers are used as
- * values
- */
- public function __construct(iterable $choices, ?callable $value = null)
- {
- if ($choices instanceof \Traversable) {
- $choices = iterator_to_array($choices);
- }
- if (null === $value && $this->castableToString($choices)) {
- $value = static fn ($choice) => false === $choice ? '0' : (string) $choice;
- }
- if (null !== $value) {
- // If a deterministic value generator was passed, use it later
- $this->valueCallback = $value(...);
- } else {
- // Otherwise generate incrementing integers as values
- $value = static function () {
- static $i = 0;
- return $i++;
- };
- }
- // If the choices are given as recursive array (i.e. with explicit
- // choice groups), flatten the array. The grouping information is needed
- // in the view only.
- $this->flatten($choices, $value, $choicesByValues, $keysByValues, $structuredValues);
- $this->choices = $choicesByValues;
- $this->originalKeys = $keysByValues;
- $this->structuredValues = $structuredValues;
- }
- public function getChoices(): array
- {
- return $this->choices;
- }
- public function getValues(): array
- {
- return array_map('strval', array_keys($this->choices));
- }
- public function getStructuredValues(): array
- {
- return $this->structuredValues;
- }
- public function getOriginalKeys(): array
- {
- return $this->originalKeys;
- }
- public function getChoicesForValues(array $values): array
- {
- $choices = [];
- foreach ($values as $i => $givenValue) {
- if (\array_key_exists($givenValue, $this->choices)) {
- $choices[$i] = $this->choices[$givenValue];
- }
- }
- return $choices;
- }
- public function getValuesForChoices(array $choices): array
- {
- $values = [];
- // Use the value callback to compare choices by their values, if present
- if ($this->valueCallback) {
- $givenValues = [];
- foreach ($choices as $i => $givenChoice) {
- $givenValues[$i] = (string) ($this->valueCallback)($givenChoice);
- }
- return array_intersect($givenValues, array_keys($this->choices));
- }
- // Otherwise compare choices by identity
- foreach ($choices as $i => $givenChoice) {
- foreach ($this->choices as $value => $choice) {
- if ($choice === $givenChoice) {
- $values[$i] = (string) $value;
- break;
- }
- }
- }
- return $values;
- }
- /**
- * Flattens an array into the given output variables.
- *
- * @param array $choices The array to flatten
- * @param callable $value The callable for generating choice values
- * @param array|null $choicesByValues The flattened choices indexed by the
- * corresponding values
- * @param array|null $keysByValues The original keys indexed by the
- * corresponding values
- * @param array|null $structuredValues The values indexed by the original keys
- *
- * @internal
- */
- protected function flatten(array $choices, callable $value, ?array &$choicesByValues, ?array &$keysByValues, ?array &$structuredValues): void
- {
- if (null === $choicesByValues) {
- $choicesByValues = [];
- $keysByValues = [];
- $structuredValues = [];
- }
- foreach ($choices as $key => $choice) {
- if (\is_array($choice)) {
- $this->flatten($choice, $value, $choicesByValues, $keysByValues, $structuredValues[$key]);
- continue;
- }
- $choiceValue = (string) $value($choice);
- $choicesByValues[$choiceValue] = $choice;
- $keysByValues[$choiceValue] = $key;
- $structuredValues[$key] = $choiceValue;
- }
- }
- /**
- * Checks whether the given choices can be cast to strings without
- * generating duplicates.
- * This method is responsible for preventing conflict between scalar values
- * and the empty value.
- */
- private function castableToString(array $choices, array &$cache = []): bool
- {
- foreach ($choices as $choice) {
- if (\is_array($choice)) {
- if (!$this->castableToString($choice, $cache)) {
- return false;
- }
- continue;
- } elseif (!\is_scalar($choice)) {
- return false;
- }
- // prevent having false casted to the empty string by isset()
- $choice = false === $choice ? '0' : (string) $choice;
- if (isset($cache[$choice])) {
- return false;
- }
- $cache[$choice] = true;
- }
- return true;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/CachingFactoryDecorator.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory;
- use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
- use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
- use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
- use Symfony\Contracts\Service\ResetInterface;
- /**
- * Caches the choice lists created by the decorated factory.
- *
- * To cache a list based on its options, arguments must be decorated
- * by a {@see Cache\AbstractStaticOption} implementation.
- *
- * @author Bernhard Schussek <[email protected]>
- * @author Jules Pietri <[email protected]>
- */
- class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterface
- {
- /**
- * @var ChoiceListInterface[]
- */
- private array $lists = [];
- /**
- * @var ChoiceListView[]
- */
- private array $views = [];
- /**
- * Generates a SHA-256 hash for the given value.
- *
- * Optionally, a namespace string can be passed. Calling this method will
- * the same values, but different namespaces, will return different hashes.
- *
- * @return string The SHA-256 hash
- *
- * @internal
- */
- public static function generateHash(mixed $value, string $namespace = ''): string
- {
- if (\is_object($value)) {
- $value = spl_object_hash($value);
- } elseif (\is_array($value)) {
- array_walk_recursive($value, static function (&$v) {
- if (\is_object($v)) {
- $v = spl_object_hash($v);
- }
- });
- }
- return hash('sha256', $namespace.':'.serialize($value));
- }
- public function __construct(
- private ChoiceListFactoryInterface $decoratedFactory,
- ) {
- }
- /**
- * Returns the decorated factory.
- */
- public function getDecoratedFactory(): ChoiceListFactoryInterface
- {
- return $this->decoratedFactory;
- }
- public function createListFromChoices(iterable $choices, mixed $value = null, mixed $filter = null): ChoiceListInterface
- {
- if ($choices instanceof \Traversable) {
- $choices = iterator_to_array($choices);
- }
- $cache = true;
- // Only cache per value and filter when needed. The value is not validated on purpose.
- // The decorated factory may decide which values to accept and which not.
- if ($value instanceof Cache\ChoiceValue) {
- $value = $value->getOption();
- } elseif ($value) {
- $cache = false;
- }
- if ($filter instanceof Cache\ChoiceFilter) {
- $filter = $filter->getOption();
- } elseif ($filter) {
- $cache = false;
- }
- if (!$cache) {
- return $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
- }
- $hash = self::generateHash([$choices, $value, $filter], 'fromChoices');
- if (!isset($this->lists[$hash])) {
- $this->lists[$hash] = $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
- }
- return $this->lists[$hash];
- }
- public function createListFromLoader(ChoiceLoaderInterface $loader, mixed $value = null, mixed $filter = null): ChoiceListInterface
- {
- $cache = true;
- if ($loader instanceof Cache\ChoiceLoader) {
- $loader = $loader->getOption();
- } else {
- $cache = false;
- }
- if ($value instanceof Cache\ChoiceValue) {
- $value = $value->getOption();
- } elseif ($value) {
- $cache = false;
- }
- if ($filter instanceof Cache\ChoiceFilter) {
- $filter = $filter->getOption();
- } elseif ($filter) {
- $cache = false;
- }
- if (!$cache) {
- return $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
- }
- $hash = self::generateHash([$loader, $value, $filter], 'fromLoader');
- if (!isset($this->lists[$hash])) {
- $this->lists[$hash] = $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
- }
- return $this->lists[$hash];
- }
- 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
- {
- $cache = true;
- if ($preferredChoices instanceof Cache\PreferredChoice) {
- $preferredChoices = $preferredChoices->getOption();
- } elseif ($preferredChoices) {
- $cache = false;
- }
- if ($label instanceof Cache\ChoiceLabel) {
- $label = $label->getOption();
- } elseif (null !== $label) {
- $cache = false;
- }
- if ($index instanceof Cache\ChoiceFieldName) {
- $index = $index->getOption();
- } elseif ($index) {
- $cache = false;
- }
- if ($groupBy instanceof Cache\GroupBy) {
- $groupBy = $groupBy->getOption();
- } elseif ($groupBy) {
- $cache = false;
- }
- if ($attr instanceof Cache\ChoiceAttr) {
- $attr = $attr->getOption();
- } elseif ($attr) {
- $cache = false;
- }
- if ($labelTranslationParameters instanceof Cache\ChoiceTranslationParameters) {
- $labelTranslationParameters = $labelTranslationParameters->getOption();
- } elseif ([] !== $labelTranslationParameters) {
- $cache = false;
- }
- if (!$cache) {
- return $this->decoratedFactory->createView(
- $list,
- $preferredChoices,
- $label,
- $index,
- $groupBy,
- $attr,
- $labelTranslationParameters,
- $duplicatePreferredChoices,
- );
- }
- $hash = self::generateHash([$list, $preferredChoices, $label, $index, $groupBy, $attr, $labelTranslationParameters, $duplicatePreferredChoices]);
- if (!isset($this->views[$hash])) {
- $this->views[$hash] = $this->decoratedFactory->createView(
- $list,
- $preferredChoices,
- $label,
- $index,
- $groupBy,
- $attr,
- $labelTranslationParameters,
- $duplicatePreferredChoices,
- );
- }
- return $this->views[$hash];
- }
- public function reset(): void
- {
- $this->lists = [];
- $this->views = [];
- Cache\AbstractStaticOption::reset();
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/Cache/GroupBy.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
- * which configures a "group_by" callback.
- *
- * @internal
- *
- * @author Jules Pietri <[email protected]>
- */
- final class GroupBy extends AbstractStaticOption
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/Cache/ChoiceLabel.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
- * which configures a "choice_label" option.
- *
- * @internal
- *
- * @author Jules Pietri <[email protected]>
- */
- final class ChoiceLabel extends AbstractStaticOption
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/Cache/ChoiceAttr.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
- * which configures a "choice_attr" option.
- *
- * @internal
- *
- * @author Jules Pietri <[email protected]>
- */
- final class ChoiceAttr extends AbstractStaticOption
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/Cache/ChoiceFilter.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
- * which configures a "choice_filter" option.
- *
- * @internal
- *
- * @author Jules Pietri <[email protected]>
- */
- final class ChoiceFilter extends AbstractStaticOption
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/Cache/PreferredChoice.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
- * which configures a "preferred_choices" option.
- *
- * @internal
- *
- * @author Jules Pietri <[email protected]>
- */
- final class PreferredChoice extends AbstractStaticOption
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/Cache/ChoiceFieldName.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
- * which configures a "choice_name" callback.
- *
- * @internal
- *
- * @author Jules Pietri <[email protected]>
- */
- final class ChoiceFieldName extends AbstractStaticOption
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/Cache/ChoiceLoader.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
- use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
- use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A cacheable wrapper for {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
- * which configures a "choice_loader" option.
- *
- * @internal
- *
- * @author Jules Pietri <[email protected]>
- */
- final class ChoiceLoader extends AbstractStaticOption implements ChoiceLoaderInterface
- {
- public function loadChoiceList(?callable $value = null): ChoiceListInterface
- {
- return $this->getOption()->loadChoiceList($value);
- }
- public function loadChoicesForValues(array $values, ?callable $value = null): array
- {
- return $this->getOption()->loadChoicesForValues($values, $value);
- }
- public function loadValuesForChoices(array $choices, ?callable $value = null): array
- {
- return $this->getOption()->loadValuesForChoices($choices, $value);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/Cache/AbstractStaticOption.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
- use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
- use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
- use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A template decorator for static {@see ChoiceType} options.
- *
- * Used as fly weight for {@see CachingFactoryDecorator}.
- *
- * @internal
- *
- * @author Jules Pietri <[email protected]>
- */
- abstract class AbstractStaticOption
- {
- private static array $options = [];
- private bool|string|array|\Closure|ChoiceLoaderInterface $option;
- /**
- * @param mixed $option Any pseudo callable, array, string or bool to define a choice list option
- * @param mixed $vary Dynamic data used to compute a unique hash when caching the option
- */
- final public function __construct(FormTypeInterface|FormTypeExtensionInterface $formType, mixed $option, mixed $vary = null)
- {
- $hash = CachingFactoryDecorator::generateHash([static::class, $formType, $vary]);
- $this->option = self::$options[$hash] ??= $option instanceof \Closure || \is_string($option) || \is_bool($option) || $option instanceof ChoiceLoaderInterface || !\is_callable($option) ? $option : $option(...);
- }
- final public function getOption(): mixed
- {
- return $this->option;
- }
- final public static function reset(): void
- {
- self::$options = [];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/Cache/ChoiceValue.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
- * which configures a "choice_value" callback.
- *
- * @internal
- *
- * @author Jules Pietri <[email protected]>
- */
- final class ChoiceValue extends AbstractStaticOption
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/Cache/ChoiceTranslationParameters.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
- * which configures a "choice_translation_parameters" option.
- *
- * @internal
- *
- * @author Vincent Langlet <[email protected]>
- */
- final class ChoiceTranslationParameters extends AbstractStaticOption
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/DefaultChoiceListFactory.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory;
- use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
- use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
- use Symfony\Component\Form\ChoiceList\LazyChoiceList;
- use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
- use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
- use Symfony\Component\Form\ChoiceList\Loader\FilterChoiceLoaderDecorator;
- use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
- use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
- use Symfony\Component\Form\ChoiceList\View\ChoiceView;
- use Symfony\Contracts\Translation\TranslatableInterface;
- /**
- * Default implementation of {@link ChoiceListFactoryInterface}.
- *
- * @author Bernhard Schussek <[email protected]>
- * @author Jules Pietri <[email protected]>
- */
- class DefaultChoiceListFactory implements ChoiceListFactoryInterface
- {
- public function createListFromChoices(iterable $choices, ?callable $value = null, ?callable $filter = null): ChoiceListInterface
- {
- if ($filter) {
- // filter the choice list lazily
- return $this->createListFromLoader(new FilterChoiceLoaderDecorator(
- new CallbackChoiceLoader(static fn () => $choices),
- $filter
- ), $value);
- }
- return new ArrayChoiceList($choices, $value);
- }
- public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $value = null, ?callable $filter = null): ChoiceListInterface
- {
- if ($filter) {
- $loader = new FilterChoiceLoaderDecorator($loader, $filter);
- }
- return new LazyChoiceList($loader, $value);
- }
- 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
- {
- $preferredViews = [];
- $preferredViewsOrder = [];
- $otherViews = [];
- $choices = $list->getChoices();
- $keys = $list->getOriginalKeys();
- if (!\is_callable($preferredChoices)) {
- if (!$preferredChoices) {
- $preferredChoices = null;
- } else {
- // make sure we have keys that reflect order
- $preferredChoices = array_values($preferredChoices);
- $preferredChoices = static fn ($choice) => array_search($choice, $preferredChoices, true);
- }
- }
- // The names are generated from an incrementing integer by default
- $index ??= 0;
- // If $groupBy is a callable returning a string
- // choices are added to the group with the name returned by the callable.
- // If $groupBy is a callable returning an array
- // choices are added to the groups with names returned by the callable
- // If the callable returns null, the choice is not added to any group
- if (\is_callable($groupBy)) {
- foreach ($choices as $value => $choice) {
- self::addChoiceViewsGroupedByCallable(
- $groupBy,
- $choice,
- $value,
- $label,
- $keys,
- $index,
- $attr,
- $labelTranslationParameters,
- $preferredChoices,
- $preferredViews,
- $preferredViewsOrder,
- $otherViews,
- $duplicatePreferredChoices,
- );
- }
- // Remove empty group views that may have been created by
- // addChoiceViewsGroupedByCallable()
- foreach ($preferredViews as $key => $view) {
- if ($view instanceof ChoiceGroupView && 0 === \count($view->choices)) {
- unset($preferredViews[$key]);
- }
- }
- foreach ($otherViews as $key => $view) {
- if ($view instanceof ChoiceGroupView && 0 === \count($view->choices)) {
- unset($otherViews[$key]);
- }
- }
- foreach ($preferredViewsOrder as $key => $groupViewsOrder) {
- if ($groupViewsOrder) {
- $preferredViewsOrder[$key] = min($groupViewsOrder);
- } else {
- unset($preferredViewsOrder[$key]);
- }
- }
- } else {
- // Otherwise use the original structure of the choices
- self::addChoiceViewsFromStructuredValues(
- $list->getStructuredValues(),
- $label,
- $choices,
- $keys,
- $index,
- $attr,
- $labelTranslationParameters,
- $preferredChoices,
- $preferredViews,
- $preferredViewsOrder,
- $otherViews,
- $duplicatePreferredChoices,
- );
- }
- uksort($preferredViews, static fn ($a, $b) => isset($preferredViewsOrder[$a], $preferredViewsOrder[$b]) ? $preferredViewsOrder[$a] <=> $preferredViewsOrder[$b] : 0);
- return new ChoiceListView($otherViews, $preferredViews);
- }
- private static function addChoiceView($choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews, bool $duplicatePreferredChoices): void
- {
- // $value may be an integer or a string, since it's stored in the array
- // keys. We want to guarantee it's a string though.
- $key = $keys[$value];
- $nextIndex = \is_int($index) ? $index++ : $index($choice, $key, $value);
- // BC normalize label to accept a false value
- if (null === $label) {
- // If the labels are null, use the original choice key by default
- $label = (string) $key;
- } elseif (false !== $label) {
- // If "choice_label" is set to false and "expanded" is true, the value false
- // should be passed on to the "label" option of the checkboxes/radio buttons
- $dynamicLabel = $label($choice, $key, $value);
- if (false === $dynamicLabel) {
- $label = false;
- } elseif ($dynamicLabel instanceof TranslatableInterface) {
- $label = $dynamicLabel;
- } else {
- $label = (string) $dynamicLabel;
- }
- }
- $view = new ChoiceView(
- $choice,
- $value,
- $label,
- // The attributes may be a callable or a mapping from choice indices
- // to nested arrays
- \is_callable($attr) ? $attr($choice, $key, $value) : ($attr[$key] ?? []),
- // The label translation parameters may be a callable or a mapping from choice indices
- // to nested arrays
- \is_callable($labelTranslationParameters) ? $labelTranslationParameters($choice, $key, $value) : ($labelTranslationParameters[$key] ?? [])
- );
- // $isPreferred may be null if no choices are preferred
- if (null !== $isPreferred && false !== $preferredKey = $isPreferred($choice, $key, $value)) {
- $preferredViews[$nextIndex] = $view;
- $preferredViewsOrder[$nextIndex] = $preferredKey;
- if ($duplicatePreferredChoices) {
- $otherViews[$nextIndex] = $view;
- }
- } else {
- $otherViews[$nextIndex] = $view;
- }
- }
- 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
- {
- foreach ($values as $key => $value) {
- if (null === $value) {
- continue;
- }
- // Add the contents of groups to new ChoiceGroupView instances
- if (\is_array($value)) {
- $preferredViewsForGroup = [];
- $otherViewsForGroup = [];
- self::addChoiceViewsFromStructuredValues(
- $value,
- $label,
- $choices,
- $keys,
- $index,
- $attr,
- $labelTranslationParameters,
- $isPreferred,
- $preferredViewsForGroup,
- $preferredViewsOrder,
- $otherViewsForGroup,
- $duplicatePreferredChoices,
- );
- if (\count($preferredViewsForGroup) > 0) {
- $preferredViews[$key] = new ChoiceGroupView($key, $preferredViewsForGroup);
- }
- if (\count($otherViewsForGroup) > 0) {
- $otherViews[$key] = new ChoiceGroupView($key, $otherViewsForGroup);
- }
- continue;
- }
- // Add ungrouped items directly
- self::addChoiceView(
- $choices[$value],
- $value,
- $label,
- $keys,
- $index,
- $attr,
- $labelTranslationParameters,
- $isPreferred,
- $preferredViews,
- $preferredViewsOrder,
- $otherViews,
- $duplicatePreferredChoices,
- );
- }
- }
- 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
- {
- $groupLabels = $groupBy($choice, $keys[$value], $value);
- if (null === $groupLabels) {
- // If the callable returns null, don't group the choice
- self::addChoiceView(
- $choice,
- $value,
- $label,
- $keys,
- $index,
- $attr,
- $labelTranslationParameters,
- $isPreferred,
- $preferredViews,
- $preferredViewsOrder,
- $otherViews,
- $duplicatePreferredChoices,
- );
- return;
- }
- $groupLabels = \is_array($groupLabels) ? array_map('strval', $groupLabels) : [(string) $groupLabels];
- foreach ($groupLabels as $groupLabel) {
- // Initialize the group views if necessary. Unnecessarily built group
- // views will be cleaned up at the end of createView()
- if (!isset($preferredViews[$groupLabel])) {
- $preferredViews[$groupLabel] = new ChoiceGroupView($groupLabel);
- $otherViews[$groupLabel] = new ChoiceGroupView($groupLabel);
- }
- if (!isset($preferredViewsOrder[$groupLabel])) {
- $preferredViewsOrder[$groupLabel] = [];
- }
- self::addChoiceView(
- $choice,
- $value,
- $label,
- $keys,
- $index,
- $attr,
- $labelTranslationParameters,
- $isPreferred,
- $preferredViews[$groupLabel]->choices,
- $preferredViewsOrder[$groupLabel],
- $otherViews[$groupLabel]->choices,
- $duplicatePreferredChoices,
- );
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/ChoiceListFactoryInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory;
- use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
- use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
- use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
- /**
- * Creates {@link ChoiceListInterface} instances.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface ChoiceListFactoryInterface
- {
- /**
- * Creates a choice list for the given choices.
- *
- * The choices should be passed in the values of the choices array.
- *
- * Optionally, a callable can be passed for generating the choice values.
- * The callable receives the choice as only argument.
- * Null may be passed when the choice list contains the empty value.
- *
- * @param callable|null $filter The callable filtering the choices
- */
- public function createListFromChoices(iterable $choices, ?callable $value = null, ?callable $filter = null): ChoiceListInterface;
- /**
- * Creates a choice list that is loaded with the given loader.
- *
- * Optionally, a callable can be passed for generating the choice values.
- * The callable receives the choice as only argument.
- * Null may be passed when the choice list contains the empty value.
- *
- * @param callable|null $filter The callable filtering the choices
- */
- public function createListFromLoader(ChoiceLoaderInterface $loader, ?callable $value = null, ?callable $filter = null): ChoiceListInterface;
- /**
- * Creates a view for the given choice list.
- *
- * Callables may be passed for all optional arguments. The callables receive
- * the choice as first and the array key as the second argument.
- *
- * * The callable for the label and the name should return the generated
- * label/choice name.
- * * The callable for the preferred choices should return true or false,
- * depending on whether the choice should be preferred or not.
- * * The callable for the grouping should return the group name or null if
- * a choice should not be grouped.
- * * The callable for the attributes should return an array of HTML
- * attributes that will be inserted in the tag of the choice.
- *
- * If no callable is passed, the labels will be generated from the choice
- * keys. The view indices will be generated using an incrementing integer
- * by default.
- *
- * The preferred choices can also be passed as array. Each choice that is
- * contained in that array will be marked as preferred.
- *
- * The attributes can be passed as multi-dimensional array. The keys should
- * match the keys of the choices. The values should be arrays of HTML
- * attributes that should be added to the respective choice.
- *
- * @param array|callable|null $preferredChoices The preferred choices
- * @param callable|false|null $label The callable generating the choice labels;
- * pass false to discard the label
- * @param array|callable|null $attr The callable generating the HTML attributes
- * @param array|callable $labelTranslationParameters The parameters used to translate the choice labels
- * @param bool $duplicatePreferredChoices Whether the preferred choices should be duplicated
- * on top of the list and in their original position
- * or only in the top of the list
- */
- 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;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/Factory/PropertyAccessDecorator.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\Factory;
- use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
- use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
- use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
- use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
- use Symfony\Component\PropertyAccess\PropertyAccess;
- use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
- use Symfony\Component\PropertyAccess\PropertyPath;
- use Symfony\Component\PropertyAccess\PropertyPathInterface;
- /**
- * Adds property path support to a choice list factory.
- *
- * Pass the decorated factory to the constructor:
- *
- * $decorator = new PropertyAccessDecorator($factory);
- *
- * You can now pass property paths for generating choice values, labels, view
- * indices, HTML attributes and for determining the preferred choices and the
- * choice groups:
- *
- * // extract values from the $value property
- * $list = $createListFromChoices($objects, 'value');
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class PropertyAccessDecorator implements ChoiceListFactoryInterface
- {
- private PropertyAccessorInterface $propertyAccessor;
- public function __construct(
- private ChoiceListFactoryInterface $decoratedFactory,
- ?PropertyAccessorInterface $propertyAccessor = null,
- ) {
- $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
- }
- /**
- * Returns the decorated factory.
- */
- public function getDecoratedFactory(): ChoiceListFactoryInterface
- {
- return $this->decoratedFactory;
- }
- public function createListFromChoices(iterable $choices, mixed $value = null, mixed $filter = null): ChoiceListInterface
- {
- if (\is_string($value)) {
- $value = new PropertyPath($value);
- }
- if ($value instanceof PropertyPathInterface) {
- $accessor = $this->propertyAccessor;
- // The callable may be invoked with a non-object/array value
- // when such values are passed to
- // ChoiceListInterface::getValuesForChoices(). Handle this case
- // so that the call to getValue() doesn't break.
- $value = static fn ($choice) => \is_object($choice) || \is_array($choice) ? $accessor->getValue($choice, $value) : null;
- }
- if (\is_string($filter)) {
- $filter = new PropertyPath($filter);
- }
- if ($filter instanceof PropertyPath) {
- $accessor = $this->propertyAccessor;
- $filter = static fn ($choice) => (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter);
- }
- return $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
- }
- public function createListFromLoader(ChoiceLoaderInterface $loader, mixed $value = null, mixed $filter = null): ChoiceListInterface
- {
- if (\is_string($value)) {
- $value = new PropertyPath($value);
- }
- if ($value instanceof PropertyPathInterface) {
- $accessor = $this->propertyAccessor;
- // The callable may be invoked with a non-object/array value
- // when such values are passed to
- // ChoiceListInterface::getValuesForChoices(). Handle this case
- // so that the call to getValue() doesn't break.
- $value = static fn ($choice) => \is_object($choice) || \is_array($choice) ? $accessor->getValue($choice, $value) : null;
- }
- if (\is_string($filter)) {
- $filter = new PropertyPath($filter);
- }
- if ($filter instanceof PropertyPath) {
- $accessor = $this->propertyAccessor;
- $filter = static fn ($choice) => (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter);
- }
- return $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
- }
- 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
- {
- $accessor = $this->propertyAccessor;
- if (\is_string($label)) {
- $label = new PropertyPath($label);
- }
- if ($label instanceof PropertyPathInterface) {
- $label = static fn ($choice) => $accessor->getValue($choice, $label);
- }
- if (\is_string($preferredChoices)) {
- $preferredChoices = new PropertyPath($preferredChoices);
- }
- if ($preferredChoices instanceof PropertyPathInterface) {
- $preferredChoices = static function ($choice) use ($accessor, $preferredChoices) {
- try {
- return $accessor->getValue($choice, $preferredChoices);
- } catch (UnexpectedTypeException) {
- // Assume not preferred if not readable
- return false;
- }
- };
- }
- if (\is_string($index)) {
- $index = new PropertyPath($index);
- }
- if ($index instanceof PropertyPathInterface) {
- $index = static fn ($choice) => $accessor->getValue($choice, $index);
- }
- if (\is_string($groupBy)) {
- $groupBy = new PropertyPath($groupBy);
- }
- if ($groupBy instanceof PropertyPathInterface) {
- $groupBy = static function ($choice) use ($accessor, $groupBy) {
- try {
- return $accessor->getValue($choice, $groupBy);
- } catch (UnexpectedTypeException) {
- // Don't group if path is not readable
- return null;
- }
- };
- }
- if (\is_string($attr)) {
- $attr = new PropertyPath($attr);
- }
- if ($attr instanceof PropertyPathInterface) {
- $attr = static fn ($choice) => $accessor->getValue($choice, $attr);
- }
- if (\is_string($labelTranslationParameters)) {
- $labelTranslationParameters = new PropertyPath($labelTranslationParameters);
- }
- if ($labelTranslationParameters instanceof PropertyPath) {
- $labelTranslationParameters = static fn ($choice) => $accessor->getValue($choice, $labelTranslationParameters);
- }
- return $this->decoratedFactory->createView(
- $list,
- $preferredChoices,
- $label,
- $index,
- $groupBy,
- $attr,
- $labelTranslationParameters,
- $duplicatePreferredChoices,
- );
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/View/ChoiceView.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\View;
- use Symfony\Contracts\Translation\TranslatableInterface;
- /**
- * Represents a choice in templates.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ChoiceView
- {
- /**
- * Creates a new choice view.
- *
- * @param mixed $data The original choice
- * @param string $value The view representation of the choice
- * @param string|TranslatableInterface|false $label The label displayed to humans; pass false to discard the label
- * @param array $attr Additional attributes for the HTML tag
- * @param array $labelTranslationParameters Additional parameters used to translate the label
- */
- public function __construct(
- public mixed $data,
- public string $value,
- public string|TranslatableInterface|false $label,
- public array $attr = [],
- public array $labelTranslationParameters = [],
- ) {
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/View/ChoiceListView.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\View;
- /**
- * Represents a choice list in templates.
- *
- * A choice list contains choices and optionally preferred choices which are
- * displayed in the very beginning of the list. Both choices and preferred
- * choices may be grouped in {@link ChoiceGroupView} instances.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ChoiceListView
- {
- /**
- * Creates a new choice list view.
- *
- * @param array<ChoiceGroupView|ChoiceView> $choices The choice views
- * @param array<ChoiceGroupView|ChoiceView> $preferredChoices the preferred choice views
- */
- public function __construct(
- public array $choices = [],
- public array $preferredChoices = [],
- ) {
- }
- /**
- * Returns whether a placeholder is in the choices.
- *
- * A placeholder must be the first child element, not be in a group and have an empty value.
- */
- public function hasPlaceholder(): bool
- {
- if ($this->preferredChoices) {
- $firstChoice = reset($this->preferredChoices);
- return $firstChoice instanceof ChoiceView && '' === $firstChoice->value;
- }
- $firstChoice = reset($this->choices);
- return $firstChoice instanceof ChoiceView && '' === $firstChoice->value;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ChoiceList/View/ChoiceGroupView.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\ChoiceList\View;
- /**
- * Represents a group of choices in templates.
- *
- * @author Bernhard Schussek <[email protected]>
- *
- * @implements \IteratorAggregate<array-key, ChoiceGroupView|ChoiceView>
- */
- class ChoiceGroupView implements \IteratorAggregate
- {
- /**
- * Creates a new choice group view.
- *
- * @param array<ChoiceGroupView|ChoiceView> $choices the choice views in the group
- */
- public function __construct(
- public string $label,
- public array $choices = [],
- ) {
- }
- /**
- * @return \Traversable<array-key, ChoiceGroupView|ChoiceView>
- */
- public function getIterator(): \Traversable
- {
- return new \ArrayIterator($this->choices);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./RequestHandlerInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * Submits forms if they were submitted.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface RequestHandlerInterface
- {
- /**
- * Submits a form if it was submitted.
- */
- public function handleRequest(FormInterface $form, mixed $request = null): void;
- /**
- * Returns true if the given data is a file upload.
- */
- public function isFileUpload(mixed $data): bool;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Guess/TypeGuess.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Guess;
- /**
- * Contains a guessed class name and a list of options for creating an instance
- * of that class.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class TypeGuess extends Guess
- {
- /**
- * @param string $type The guessed field type
- * @param array $options The options for creating instances of the
- * guessed class
- * @param int $confidence The confidence that the guessed class name
- * is correct
- */
- public function __construct(
- private string $type,
- private array $options,
- int $confidence,
- ) {
- parent::__construct($confidence);
- }
- /**
- * Returns the guessed field type.
- */
- public function getType(): string
- {
- return $this->type;
- }
- /**
- * Returns the guessed options for creating instances of the guessed type.
- */
- public function getOptions(): array
- {
- return $this->options;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Guess/Guess.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Guess;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- /**
- * Base class for guesses made by TypeGuesserInterface implementation.
- *
- * Each instance contains a confidence value about the correctness of the guess.
- * Thus an instance with confidence HIGH_CONFIDENCE is more likely to be
- * correct than an instance with confidence LOW_CONFIDENCE.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- abstract class Guess
- {
- /**
- * Marks an instance with a value that is extremely likely to be correct.
- */
- public const VERY_HIGH_CONFIDENCE = 3;
- /**
- * Marks an instance with a value that is very likely to be correct.
- */
- public const HIGH_CONFIDENCE = 2;
- /**
- * Marks an instance with a value that is likely to be correct.
- */
- public const MEDIUM_CONFIDENCE = 1;
- /**
- * Marks an instance with a value that may be correct.
- */
- public const LOW_CONFIDENCE = 0;
- /**
- * The confidence about the correctness of the value.
- *
- * One of VERY_HIGH_CONFIDENCE, HIGH_CONFIDENCE, MEDIUM_CONFIDENCE
- * and LOW_CONFIDENCE.
- */
- private int $confidence;
- /**
- * Returns the guess most likely to be correct from a list of guesses.
- *
- * If there are multiple guesses with the same, highest confidence, the
- * returned guess is any of them.
- *
- * @param static[] $guesses An array of guesses
- */
- public static function getBestGuess(array $guesses): ?static
- {
- $result = null;
- $maxConfidence = -1;
- foreach ($guesses as $guess) {
- if ($maxConfidence < $confidence = $guess->getConfidence()) {
- $maxConfidence = $confidence;
- $result = $guess;
- }
- }
- return $result;
- }
- /**
- * @throws InvalidArgumentException if the given value of confidence is unknown
- */
- public function __construct(int $confidence)
- {
- if (self::VERY_HIGH_CONFIDENCE !== $confidence && self::HIGH_CONFIDENCE !== $confidence
- && self::MEDIUM_CONFIDENCE !== $confidence && self::LOW_CONFIDENCE !== $confidence) {
- throw new InvalidArgumentException('The confidence should be one of the constants defined in Guess.');
- }
- $this->confidence = $confidence;
- }
- /**
- * Returns the confidence that the guessed value is correct.
- *
- * @return int One of the constants VERY_HIGH_CONFIDENCE, HIGH_CONFIDENCE,
- * MEDIUM_CONFIDENCE and LOW_CONFIDENCE
- */
- public function getConfidence(): int
- {
- return $this->confidence;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Guess/ValueGuess.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Guess;
- /**
- * Contains a guessed value.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ValueGuess extends Guess
- {
- /**
- * @param int $confidence The confidence that the guessed class name is correct
- */
- public function __construct(
- private string|int|bool|null $value,
- int $confidence,
- ) {
- parent::__construct($confidence);
- }
- /**
- * Returns the guessed value.
- */
- public function getValue(): string|int|bool|null
- {
- return $this->value;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormFactoryBuilder.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Extension\Core\CoreExtension;
- /**
- * The default implementation of FormFactoryBuilderInterface.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class FormFactoryBuilder implements FormFactoryBuilderInterface
- {
- private ResolvedFormTypeFactoryInterface $resolvedTypeFactory;
- /**
- * @var FormExtensionInterface[]
- */
- private array $extensions = [];
- /**
- * @var FormTypeInterface[]
- */
- private array $types = [];
- /**
- * @var FormTypeExtensionInterface[][]
- */
- private array $typeExtensions = [];
- /**
- * @var FormTypeGuesserInterface[]
- */
- private array $typeGuessers = [];
- public function __construct(
- private bool $forceCoreExtension = false,
- ) {
- }
- public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory): static
- {
- $this->resolvedTypeFactory = $resolvedTypeFactory;
- return $this;
- }
- public function addExtension(FormExtensionInterface $extension): static
- {
- $this->extensions[] = $extension;
- return $this;
- }
- public function addExtensions(array $extensions): static
- {
- $this->extensions = array_merge($this->extensions, $extensions);
- return $this;
- }
- public function addType(FormTypeInterface $type): static
- {
- $this->types[] = $type;
- return $this;
- }
- public function addTypes(array $types): static
- {
- foreach ($types as $type) {
- $this->types[] = $type;
- }
- return $this;
- }
- public function addTypeExtension(FormTypeExtensionInterface $typeExtension): static
- {
- foreach ($typeExtension::getExtendedTypes() as $extendedType) {
- $this->typeExtensions[$extendedType][] = $typeExtension;
- }
- return $this;
- }
- public function addTypeExtensions(array $typeExtensions): static
- {
- foreach ($typeExtensions as $typeExtension) {
- $this->addTypeExtension($typeExtension);
- }
- return $this;
- }
- public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser): static
- {
- $this->typeGuessers[] = $typeGuesser;
- return $this;
- }
- public function addTypeGuessers(array $typeGuessers): static
- {
- $this->typeGuessers = array_merge($this->typeGuessers, $typeGuessers);
- return $this;
- }
- public function getFormFactory(): FormFactoryInterface
- {
- $extensions = $this->extensions;
- if ($this->forceCoreExtension) {
- $hasCoreExtension = false;
- foreach ($extensions as $extension) {
- if ($extension instanceof CoreExtension) {
- $hasCoreExtension = true;
- break;
- }
- }
- if (!$hasCoreExtension) {
- array_unshift($extensions, new CoreExtension());
- }
- }
- if (\count($this->types) > 0 || \count($this->typeExtensions) > 0 || \count($this->typeGuessers) > 0) {
- if (\count($this->typeGuessers) > 1) {
- $typeGuesser = new FormTypeGuesserChain($this->typeGuessers);
- } else {
- $typeGuesser = $this->typeGuessers[0] ?? null;
- }
- $extensions[] = new PreloadedExtension($this->types, $this->typeExtensions, $typeGuesser);
- }
- $registry = new FormRegistry($extensions, $this->resolvedTypeFactory ?? new ResolvedFormTypeFactory());
- return new FormFactory($registry);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./AbstractExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- abstract class AbstractExtension implements FormExtensionInterface
- {
- /**
- * The types provided by this extension.
- *
- * @var FormTypeInterface[]
- */
- private array $types;
- /**
- * The type extensions provided by this extension.
- *
- * @var FormTypeExtensionInterface[][]
- */
- private array $typeExtensions;
- /**
- * The type guesser provided by this extension.
- */
- private ?FormTypeGuesserInterface $typeGuesser = null;
- /**
- * Whether the type guesser has been loaded.
- */
- private bool $typeGuesserLoaded = false;
- public function getType(string $name): FormTypeInterface
- {
- if (!isset($this->types)) {
- $this->initTypes();
- }
- if (!isset($this->types[$name])) {
- throw new InvalidArgumentException(\sprintf('The type "%s" cannot be loaded by this extension.', $name));
- }
- return $this->types[$name];
- }
- public function hasType(string $name): bool
- {
- if (!isset($this->types)) {
- $this->initTypes();
- }
- return isset($this->types[$name]);
- }
- public function getTypeExtensions(string $name): array
- {
- if (!isset($this->typeExtensions)) {
- $this->initTypeExtensions();
- }
- return $this->typeExtensions[$name]
- ?? [];
- }
- public function hasTypeExtensions(string $name): bool
- {
- if (!isset($this->typeExtensions)) {
- $this->initTypeExtensions();
- }
- return isset($this->typeExtensions[$name]) && \count($this->typeExtensions[$name]) > 0;
- }
- public function getTypeGuesser(): ?FormTypeGuesserInterface
- {
- if (!$this->typeGuesserLoaded) {
- $this->initTypeGuesser();
- }
- return $this->typeGuesser;
- }
- /**
- * Registers the types.
- *
- * @return FormTypeInterface[]
- */
- protected function loadTypes(): array
- {
- return [];
- }
- /**
- * Registers the type extensions.
- *
- * @return FormTypeExtensionInterface[]
- */
- protected function loadTypeExtensions(): array
- {
- return [];
- }
- /**
- * Registers the type guesser.
- */
- protected function loadTypeGuesser(): ?FormTypeGuesserInterface
- {
- return null;
- }
- /**
- * Initializes the types.
- *
- * @throws UnexpectedTypeException if any registered type is not an instance of FormTypeInterface
- */
- private function initTypes(): void
- {
- $this->types = [];
- foreach ($this->loadTypes() as $type) {
- if (!$type instanceof FormTypeInterface) {
- throw new UnexpectedTypeException($type, FormTypeInterface::class);
- }
- $this->types[$type::class] = $type;
- }
- }
- /**
- * Initializes the type extensions.
- *
- * @throws UnexpectedTypeException if any registered type extension is not
- * an instance of FormTypeExtensionInterface
- */
- private function initTypeExtensions(): void
- {
- $this->typeExtensions = [];
- foreach ($this->loadTypeExtensions() as $extension) {
- if (!$extension instanceof FormTypeExtensionInterface) {
- throw new UnexpectedTypeException($extension, FormTypeExtensionInterface::class);
- }
- foreach ($extension::getExtendedTypes() as $extendedType) {
- $this->typeExtensions[$extendedType][] = $extension;
- }
- }
- }
- /**
- * Initializes the type guesser.
- *
- * @throws UnexpectedTypeException if the type guesser is not an instance of FormTypeGuesserInterface
- */
- private function initTypeGuesser(): void
- {
- $this->typeGuesserLoaded = true;
- $this->typeGuesser = $this->loadTypeGuesser();
- if (null !== $this->typeGuesser && !$this->typeGuesser instanceof FormTypeGuesserInterface) {
- throw new UnexpectedTypeException($this->typeGuesser, FormTypeGuesserInterface::class);
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./NativeRequestHandler.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- use Symfony\Component\Form\Util\FormUtil;
- use Symfony\Component\Form\Util\ServerParams;
- /**
- * A request handler using PHP super globals $_GET, $_POST and $_SERVER.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class NativeRequestHandler implements RequestHandlerInterface
- {
- private ServerParams $serverParams;
- /**
- * The allowed keys of the $_FILES array.
- */
- private const FILE_KEYS = [
- 'error',
- 'full_path',
- 'name',
- 'size',
- 'tmp_name',
- 'type',
- ];
- public function __construct(?ServerParams $params = null)
- {
- $this->serverParams = $params ?? new ServerParams();
- }
- /**
- * @throws UnexpectedTypeException If the $request is not null
- */
- public function handleRequest(FormInterface $form, mixed $request = null): void
- {
- if (null !== $request) {
- throw new UnexpectedTypeException($request, 'null');
- }
- $name = $form->getName();
- $method = $form->getConfig()->getMethod();
- if ($method !== self::getRequestMethod()) {
- return;
- }
- // For request methods that must not have a request body we fetch data
- // from the query string. Otherwise we look for data in the request body.
- if ('GET' === $method || 'HEAD' === $method || 'TRACE' === $method) {
- if ('' === $name) {
- $data = $_GET;
- } else {
- // Don't submit GET requests if the form's name does not exist
- // in the request
- if (!isset($_GET[$name])) {
- return;
- }
- $data = $_GET[$name];
- }
- } else {
- // Mark the form with an error if the uploaded size was too large
- // This is done here and not in FormValidator because $_POST is
- // empty when that error occurs. Hence the form is never submitted.
- if ($this->serverParams->hasPostMaxSizeBeenExceeded()) {
- // Submit the form, but don't clear the default values
- $form->submit(null, false);
- $form->addError(new FormError(
- $form->getConfig()->getOption('upload_max_size_message')(),
- null,
- ['{{ max }}' => $this->serverParams->getNormalizedIniPostMaxSize()]
- ));
- return;
- }
- $fixedFiles = [];
- foreach ($_FILES as $fileKey => $file) {
- $fixedFiles[$fileKey] = self::stripEmptyFiles(self::fixPhpFilesArray($file));
- }
- if ('' === $name) {
- $params = $_POST;
- $files = $fixedFiles;
- } elseif (\array_key_exists($name, $_POST) || \array_key_exists($name, $fixedFiles)) {
- $default = $form->getConfig()->getCompound() ? [] : null;
- $params = \array_key_exists($name, $_POST) ? $_POST[$name] : $default;
- $files = \array_key_exists($name, $fixedFiles) ? $fixedFiles[$name] : $default;
- } else {
- // Don't submit the form if it is not present in the request
- return;
- }
- if (\is_array($params) && \is_array($files)) {
- $data = FormUtil::mergeParamsAndFiles($params, $files);
- } else {
- $data = $params ?: $files;
- }
- }
- // Don't auto-submit the form unless at least one field is present.
- if ('' === $name && \count(array_intersect_key($data, $form->all())) <= 0) {
- return;
- }
- if (\is_array($data) && \array_key_exists('_method', $data) && $method === $data['_method'] && !$form->has('_method')) {
- unset($data['_method']);
- }
- $form->submit($data, 'PATCH' !== $method);
- }
- public function isFileUpload(mixed $data): bool
- {
- // POST data will always be strings or arrays of strings. Thus, we can be sure
- // that the submitted data is a file upload if the "error" value is an integer
- // (this value must have been injected by PHP itself).
- return \is_array($data) && isset($data['error']) && \is_int($data['error']);
- }
- public function getUploadFileError(mixed $data): ?int
- {
- if (!\is_array($data)) {
- return null;
- }
- if (!isset($data['error'])) {
- return null;
- }
- if (!\is_int($data['error'])) {
- return null;
- }
- if (\UPLOAD_ERR_OK === $data['error']) {
- return null;
- }
- return $data['error'];
- }
- private static function getRequestMethod(): string
- {
- $method = isset($_SERVER['REQUEST_METHOD'])
- ? strtoupper($_SERVER['REQUEST_METHOD'])
- : 'GET';
- if ('POST' === $method && isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
- $method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
- }
- return $method;
- }
- /**
- * Fixes a malformed PHP $_FILES array.
- *
- * PHP has a bug that the format of the $_FILES array differs, depending on
- * whether the uploaded file fields had normal field names or array-like
- * field names ("normal" vs. "parent[child]").
- *
- * This method fixes the array to look like the "normal" $_FILES array.
- *
- * It's safe to pass an already converted array, in which case this method
- * just returns the original array unmodified.
- *
- * This method is identical to {@link \Symfony\Component\HttpFoundation\FileBag::fixPhpFilesArray}
- * and should be kept as such in order to port fixes quickly and easily.
- */
- private static function fixPhpFilesArray(mixed $data): mixed
- {
- if (!\is_array($data)) {
- return $data;
- }
- $keys = array_keys($data + ['full_path' => null]);
- sort($keys);
- if (self::FILE_KEYS !== $keys || !isset($data['name']) || !\is_array($data['name'])) {
- return $data;
- }
- $files = $data;
- foreach (self::FILE_KEYS as $k) {
- unset($files[$k]);
- }
- foreach ($data['name'] as $key => $name) {
- $files[$key] = self::fixPhpFilesArray([
- 'error' => $data['error'][$key],
- 'name' => $name,
- 'type' => $data['type'][$key],
- 'tmp_name' => $data['tmp_name'][$key],
- 'size' => $data['size'][$key],
- ] + (isset($data['full_path'][$key]) ? [
- 'full_path' => $data['full_path'][$key],
- ] : []));
- }
- return $files;
- }
- private static function stripEmptyFiles(mixed $data): mixed
- {
- if (!\is_array($data)) {
- return $data;
- }
- $keys = array_keys($data + ['full_path' => null]);
- sort($keys);
- if (self::FILE_KEYS === $keys) {
- if (\UPLOAD_ERR_NO_FILE === $data['error']) {
- return null;
- }
- return $data;
- }
- foreach ($data as $key => $value) {
- $data[$key] = self::stripEmptyFiles($value);
- }
- return $data;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./SubmitButton.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * A button that submits the form.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class SubmitButton extends Button implements ClickableInterface
- {
- private bool $clicked = false;
- public function isClicked(): bool
- {
- return $this->clicked;
- }
- /**
- * Submits data to the button.
- *
- * @return $this
- *
- * @throws Exception\AlreadySubmittedException if the form has already been submitted
- */
- public function submit(array|string|null $submittedData, bool $clearMissing = true): static
- {
- if ($this->getConfig()->getDisabled()) {
- $this->clicked = false;
- return $this;
- }
- parent::submit($submittedData, $clearMissing);
- $this->clicked = null !== $submittedData;
- return $this;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Test/Traits/ValidatorExtensionTrait.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Test\Traits;
- use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
- use Symfony\Component\Form\Test\TypeTestCase;
- use Symfony\Component\Validator\ConstraintViolationList;
- use Symfony\Component\Validator\Mapping\ClassMetadata;
- use Symfony\Component\Validator\Validator\ValidatorInterface;
- trait ValidatorExtensionTrait
- {
- protected ValidatorInterface $validator;
- protected function getValidatorExtension(): ValidatorExtension
- {
- if (!interface_exists(ValidatorInterface::class)) {
- throw new \Exception('In order to use the "ValidatorExtensionTrait", the symfony/validator component must be installed.');
- }
- if (!$this instanceof TypeTestCase) {
- throw new \Exception(\sprintf('The trait "ValidatorExtensionTrait" can only be added to a class that extends "%s".', TypeTestCase::class));
- }
- $this->validator = $this->createMock(ValidatorInterface::class);
- $metadata = $this->getMockBuilder(ClassMetadata::class)->setConstructorArgs([''])->onlyMethods(['addPropertyConstraint'])->getMock();
- $this->validator->expects($this->any())->method('getMetadataFor')->willReturn($metadata);
- $this->validator->expects($this->any())->method('validate')->willReturn(new ConstraintViolationList());
- return new ValidatorExtension($this->validator, false);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Test/FormPerformanceTestCase.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Test;
- /**
- * Base class for performance tests.
- *
- * Copied from Doctrine 2's OrmPerformanceTestCase.
- *
- * @author robo
- * @author Bernhard Schussek <[email protected]>
- */
- abstract class FormPerformanceTestCase extends FormIntegrationTestCase
- {
- private float $startTime;
- protected int $maxRunningTime = 0;
- protected function setUp(): void
- {
- parent::setUp();
- $this->startTime = microtime(true);
- }
- protected function assertPostConditions(): void
- {
- parent::assertPostConditions();
- $time = microtime(true) - $this->startTime;
- if (0 != $this->maxRunningTime && $time > $this->maxRunningTime) {
- $this->fail(\sprintf('expected running time: <= %s but was: %s', $this->maxRunningTime, $time));
- }
- $this->expectNotToPerformAssertions();
- }
- /**
- * @throws \InvalidArgumentException
- */
- public function setMaxRunningTime(int $maxRunningTime): void
- {
- if ($maxRunningTime < 0) {
- throw new \InvalidArgumentException();
- }
- $this->maxRunningTime = $maxRunningTime;
- }
- public function getMaxRunningTime(): int
- {
- return $this->maxRunningTime;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Test/FormBuilderInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Test;
- use Symfony\Component\Form\FormBuilderInterface as BaseFormBuilderInterface;
- interface FormBuilderInterface extends \Iterator, BaseFormBuilderInterface
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Test/FormIntegrationTestCase.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Test;
- use PHPUnit\Framework\TestCase;
- use Symfony\Component\Form\FormExtensionInterface;
- use Symfony\Component\Form\FormFactoryInterface;
- use Symfony\Component\Form\Forms;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeGuesserInterface;
- use Symfony\Component\Form\FormTypeInterface;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- abstract class FormIntegrationTestCase extends TestCase
- {
- protected FormFactoryInterface $factory;
- protected function setUp(): void
- {
- $this->factory = Forms::createFormFactoryBuilder()
- ->addExtensions($this->getExtensions())
- ->addTypeExtensions($this->getTypeExtensions())
- ->addTypes($this->getTypes())
- ->addTypeGuessers($this->getTypeGuessers())
- ->getFormFactory();
- }
- /**
- * @return FormExtensionInterface[]
- */
- protected function getExtensions()
- {
- return [];
- }
- /**
- * @return FormTypeExtensionInterface[]
- */
- protected function getTypeExtensions()
- {
- return [];
- }
- /**
- * @return FormTypeInterface[]
- */
- protected function getTypes()
- {
- return [];
- }
- /**
- * @return FormTypeGuesserInterface[]
- */
- protected function getTypeGuessers()
- {
- return [];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Test/FormInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Test;
- use Symfony\Component\Form\FormInterface as BaseFormInterface;
- interface FormInterface extends \Iterator, BaseFormInterface
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Test/TypeTestCase.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Test;
- use Symfony\Component\EventDispatcher\EventDispatcherInterface;
- use Symfony\Component\Form\FormBuilder;
- use Symfony\Component\Form\FormExtensionInterface;
- use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
- abstract class TypeTestCase extends FormIntegrationTestCase
- {
- protected FormBuilder $builder;
- protected EventDispatcherInterface $dispatcher;
- protected function setUp(): void
- {
- parent::setUp();
- $this->dispatcher = $this->createMock(EventDispatcherInterface::class);
- $this->builder = new FormBuilder('', null, $this->dispatcher, $this->factory);
- }
- /**
- * @return FormExtensionInterface[]
- */
- protected function getExtensions()
- {
- $extensions = [];
- if (\in_array(ValidatorExtensionTrait::class, class_uses($this), true)) {
- $extensions[] = $this->getValidatorExtension();
- }
- return $extensions;
- }
- /**
- * @return void
- */
- public static function assertDateTimeEquals(\DateTime $expected, \DateTime $actual)
- {
- self::assertEquals($expected->format('c'), $actual->format('c'));
- }
- /**
- * @return void
- */
- public static function assertDateIntervalEquals(\DateInterval $expected, \DateInterval $actual)
- {
- self::assertEquals($expected->format('%RP%yY%mM%dDT%hH%iM%sS'), $actual->format('%RP%yY%mM%dDT%hH%iM%sS'));
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormConfigInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\EventDispatcher\EventDispatcherInterface;
- use Symfony\Component\PropertyAccess\PropertyPathInterface;
- /**
- * The configuration of a {@link Form} object.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormConfigInterface
- {
- /**
- * Returns the event dispatcher used to dispatch form events.
- */
- public function getEventDispatcher(): EventDispatcherInterface;
- /**
- * Returns the name of the form used as HTTP parameter.
- */
- public function getName(): string;
- /**
- * Returns the property path that the form should be mapped to.
- */
- public function getPropertyPath(): ?PropertyPathInterface;
- /**
- * Returns whether the form should be mapped to an element of its
- * parent's data.
- */
- public function getMapped(): bool;
- /**
- * Returns whether the form's data should be modified by reference.
- */
- public function getByReference(): bool;
- /**
- * Returns whether the form should read and write the data of its parent.
- */
- public function getInheritData(): bool;
- /**
- * Returns whether the form is compound.
- *
- * This property is independent of whether the form actually has
- * children. A form can be compound and have no children at all, like
- * for example an empty collection form.
- * The contrary is not possible, a form which is not compound
- * cannot have any children.
- */
- public function getCompound(): bool;
- /**
- * Returns the resolved form type used to construct the form.
- */
- public function getType(): ResolvedFormTypeInterface;
- /**
- * Returns the view transformers of the form.
- *
- * @return DataTransformerInterface[]
- */
- public function getViewTransformers(): array;
- /**
- * Returns the model transformers of the form.
- *
- * @return DataTransformerInterface[]
- */
- public function getModelTransformers(): array;
- /**
- * Returns the data mapper of the compound form or null for a simple form.
- */
- public function getDataMapper(): ?DataMapperInterface;
- /**
- * Returns whether the form is required.
- */
- public function getRequired(): bool;
- /**
- * Returns whether the form is disabled.
- */
- public function getDisabled(): bool;
- /**
- * Returns whether errors attached to the form will bubble to its parent.
- */
- public function getErrorBubbling(): bool;
- /**
- * Used when the view data is empty on submission.
- *
- * When the form is compound it will also be used to map the
- * children data.
- *
- * The empty data must match the view format as it will passed to the first view transformer's
- * "reverseTransform" method.
- */
- public function getEmptyData(): mixed;
- /**
- * Returns additional attributes of the form.
- */
- public function getAttributes(): array;
- /**
- * Returns whether the attribute with the given name exists.
- */
- public function hasAttribute(string $name): bool;
- /**
- * Returns the value of the given attribute.
- */
- public function getAttribute(string $name, mixed $default = null): mixed;
- /**
- * Returns the initial data of the form.
- */
- public function getData(): mixed;
- /**
- * Returns the class of the view data or null if the data is scalar or an array.
- */
- public function getDataClass(): ?string;
- /**
- * Returns whether the form's data is locked.
- *
- * A form with locked data is restricted to the data passed in
- * this configuration. The data can only be modified then by
- * submitting the form.
- */
- public function getDataLocked(): bool;
- /**
- * Returns the form factory used for creating new forms.
- */
- public function getFormFactory(): FormFactoryInterface;
- /**
- * Returns the target URL of the form.
- */
- public function getAction(): string;
- /**
- * Returns the HTTP method used by the form.
- */
- public function getMethod(): string;
- /**
- * Returns the request handler used by the form.
- */
- public function getRequestHandler(): RequestHandlerInterface;
- /**
- * Returns whether the form should be initialized upon creation.
- */
- public function getAutoInitialize(): bool;
- /**
- * Returns all options passed during the construction of the form.
- *
- * @return array<string, mixed> The passed options
- */
- public function getOptions(): array;
- /**
- * Returns whether a specific option exists.
- */
- public function hasOption(string $name): bool;
- /**
- * Returns the value of a specific option.
- */
- public function getOption(string $name, mixed $default = null): mixed;
- /**
- * Returns a callable that takes the model data as argument and that returns if it is empty or not.
- */
- public function getIsEmptyCallback(): ?callable;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Button.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Exception\AlreadySubmittedException;
- use Symfony\Component\Form\Exception\BadMethodCallException;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- use Symfony\Component\PropertyAccess\PropertyPathInterface;
- /**
- * A form button.
- *
- * @author Bernhard Schussek <[email protected]>
- *
- * @implements \IteratorAggregate<string, FormInterface>
- */
- class Button implements \IteratorAggregate, FormInterface
- {
- private ?FormInterface $parent = null;
- private bool $submitted = false;
- /**
- * Creates a new button from a form configuration.
- */
- public function __construct(
- private FormConfigInterface $config,
- ) {
- }
- /**
- * Unsupported method.
- */
- public function offsetExists(mixed $offset): bool
- {
- return false;
- }
- /**
- * Unsupported method.
- *
- * This method should not be invoked.
- *
- * @throws BadMethodCallException
- */
- public function offsetGet(mixed $offset): FormInterface
- {
- throw new BadMethodCallException('Buttons cannot have children.');
- }
- /**
- * Unsupported method.
- *
- * This method should not be invoked.
- *
- * @throws BadMethodCallException
- */
- public function offsetSet(mixed $offset, mixed $value): void
- {
- throw new BadMethodCallException('Buttons cannot have children.');
- }
- /**
- * Unsupported method.
- *
- * This method should not be invoked.
- *
- * @throws BadMethodCallException
- */
- public function offsetUnset(mixed $offset): void
- {
- throw new BadMethodCallException('Buttons cannot have children.');
- }
- public function setParent(?FormInterface $parent): static
- {
- if ($this->submitted) {
- throw new AlreadySubmittedException('You cannot set the parent of a submitted button.');
- }
- $this->parent = $parent;
- return $this;
- }
- public function getParent(): ?FormInterface
- {
- return $this->parent;
- }
- /**
- * Unsupported method.
- *
- * This method should not be invoked.
- *
- * @throws BadMethodCallException
- */
- public function add(string|FormInterface $child, ?string $type = null, array $options = []): static
- {
- throw new BadMethodCallException('Buttons cannot have children.');
- }
- /**
- * Unsupported method.
- *
- * This method should not be invoked.
- *
- * @throws BadMethodCallException
- */
- public function get(string $name): FormInterface
- {
- throw new BadMethodCallException('Buttons cannot have children.');
- }
- /**
- * Unsupported method.
- */
- public function has(string $name): bool
- {
- return false;
- }
- /**
- * Unsupported method.
- *
- * This method should not be invoked.
- *
- * @throws BadMethodCallException
- */
- public function remove(string $name): static
- {
- throw new BadMethodCallException('Buttons cannot have children.');
- }
- public function all(): array
- {
- return [];
- }
- public function getErrors(bool $deep = false, bool $flatten = true): FormErrorIterator
- {
- return new FormErrorIterator($this, []);
- }
- /**
- * Unsupported method.
- *
- * This method should not be invoked.
- *
- * @return $this
- */
- public function setData(mixed $modelData): static
- {
- // no-op, called during initialization of the form tree
- return $this;
- }
- /**
- * Unsupported method.
- */
- public function getData(): mixed
- {
- return null;
- }
- /**
- * Unsupported method.
- */
- public function getNormData(): mixed
- {
- return null;
- }
- /**
- * Unsupported method.
- */
- public function getViewData(): mixed
- {
- return null;
- }
- /**
- * Unsupported method.
- */
- public function getExtraData(): array
- {
- return [];
- }
- /**
- * Returns the button's configuration.
- */
- public function getConfig(): FormConfigInterface
- {
- return $this->config;
- }
- /**
- * Returns whether the button is submitted.
- */
- public function isSubmitted(): bool
- {
- return $this->submitted;
- }
- /**
- * Returns the name by which the button is identified in forms.
- */
- public function getName(): string
- {
- return $this->config->getName();
- }
- /**
- * Unsupported method.
- */
- public function getPropertyPath(): ?PropertyPathInterface
- {
- return null;
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function addError(FormError $error): static
- {
- throw new BadMethodCallException('Buttons cannot have errors.');
- }
- /**
- * Unsupported method.
- */
- public function isValid(): bool
- {
- return true;
- }
- /**
- * Unsupported method.
- */
- public function isRequired(): bool
- {
- return false;
- }
- public function isDisabled(): bool
- {
- if ($this->parent?->isDisabled()) {
- return true;
- }
- return $this->config->getDisabled();
- }
- /**
- * Unsupported method.
- */
- public function isEmpty(): bool
- {
- return true;
- }
- /**
- * Unsupported method.
- */
- public function isSynchronized(): bool
- {
- return true;
- }
- /**
- * Unsupported method.
- */
- public function getTransformationFailure(): ?TransformationFailedException
- {
- return null;
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function initialize(): static
- {
- throw new BadMethodCallException('Buttons cannot be initialized. Call initialize() on the root form instead.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function handleRequest(mixed $request = null): static
- {
- throw new BadMethodCallException('Buttons cannot handle requests. Call handleRequest() on the root form instead.');
- }
- /**
- * Submits data to the button.
- *
- * @return $this
- *
- * @throws AlreadySubmittedException if the button has already been submitted
- */
- public function submit(array|string|null $submittedData, bool $clearMissing = true): static
- {
- if ($this->submitted) {
- throw new AlreadySubmittedException('A form can only be submitted once.');
- }
- $this->submitted = true;
- return $this;
- }
- public function getRoot(): FormInterface
- {
- return $this->parent ? $this->parent->getRoot() : $this;
- }
- public function isRoot(): bool
- {
- return null === $this->parent;
- }
- public function createView(?FormView $parent = null): FormView
- {
- if (null === $parent && $this->parent) {
- $parent = $this->parent->createView();
- }
- $type = $this->config->getType();
- $options = $this->config->getOptions();
- $view = $type->createView($this, $parent);
- $type->buildView($view, $this, $options);
- $type->finishView($view, $this, $options);
- return $view;
- }
- /**
- * Unsupported method.
- */
- public function count(): int
- {
- return 0;
- }
- /**
- * Unsupported method.
- */
- public function getIterator(): \EmptyIterator
- {
- return new \EmptyIterator();
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./DataMapperInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- interface DataMapperInterface
- {
- /**
- * Maps the view data of a compound form to its children.
- *
- * The method is responsible for calling {@link FormInterface::setData()}
- * on the children of compound forms, defining their underlying model data.
- *
- * @param mixed $viewData View data of the compound form being initialized
- * @param \Traversable<mixed, FormInterface> $forms A list of {@link FormInterface} instances
- *
- * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported
- */
- public function mapDataToForms(mixed $viewData, \Traversable $forms): void;
- /**
- * Maps the model data of a list of children forms into the view data of their parent.
- *
- * This is the internal cascade call of FormInterface::submit for compound forms, since they
- * cannot be bound to any input nor the request as scalar, but their children may:
- *
- * $compoundForm->submit($arrayOfChildrenViewData)
- * // inside:
- * $childForm->submit($childViewData);
- * // for each entry, do the same and/or reverse transform
- * $this->dataMapper->mapFormsToData($compoundForm, $compoundInitialViewData)
- * // then reverse transform
- *
- * When a simple form is submitted the following is happening:
- *
- * $simpleForm->submit($submittedViewData)
- * // inside:
- * $this->viewData = $submittedViewData
- * // then reverse transform
- *
- * The model data can be an array or an object, so this second argument is always passed
- * by reference.
- *
- * @param \Traversable<mixed, FormInterface> $forms A list of {@link FormInterface} instances
- * @param mixed &$viewData The compound form's view data that get mapped
- * its children model data
- *
- * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported
- */
- public function mapFormsToData(\Traversable $forms, mixed &$viewData): void;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormErrorIterator.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Exception\BadMethodCallException;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Exception\OutOfBoundsException;
- use Symfony\Component\Validator\ConstraintViolation;
- /**
- * Iterates over the errors of a form.
- *
- * This class supports recursive iteration. In order to iterate recursively,
- * pass a structure of {@link FormError} and {@link FormErrorIterator} objects
- * to the $errors constructor argument.
- *
- * You can also wrap the iterator into a {@link \RecursiveIteratorIterator} to
- * flatten the recursive structure into a flat list of errors.
- *
- * @author Bernhard Schussek <[email protected]>
- *
- * @template T of FormError|FormErrorIterator
- *
- * @implements \ArrayAccess<int, T>
- * @implements \RecursiveIterator<int, T>
- * @implements \SeekableIterator<int, T>
- */
- class FormErrorIterator implements \RecursiveIterator, \SeekableIterator, \ArrayAccess, \Countable, \Stringable
- {
- /**
- * The prefix used for indenting nested error messages.
- */
- public const INDENTATION = ' ';
- /**
- * @var list<T>
- */
- private array $errors;
- /**
- * @param list<T> $errors
- *
- * @throws InvalidArgumentException If the errors are invalid
- */
- public function __construct(
- private FormInterface $form,
- array $errors,
- ) {
- foreach ($errors as $error) {
- if (!($error instanceof FormError || $error instanceof self)) {
- throw new InvalidArgumentException(\sprintf('The errors must be instances of "Symfony\Component\Form\FormError" or "%s". Got: "%s".', __CLASS__, get_debug_type($error)));
- }
- }
- $this->errors = $errors;
- }
- /**
- * Returns all iterated error messages as string.
- */
- public function __toString(): string
- {
- $string = '';
- foreach ($this->errors as $error) {
- if ($error instanceof FormError) {
- $string .= 'ERROR: '.$error->getMessage()."\n";
- } else {
- /* @var self $error */
- $string .= $error->getForm()->getName().":\n";
- $string .= self::indent((string) $error);
- }
- }
- return $string;
- }
- /**
- * Returns the iterated form.
- */
- public function getForm(): FormInterface
- {
- return $this->form;
- }
- /**
- * Returns the current element of the iterator.
- *
- * @return T An error or an iterator containing nested errors
- */
- public function current(): FormError|self
- {
- return current($this->errors);
- }
- /**
- * Advances the iterator to the next position.
- */
- public function next(): void
- {
- next($this->errors);
- }
- /**
- * Returns the current position of the iterator.
- */
- public function key(): int
- {
- return key($this->errors);
- }
- /**
- * Returns whether the iterator's position is valid.
- */
- public function valid(): bool
- {
- return null !== key($this->errors);
- }
- /**
- * Sets the iterator's position to the beginning.
- *
- * This method detects if errors have been added to the form since the
- * construction of the iterator.
- */
- public function rewind(): void
- {
- reset($this->errors);
- }
- /**
- * Returns whether a position exists in the iterator.
- *
- * @param int $position The position
- */
- public function offsetExists(mixed $position): bool
- {
- return isset($this->errors[$position]);
- }
- /**
- * Returns the element at a position in the iterator.
- *
- * @param int $position The position
- *
- * @return T
- *
- * @throws OutOfBoundsException If the given position does not exist
- */
- public function offsetGet(mixed $position): FormError|self
- {
- if (!isset($this->errors[$position])) {
- throw new OutOfBoundsException('The offset '.$position.' does not exist.');
- }
- return $this->errors[$position];
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function offsetSet(mixed $position, mixed $value): void
- {
- throw new BadMethodCallException('The iterator doesn\'t support modification of elements.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function offsetUnset(mixed $position): void
- {
- throw new BadMethodCallException('The iterator doesn\'t support modification of elements.');
- }
- /**
- * Returns whether the current element of the iterator can be recursed
- * into.
- */
- public function hasChildren(): bool
- {
- return current($this->errors) instanceof self;
- }
- public function getChildren(): self
- {
- if (!$this->hasChildren()) {
- throw new LogicException(\sprintf('The current element is not iterable. Use "%s" to get the current element.', self::class.'::current()'));
- }
- /** @var self $children */
- $children = current($this->errors);
- return $children;
- }
- /**
- * Returns the number of elements in the iterator.
- *
- * Note that this is not the total number of errors, if the constructor
- * parameter $deep was set to true! In that case, you should wrap the
- * iterator into a {@link \RecursiveIteratorIterator} with the standard mode
- * {@link \RecursiveIteratorIterator::LEAVES_ONLY} and count the result.
- *
- * $iterator = new \RecursiveIteratorIterator($form->getErrors(true));
- * $count = count(iterator_to_array($iterator));
- *
- * Alternatively, set the constructor argument $flatten to true as well.
- *
- * $count = count($form->getErrors(true, true));
- */
- public function count(): int
- {
- return \count($this->errors);
- }
- /**
- * Sets the position of the iterator.
- *
- * @throws OutOfBoundsException If the position is invalid
- */
- public function seek(int $position): void
- {
- if (!isset($this->errors[$position])) {
- throw new OutOfBoundsException('The offset '.$position.' does not exist.');
- }
- reset($this->errors);
- while ($position !== key($this->errors)) {
- next($this->errors);
- }
- }
- /**
- * Creates iterator for errors with specific codes.
- *
- * @param string|string[] $codes The codes to find
- */
- public function findByCodes(string|array $codes): static
- {
- $codes = (array) $codes;
- $errors = [];
- foreach ($this as $error) {
- $cause = $error->getCause();
- if ($cause instanceof ConstraintViolation && \in_array($cause->getCode(), $codes, true)) {
- $errors[] = $error;
- }
- }
- return new static($this->form, $errors);
- }
- /**
- * Utility function for indenting multi-line strings.
- */
- private static function indent(string $string): string
- {
- return rtrim(self::INDENTATION.str_replace("\n", "\n".self::INDENTATION, $string), ' ');
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormConfigBuilder.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\EventDispatcher\EventDispatcherInterface;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
- use Symfony\Component\Form\Exception\BadMethodCallException;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- use Symfony\Component\PropertyAccess\PropertyPath;
- use Symfony\Component\PropertyAccess\PropertyPathInterface;
- /**
- * A basic form configuration.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class FormConfigBuilder implements FormConfigBuilderInterface
- {
- protected bool $locked = false;
- /**
- * Caches a globally unique {@link NativeRequestHandler} instance.
- */
- private static NativeRequestHandler $nativeRequestHandler;
- private string $name;
- private ?PropertyPathInterface $propertyPath = null;
- private bool $mapped = true;
- private bool $byReference = true;
- private bool $inheritData = false;
- private bool $compound = false;
- private ResolvedFormTypeInterface $type;
- private array $viewTransformers = [];
- private array $modelTransformers = [];
- private ?DataMapperInterface $dataMapper = null;
- private bool $required = true;
- private bool $disabled = false;
- private bool $errorBubbling = false;
- private mixed $emptyData = null;
- private array $attributes = [];
- private mixed $data = null;
- private ?string $dataClass;
- private bool $dataLocked = false;
- private FormFactoryInterface $formFactory;
- private string $action = '';
- private string $method = 'POST';
- private RequestHandlerInterface $requestHandler;
- private bool $autoInitialize = false;
- private ?\Closure $isEmptyCallback = null;
- /**
- * Creates an empty form configuration.
- *
- * @param string|null $name The form name
- * @param string|null $dataClass The class of the form's data
- *
- * @throws InvalidArgumentException if the data class is not a valid class or if
- * the name contains invalid characters
- */
- public function __construct(
- ?string $name,
- ?string $dataClass,
- private EventDispatcherInterface $dispatcher,
- private array $options = [],
- ) {
- self::validateName($name);
- if (null !== $dataClass && !class_exists($dataClass) && !interface_exists($dataClass, false)) {
- throw new InvalidArgumentException(\sprintf('Class "%s" not found. Is the "data_class" form option set correctly?', $dataClass));
- }
- $this->name = (string) $name;
- $this->dataClass = $dataClass;
- }
- public function addEventListener(string $eventName, callable $listener, int $priority = 0): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->dispatcher->addListener($eventName, $listener, $priority);
- return $this;
- }
- public function addEventSubscriber(EventSubscriberInterface $subscriber): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->dispatcher->addSubscriber($subscriber);
- return $this;
- }
- public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- if ($forcePrepend) {
- array_unshift($this->viewTransformers, $viewTransformer);
- } else {
- $this->viewTransformers[] = $viewTransformer;
- }
- return $this;
- }
- public function resetViewTransformers(): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->viewTransformers = [];
- return $this;
- }
- public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- if ($forceAppend) {
- $this->modelTransformers[] = $modelTransformer;
- } else {
- array_unshift($this->modelTransformers, $modelTransformer);
- }
- return $this;
- }
- public function resetModelTransformers(): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->modelTransformers = [];
- return $this;
- }
- public function getEventDispatcher(): EventDispatcherInterface
- {
- if ($this->locked && !$this->dispatcher instanceof ImmutableEventDispatcher) {
- $this->dispatcher = new ImmutableEventDispatcher($this->dispatcher);
- }
- return $this->dispatcher;
- }
- public function getName(): string
- {
- return $this->name;
- }
- public function getPropertyPath(): ?PropertyPathInterface
- {
- return $this->propertyPath;
- }
- public function getMapped(): bool
- {
- return $this->mapped;
- }
- public function getByReference(): bool
- {
- return $this->byReference;
- }
- public function getInheritData(): bool
- {
- return $this->inheritData;
- }
- public function getCompound(): bool
- {
- return $this->compound;
- }
- public function getType(): ResolvedFormTypeInterface
- {
- return $this->type;
- }
- public function getViewTransformers(): array
- {
- return $this->viewTransformers;
- }
- public function getModelTransformers(): array
- {
- return $this->modelTransformers;
- }
- public function getDataMapper(): ?DataMapperInterface
- {
- return $this->dataMapper;
- }
- public function getRequired(): bool
- {
- return $this->required;
- }
- public function getDisabled(): bool
- {
- return $this->disabled;
- }
- public function getErrorBubbling(): bool
- {
- return $this->errorBubbling;
- }
- public function getEmptyData(): mixed
- {
- return $this->emptyData;
- }
- public function getAttributes(): array
- {
- return $this->attributes;
- }
- public function hasAttribute(string $name): bool
- {
- return \array_key_exists($name, $this->attributes);
- }
- public function getAttribute(string $name, mixed $default = null): mixed
- {
- return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
- }
- public function getData(): mixed
- {
- return $this->data;
- }
- public function getDataClass(): ?string
- {
- return $this->dataClass;
- }
- public function getDataLocked(): bool
- {
- return $this->dataLocked;
- }
- public function getFormFactory(): FormFactoryInterface
- {
- if (!isset($this->formFactory)) {
- throw new BadMethodCallException('The form factory must be set before retrieving it.');
- }
- return $this->formFactory;
- }
- public function getAction(): string
- {
- return $this->action;
- }
- public function getMethod(): string
- {
- return $this->method;
- }
- public function getRequestHandler(): RequestHandlerInterface
- {
- return $this->requestHandler ??= self::$nativeRequestHandler ??= new NativeRequestHandler();
- }
- public function getAutoInitialize(): bool
- {
- return $this->autoInitialize;
- }
- public function getOptions(): array
- {
- return $this->options;
- }
- public function hasOption(string $name): bool
- {
- return \array_key_exists($name, $this->options);
- }
- public function getOption(string $name, mixed $default = null): mixed
- {
- return \array_key_exists($name, $this->options) ? $this->options[$name] : $default;
- }
- public function getIsEmptyCallback(): ?callable
- {
- return $this->isEmptyCallback;
- }
- /**
- * @return $this
- */
- public function setAttribute(string $name, mixed $value): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->attributes[$name] = $value;
- return $this;
- }
- /**
- * @return $this
- */
- public function setAttributes(array $attributes): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->attributes = $attributes;
- return $this;
- }
- /**
- * @return $this
- */
- public function setDataMapper(?DataMapperInterface $dataMapper): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->dataMapper = $dataMapper;
- return $this;
- }
- /**
- * @return $this
- */
- public function setDisabled(bool $disabled): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->disabled = $disabled;
- return $this;
- }
- /**
- * @return $this
- */
- public function setEmptyData(mixed $emptyData): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->emptyData = $emptyData;
- return $this;
- }
- /**
- * @return $this
- */
- public function setErrorBubbling(bool $errorBubbling): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->errorBubbling = $errorBubbling;
- return $this;
- }
- /**
- * @return $this
- */
- public function setRequired(bool $required): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->required = $required;
- return $this;
- }
- /**
- * @return $this
- */
- public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- if (null !== $propertyPath && !$propertyPath instanceof PropertyPathInterface) {
- $propertyPath = new PropertyPath($propertyPath);
- }
- $this->propertyPath = $propertyPath;
- return $this;
- }
- /**
- * @return $this
- */
- public function setMapped(bool $mapped): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->mapped = $mapped;
- return $this;
- }
- /**
- * @return $this
- */
- public function setByReference(bool $byReference): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->byReference = $byReference;
- return $this;
- }
- /**
- * @return $this
- */
- public function setInheritData(bool $inheritData): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->inheritData = $inheritData;
- return $this;
- }
- /**
- * @return $this
- */
- public function setCompound(bool $compound): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->compound = $compound;
- return $this;
- }
- /**
- * @return $this
- */
- public function setType(ResolvedFormTypeInterface $type): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->type = $type;
- return $this;
- }
- /**
- * @return $this
- */
- public function setData(mixed $data): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->data = $data;
- return $this;
- }
- /**
- * @return $this
- */
- public function setDataLocked(bool $locked): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->dataLocked = $locked;
- return $this;
- }
- /**
- * @return $this
- */
- public function setFormFactory(FormFactoryInterface $formFactory): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->formFactory = $formFactory;
- return $this;
- }
- /**
- * @return $this
- */
- public function setAction(string $action): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('The config builder cannot be modified anymore.');
- }
- $this->action = $action;
- return $this;
- }
- /**
- * @return $this
- */
- public function setMethod(string $method): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('The config builder cannot be modified anymore.');
- }
- $this->method = strtoupper($method);
- return $this;
- }
- /**
- * @return $this
- */
- public function setRequestHandler(RequestHandlerInterface $requestHandler): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('The config builder cannot be modified anymore.');
- }
- $this->requestHandler = $requestHandler;
- return $this;
- }
- /**
- * @return $this
- */
- public function setAutoInitialize(bool $initialize): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->autoInitialize = $initialize;
- return $this;
- }
- public function getFormConfig(): FormConfigInterface
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- // This method should be idempotent, so clone the builder
- $config = clone $this;
- $config->locked = true;
- return $config;
- }
- /**
- * @return $this
- */
- public function setIsEmptyCallback(?callable $isEmptyCallback): static
- {
- $this->isEmptyCallback = null === $isEmptyCallback ? null : $isEmptyCallback(...);
- return $this;
- }
- /**
- * Validates whether the given variable is a valid form name.
- *
- * @throws InvalidArgumentException if the name contains invalid characters
- *
- * @internal
- */
- final public static function validateName(?string $name): void
- {
- if (!self::isValidName($name)) {
- 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));
- }
- }
- /**
- * Returns whether the given variable contains a valid form name.
- *
- * A name is accepted if it
- *
- * * is empty
- * * starts with a letter, digit or underscore
- * * contains only letters, digits, numbers, underscores ("_"),
- * hyphens ("-") and colons (":")
- */
- final public static function isValidName(?string $name): bool
- {
- return '' === $name || null === $name || preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormRegistry.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Exception\ExceptionInterface;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- /**
- * The central registry of the Form component.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class FormRegistry implements FormRegistryInterface
- {
- /**
- * @var FormExtensionInterface[]
- */
- private array $extensions = [];
- /**
- * @var ResolvedFormTypeInterface[]
- */
- private array $types = [];
- private FormTypeGuesserInterface|false|null $guesser = false;
- private array $checkedTypes = [];
- /**
- * @param FormExtensionInterface[] $extensions
- *
- * @throws UnexpectedTypeException if any extension does not implement FormExtensionInterface
- */
- public function __construct(
- array $extensions,
- private ResolvedFormTypeFactoryInterface $resolvedTypeFactory,
- ) {
- foreach ($extensions as $extension) {
- if (!$extension instanceof FormExtensionInterface) {
- throw new UnexpectedTypeException($extension, FormExtensionInterface::class);
- }
- }
- $this->extensions = $extensions;
- }
- public function getType(string $name): ResolvedFormTypeInterface
- {
- if (!isset($this->types[$name])) {
- $type = null;
- foreach ($this->extensions as $extension) {
- if ($extension->hasType($name)) {
- $type = $extension->getType($name);
- break;
- }
- }
- if (!$type) {
- // Support fully-qualified class names
- if (!class_exists($name)) {
- throw new InvalidArgumentException(\sprintf('Could not load type "%s": class does not exist.', $name));
- }
- if (!is_subclass_of($name, FormTypeInterface::class)) {
- throw new InvalidArgumentException(\sprintf('Could not load type "%s": class does not implement "Symfony\Component\Form\FormTypeInterface".', $name));
- }
- $type = new $name();
- }
- $this->types[$name] = $this->resolveType($type);
- }
- return $this->types[$name];
- }
- /**
- * Wraps a type into a ResolvedFormTypeInterface implementation and connects it with its parent type.
- */
- private function resolveType(FormTypeInterface $type): ResolvedFormTypeInterface
- {
- $parentType = $type->getParent();
- $fqcn = $type::class;
- if (isset($this->checkedTypes[$fqcn])) {
- $types = implode(' > ', array_merge(array_keys($this->checkedTypes), [$fqcn]));
- throw new LogicException(\sprintf('Circular reference detected for form type "%s" (%s).', $fqcn, $types));
- }
- $this->checkedTypes[$fqcn] = true;
- $typeExtensions = [];
- try {
- foreach ($this->extensions as $extension) {
- $typeExtensions[] = $extension->getTypeExtensions($fqcn);
- }
- return $this->resolvedTypeFactory->createResolvedType(
- $type,
- array_merge([], ...$typeExtensions),
- $parentType ? $this->getType($parentType) : null
- );
- } finally {
- unset($this->checkedTypes[$fqcn]);
- }
- }
- public function hasType(string $name): bool
- {
- if (isset($this->types[$name])) {
- return true;
- }
- try {
- $this->getType($name);
- } catch (ExceptionInterface) {
- return false;
- }
- return true;
- }
- public function getTypeGuesser(): ?FormTypeGuesserInterface
- {
- if (false === $this->guesser) {
- $guessers = [];
- foreach ($this->extensions as $extension) {
- $guesser = $extension->getTypeGuesser();
- if ($guesser) {
- $guessers[] = $guesser;
- }
- }
- $this->guesser = $guessers ? new FormTypeGuesserChain($guessers) : null;
- }
- return $this->guesser;
- }
- public function getExtensions(): array
- {
- return $this->extensions;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormView.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Exception\BadMethodCallException;
- /**
- * @author Bernhard Schussek <[email protected]>
- *
- * @implements \ArrayAccess<int|string, FormView>
- * @implements \IteratorAggregate<int|string, FormView>
- */
- class FormView implements \ArrayAccess, \IteratorAggregate, \Countable
- {
- /**
- * The variables assigned to this view.
- */
- public array $vars = [
- 'value' => null,
- 'attr' => [],
- ];
- /**
- * The child views.
- *
- * @var array<int|string, FormView>
- */
- public array $children = [];
- /**
- * Is the form attached to this renderer rendered?
- *
- * Rendering happens when either the widget or the row method was called.
- * Row implicitly includes widget, however certain rendering mechanisms
- * have to skip widget rendering when a row is rendered.
- */
- private bool $rendered = false;
- private bool $methodRendered = false;
- /**
- * @param FormView|null $parent The parent view
- */
- public function __construct(
- public ?self $parent = null,
- ) {
- }
- /**
- * Returns whether the view was already rendered.
- */
- public function isRendered(): bool
- {
- if (true === $this->rendered || 0 === \count($this->children)) {
- return $this->rendered;
- }
- foreach ($this->children as $child) {
- if (!$child->isRendered()) {
- return false;
- }
- }
- return $this->rendered = true;
- }
- /**
- * Marks the view as rendered.
- *
- * @return $this
- */
- public function setRendered(): static
- {
- $this->rendered = true;
- return $this;
- }
- public function isMethodRendered(): bool
- {
- return $this->methodRendered;
- }
- public function setMethodRendered(): void
- {
- $this->methodRendered = true;
- }
- /**
- * Returns a child by name (implements \ArrayAccess).
- *
- * @param int|string $name The child name
- */
- public function offsetGet(mixed $name): self
- {
- return $this->children[$name];
- }
- /**
- * Returns whether the given child exists (implements \ArrayAccess).
- *
- * @param int|string $name The child name
- */
- public function offsetExists(mixed $name): bool
- {
- return isset($this->children[$name]);
- }
- /**
- * Implements \ArrayAccess.
- *
- * @throws BadMethodCallException always as setting a child by name is not allowed
- */
- public function offsetSet(mixed $name, mixed $value): void
- {
- throw new BadMethodCallException('Not supported.');
- }
- /**
- * Removes a child (implements \ArrayAccess).
- *
- * @param int|string $name The child name
- */
- public function offsetUnset(mixed $name): void
- {
- unset($this->children[$name]);
- }
- /**
- * Returns an iterator to iterate over children (implements \IteratorAggregate).
- *
- * @return \ArrayIterator<int|string, FormView>
- */
- public function getIterator(): \ArrayIterator
- {
- return new \ArrayIterator($this->children);
- }
- public function count(): int
- {
- return \count($this->children);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ButtonBuilder.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\Form\Exception\BadMethodCallException;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- use Symfony\Component\PropertyAccess\PropertyPathInterface;
- /**
- * A builder for {@link Button} instances.
- *
- * @author Bernhard Schussek <[email protected]>
- *
- * @implements \IteratorAggregate<string, FormBuilderInterface>
- */
- class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
- {
- protected bool $locked = false;
- private bool $disabled = false;
- private ResolvedFormTypeInterface $type;
- private string $name;
- private array $attributes = [];
- /**
- * @throws InvalidArgumentException if the name is empty
- */
- public function __construct(
- ?string $name,
- private array $options = [],
- ) {
- if ('' === $name || null === $name) {
- throw new InvalidArgumentException('Buttons cannot have empty names.');
- }
- $this->name = $name;
- FormConfigBuilder::validateName($name);
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function add(string|FormBuilderInterface $child, ?string $type = null, array $options = []): never
- {
- throw new BadMethodCallException('Buttons cannot have children.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function create(string $name, ?string $type = null, array $options = []): never
- {
- throw new BadMethodCallException('Buttons cannot have children.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function get(string $name): never
- {
- throw new BadMethodCallException('Buttons cannot have children.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function remove(string $name): never
- {
- throw new BadMethodCallException('Buttons cannot have children.');
- }
- /**
- * Unsupported method.
- */
- public function has(string $name): bool
- {
- return false;
- }
- /**
- * Returns the children.
- */
- public function all(): array
- {
- return [];
- }
- /**
- * Creates the button.
- */
- public function getForm(): Button
- {
- return new Button($this->getFormConfig());
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function addEventListener(string $eventName, callable $listener, int $priority = 0): never
- {
- throw new BadMethodCallException('Buttons do not support event listeners.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function addEventSubscriber(EventSubscriberInterface $subscriber): never
- {
- throw new BadMethodCallException('Buttons do not support event subscribers.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): never
- {
- throw new BadMethodCallException('Buttons do not support data transformers.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function resetViewTransformers(): never
- {
- throw new BadMethodCallException('Buttons do not support data transformers.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): never
- {
- throw new BadMethodCallException('Buttons do not support data transformers.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function resetModelTransformers(): never
- {
- throw new BadMethodCallException('Buttons do not support data transformers.');
- }
- /**
- * @return $this
- */
- public function setAttribute(string $name, mixed $value): static
- {
- $this->attributes[$name] = $value;
- return $this;
- }
- /**
- * @return $this
- */
- public function setAttributes(array $attributes): static
- {
- $this->attributes = $attributes;
- return $this;
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setDataMapper(?DataMapperInterface $dataMapper): never
- {
- throw new BadMethodCallException('Buttons do not support data mappers.');
- }
- /**
- * Set whether the button is disabled.
- *
- * @return $this
- */
- public function setDisabled(bool $disabled): static
- {
- $this->disabled = $disabled;
- return $this;
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setEmptyData(mixed $emptyData): never
- {
- throw new BadMethodCallException('Buttons do not support empty data.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setErrorBubbling(bool $errorBubbling): never
- {
- throw new BadMethodCallException('Buttons do not support error bubbling.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setRequired(bool $required): never
- {
- throw new BadMethodCallException('Buttons cannot be required.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setPropertyPath(string|PropertyPathInterface|null $propertyPath): never
- {
- throw new BadMethodCallException('Buttons do not support property paths.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setMapped(bool $mapped): never
- {
- throw new BadMethodCallException('Buttons do not support data mapping.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setByReference(bool $byReference): never
- {
- throw new BadMethodCallException('Buttons do not support data mapping.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setCompound(bool $compound): never
- {
- throw new BadMethodCallException('Buttons cannot be compound.');
- }
- /**
- * Sets the type of the button.
- *
- * @return $this
- */
- public function setType(ResolvedFormTypeInterface $type): static
- {
- $this->type = $type;
- return $this;
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setData(mixed $data): never
- {
- throw new BadMethodCallException('Buttons do not support data.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setDataLocked(bool $locked): never
- {
- throw new BadMethodCallException('Buttons do not support data locking.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setFormFactory(FormFactoryInterface $formFactory): never
- {
- throw new BadMethodCallException('Buttons do not support form factories.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setAction(string $action): never
- {
- throw new BadMethodCallException('Buttons do not support actions.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setMethod(string $method): never
- {
- throw new BadMethodCallException('Buttons do not support methods.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setRequestHandler(RequestHandlerInterface $requestHandler): never
- {
- throw new BadMethodCallException('Buttons do not support request handlers.');
- }
- /**
- * Unsupported method.
- *
- * @return $this
- *
- * @throws BadMethodCallException
- */
- public function setAutoInitialize(bool $initialize): static
- {
- if (true === $initialize) {
- throw new BadMethodCallException('Buttons do not support automatic initialization.');
- }
- return $this;
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setInheritData(bool $inheritData): never
- {
- throw new BadMethodCallException('Buttons do not support data inheritance.');
- }
- /**
- * Builds and returns the button configuration.
- */
- public function getFormConfig(): FormConfigInterface
- {
- // This method should be idempotent, so clone the builder
- $config = clone $this;
- $config->locked = true;
- return $config;
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function setIsEmptyCallback(?callable $isEmptyCallback): never
- {
- throw new BadMethodCallException('Buttons do not support "is empty" callback.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function getEventDispatcher(): never
- {
- throw new BadMethodCallException('Buttons do not support event dispatching.');
- }
- public function getName(): string
- {
- return $this->name;
- }
- /**
- * Unsupported method.
- */
- public function getPropertyPath(): ?PropertyPathInterface
- {
- return null;
- }
- /**
- * Unsupported method.
- */
- public function getMapped(): bool
- {
- return false;
- }
- /**
- * Unsupported method.
- */
- public function getByReference(): bool
- {
- return false;
- }
- /**
- * Unsupported method.
- */
- public function getCompound(): bool
- {
- return false;
- }
- /**
- * Returns the form type used to construct the button.
- */
- public function getType(): ResolvedFormTypeInterface
- {
- return $this->type;
- }
- /**
- * Unsupported method.
- */
- public function getViewTransformers(): array
- {
- return [];
- }
- /**
- * Unsupported method.
- */
- public function getModelTransformers(): array
- {
- return [];
- }
- /**
- * Unsupported method.
- */
- public function getDataMapper(): ?DataMapperInterface
- {
- return null;
- }
- /**
- * Unsupported method.
- */
- public function getRequired(): bool
- {
- return false;
- }
- /**
- * Returns whether the button is disabled.
- */
- public function getDisabled(): bool
- {
- return $this->disabled;
- }
- /**
- * Unsupported method.
- */
- public function getErrorBubbling(): bool
- {
- return false;
- }
- /**
- * Unsupported method.
- */
- public function getEmptyData(): mixed
- {
- return null;
- }
- /**
- * Returns additional attributes of the button.
- */
- public function getAttributes(): array
- {
- return $this->attributes;
- }
- /**
- * Returns whether the attribute with the given name exists.
- */
- public function hasAttribute(string $name): bool
- {
- return \array_key_exists($name, $this->attributes);
- }
- /**
- * Returns the value of the given attribute.
- */
- public function getAttribute(string $name, mixed $default = null): mixed
- {
- return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
- }
- /**
- * Unsupported method.
- */
- public function getData(): mixed
- {
- return null;
- }
- /**
- * Unsupported method.
- */
- public function getDataClass(): ?string
- {
- return null;
- }
- /**
- * Unsupported method.
- */
- public function getDataLocked(): bool
- {
- return false;
- }
- /**
- * Unsupported method.
- */
- public function getFormFactory(): never
- {
- throw new BadMethodCallException('Buttons do not support adding children.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function getAction(): never
- {
- throw new BadMethodCallException('Buttons do not support actions.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function getMethod(): never
- {
- throw new BadMethodCallException('Buttons do not support methods.');
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function getRequestHandler(): never
- {
- throw new BadMethodCallException('Buttons do not support request handlers.');
- }
- /**
- * Unsupported method.
- */
- public function getAutoInitialize(): bool
- {
- return false;
- }
- /**
- * Unsupported method.
- */
- public function getInheritData(): bool
- {
- return false;
- }
- /**
- * Returns all options passed during the construction of the button.
- */
- public function getOptions(): array
- {
- return $this->options;
- }
- /**
- * Returns whether a specific option exists.
- */
- public function hasOption(string $name): bool
- {
- return \array_key_exists($name, $this->options);
- }
- /**
- * Returns the value of a specific option.
- */
- public function getOption(string $name, mixed $default = null): mixed
- {
- return \array_key_exists($name, $this->options) ? $this->options[$name] : $default;
- }
- /**
- * Unsupported method.
- *
- * @throws BadMethodCallException
- */
- public function getIsEmptyCallback(): never
- {
- throw new BadMethodCallException('Buttons do not support "is empty" callback.');
- }
- /**
- * Unsupported method.
- */
- public function count(): int
- {
- return 0;
- }
- /**
- * Unsupported method.
- */
- public function getIterator(): \EmptyIterator
- {
- return new \EmptyIterator();
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./DataAccessorInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * Writes and reads values to/from an object or array bound to a form.
- *
- * @author Yonel Ceruto <[email protected]>
- */
- interface DataAccessorInterface
- {
- /**
- * Returns the value at the end of the property of the object graph.
- *
- * @throws Exception\AccessException If unable to read from the given form data
- */
- public function getValue(object|array $viewData, FormInterface $form): mixed;
- /**
- * Sets the value at the end of the property of the object graph.
- *
- * @throws Exception\AccessException If unable to write the given value
- */
- public function setValue(object|array &$viewData, mixed $value, FormInterface $form): void;
- /**
- * Returns whether a value can be read from an object graph.
- *
- * Whenever this method returns true, {@link getValue()} is guaranteed not
- * to throw an exception when called with the same arguments.
- */
- public function isReadable(object|array $viewData, FormInterface $form): bool;
- /**
- * Returns whether a value can be written at a given object graph.
- *
- * Whenever this method returns true, {@link setValue()} is guaranteed not
- * to throw an exception when called with the same arguments.
- */
- public function isWritable(object|array $viewData, FormInterface $form): bool;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ReversedTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * Reverses a transformer.
- *
- * When the transform() method is called, the reversed transformer's
- * reverseTransform() method is called and vice versa.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ReversedTransformer implements DataTransformerInterface
- {
- public function __construct(
- protected DataTransformerInterface $reversedTransformer,
- ) {
- }
- public function transform(mixed $value): mixed
- {
- return $this->reversedTransformer->reverseTransform($value);
- }
- public function reverseTransform(mixed $value): mixed
- {
- return $this->reversedTransformer->transform($value);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormRendererEngineInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * Adapter for rendering form templates with a specific templating engine.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormRendererEngineInterface
- {
- /**
- * Sets the theme(s) to be used for rendering a view and its children.
- *
- * @param FormView $view The view to assign the theme(s) to
- * @param mixed $themes The theme(s). The type of these themes
- * is open to the implementation.
- */
- public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void;
- /**
- * Returns the resource for a block name.
- *
- * The resource is first searched in the themes attached to $view, then
- * in the themes of its parent view and so on, until a resource was found.
- *
- * The type of the resource is decided by the implementation. The resource
- * is later passed to {@link renderBlock()} by the rendering algorithm.
- *
- * @param FormView $view The view for determining the used themes.
- * First the themes attached directly to the
- * view with {@link setTheme()} are considered,
- * then the ones of its parent etc.
- *
- * @return mixed the renderer resource or false, if none was found
- */
- public function getResourceForBlockName(FormView $view, string $blockName): mixed;
- /**
- * Returns the resource for a block hierarchy.
- *
- * A block hierarchy is an array which starts with the root of the hierarchy
- * and continues with the child of that root, the child of that child etc.
- * The following is an example for a block hierarchy:
- *
- * form_widget
- * text_widget
- * url_widget
- *
- * In this example, "url_widget" is the most specific block, while the other
- * blocks are its ancestors in the hierarchy.
- *
- * The second parameter $hierarchyLevel determines the level of the hierarchy
- * that should be rendered. For example, if $hierarchyLevel is 2 for the
- * above hierarchy, the engine will first look for the block "url_widget",
- * then, if that does not exist, for the block "text_widget" etc.
- *
- * The type of the resource is decided by the implementation. The resource
- * is later passed to {@link renderBlock()} by the rendering algorithm.
- *
- * @param FormView $view The view for determining the used themes.
- * First the themes attached directly to
- * the view with {@link setTheme()} are
- * considered, then the ones of its parent etc.
- * @param string[] $blockNameHierarchy The block name hierarchy, with the root block
- * at the beginning
- * @param int $hierarchyLevel The level in the hierarchy at which to start
- * looking. Level 0 indicates the root block, i.e.
- * the first element of $blockNameHierarchy.
- *
- * @return mixed The renderer resource or false, if none was found
- */
- public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): mixed;
- /**
- * Returns the hierarchy level at which a resource can be found.
- *
- * A block hierarchy is an array which starts with the root of the hierarchy
- * and continues with the child of that root, the child of that child etc.
- * The following is an example for a block hierarchy:
- *
- * form_widget
- * text_widget
- * url_widget
- *
- * The second parameter $hierarchyLevel determines the level of the hierarchy
- * that should be rendered.
- *
- * If we call this method with the hierarchy level 2, the engine will first
- * look for a resource for block "url_widget". If such a resource exists,
- * the method returns 2. Otherwise it tries to find a resource for block
- * "text_widget" (at level 1) and, again, returns 1 if a resource was found.
- * The method continues to look for resources until the root level was
- * reached and nothing was found. In this case false is returned.
- *
- * The type of the resource is decided by the implementation. The resource
- * is later passed to {@link renderBlock()} by the rendering algorithm.
- *
- * @param FormView $view The view for determining the used themes.
- * First the themes attached directly to
- * the view with {@link setTheme()} are
- * considered, then the ones of its parent etc.
- * @param string[] $blockNameHierarchy The block name hierarchy, with the root block
- * at the beginning
- * @param int $hierarchyLevel The level in the hierarchy at which to start
- * looking. Level 0 indicates the root block, i.e.
- * the first element of $blockNameHierarchy.
- */
- public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, int $hierarchyLevel): int|false;
- /**
- * Renders a block in the given renderer resource.
- *
- * The resource can be obtained by calling {@link getResourceForBlock()}
- * or {@link getResourceForBlockHierarchy()}. The type of the resource is
- * decided by the implementation.
- *
- * @param FormView $view The view to render
- * @param mixed $resource The renderer resource
- * @param array $variables The variables to pass to the template
- */
- public function renderBlock(FormView $view, mixed $resource, string $blockName, array $variables = []): string;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./PreloadedExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- /**
- * A form extension with preloaded types, type extensions and type guessers.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class PreloadedExtension implements FormExtensionInterface
- {
- private array $types = [];
- /**
- * Creates a new preloaded extension.
- *
- * @param FormTypeInterface[] $types The types that the extension should support
- * @param FormTypeExtensionInterface[][] $typeExtensions The type extensions that the extension should support
- */
- public function __construct(
- array $types,
- private array $typeExtensions,
- private ?FormTypeGuesserInterface $typeGuesser = null,
- ) {
- foreach ($types as $type) {
- $this->types[$type::class] = $type;
- }
- }
- public function getType(string $name): FormTypeInterface
- {
- if (!isset($this->types[$name])) {
- throw new InvalidArgumentException(\sprintf('The type "%s" cannot be loaded by this extension.', $name));
- }
- return $this->types[$name];
- }
- public function hasType(string $name): bool
- {
- return isset($this->types[$name]);
- }
- public function getTypeExtensions(string $name): array
- {
- return $this->typeExtensions[$name]
- ?? [];
- }
- public function hasTypeExtensions(string $name): bool
- {
- return !empty($this->typeExtensions[$name]);
- }
- public function getTypeGuesser(): ?FormTypeGuesserInterface
- {
- return $this->typeGuesser;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./DataTransformerInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms a value between different representations.
- *
- * @author Bernhard Schussek <[email protected]>
- *
- * @template TValue
- * @template TTransformedValue
- */
- interface DataTransformerInterface
- {
- /**
- * Transforms a value from the original representation to a transformed representation.
- *
- * This method is called when the form field is initialized with its default data, on
- * two occasions for two types of transformers:
- *
- * 1. Model transformers which normalize the model data.
- * This is mainly useful when the same form type (the same configuration)
- * has to handle different kind of underlying data, e.g The DateType can
- * deal with strings or \DateTime objects as input.
- *
- * 2. View transformers which adapt the normalized data to the view format.
- * a/ When the form is simple, the value returned by convention is used
- * directly in the view and thus can only be a string or an array. In
- * this case the data class should be null.
- *
- * b/ When the form is compound the returned value should be an array or
- * an object to be mapped to the children. Each property of the compound
- * data will be used as model data by each child and will be transformed
- * too. In this case data class should be the class of the object, or null
- * when it is an array.
- *
- * All transformers are called in a configured order from model data to view value.
- * At the end of this chain the view data will be validated against the data class
- * setting.
- *
- * This method must be able to deal with empty values. Usually this will
- * be NULL, but depending on your implementation other empty values are
- * possible as well (such as empty strings). The reasoning behind this is
- * that data transformers must be chainable. If the transform() method
- * of the first data transformer outputs NULL, the second must be able to
- * process that value.
- *
- * @param TValue|null $value The value in the original representation
- *
- * @return TTransformedValue|null
- *
- * @throws TransformationFailedException when the transformation fails
- */
- public function transform(mixed $value): mixed;
- /**
- * Transforms a value from the transformed representation to its original
- * representation.
- *
- * This method is called when {@link Form::submit()} is called to transform the requests tainted data
- * into an acceptable format.
- *
- * The same transformers are called in the reverse order so the responsibility is to
- * return one of the types that would be expected as input of transform().
- *
- * This method must be able to deal with empty values. Usually this will
- * be an empty string, but depending on your implementation other empty
- * values are possible as well (such as NULL). The reasoning behind
- * this is that value transformers must be chainable. If the
- * reverseTransform() method of the first value transformer outputs an
- * empty string, the second value transformer must be able to process that
- * value.
- *
- * By convention, reverseTransform() should return NULL if an empty string
- * is passed.
- *
- * @param TTransformedValue|null $value The value in the transformed representation
- *
- * @return TValue|null
- *
- * @throws TransformationFailedException when the transformation fails
- */
- public function reverseTransform(mixed $value): mixed;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./SubmitButtonTypeInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * A type that should be converted into a {@link SubmitButton} instance.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface SubmitButtonTypeInterface extends FormTypeInterface
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ResolvedFormTypeFactoryInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * Creates ResolvedFormTypeInterface instances.
- *
- * This interface allows you to use your custom ResolvedFormTypeInterface
- * implementation, within which you can customize the concrete FormBuilderInterface
- * implementations or FormView subclasses that are used by the framework.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface ResolvedFormTypeFactoryInterface
- {
- /**
- * Resolves a form type.
- *
- * @param FormTypeExtensionInterface[] $typeExtensions
- *
- * @throws Exception\UnexpectedTypeException if the types parent {@link FormTypeInterface::getParent()} is not a string
- * @throws Exception\InvalidArgumentException if the types parent cannot be retrieved from any extension
- */
- public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Console/Helper/DescriptorHelper.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Console\Helper;
- use Symfony\Component\Console\Helper\DescriptorHelper as BaseDescriptorHelper;
- use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
- use Symfony\Component\Form\Console\Descriptor\JsonDescriptor;
- use Symfony\Component\Form\Console\Descriptor\TextDescriptor;
- /**
- * @author Yonel Ceruto <[email protected]>
- *
- * @internal
- */
- class DescriptorHelper extends BaseDescriptorHelper
- {
- public function __construct(?FileLinkFormatter $fileLinkFormatter = null)
- {
- $this
- ->register('txt', new TextDescriptor($fileLinkFormatter))
- ->register('json', new JsonDescriptor())
- ;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Console/Descriptor/JsonDescriptor.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Console\Descriptor;
- use Symfony\Component\Form\ResolvedFormTypeInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Yonel Ceruto <[email protected]>
- *
- * @internal
- */
- class JsonDescriptor extends Descriptor
- {
- protected function describeDefaults(array $options): void
- {
- $data['builtin_form_types'] = $options['core_types'];
- $data['service_form_types'] = $options['service_types'];
- if (!$options['show_deprecated']) {
- $data['type_extensions'] = $options['extensions'];
- $data['type_guessers'] = $options['guessers'];
- }
- $this->writeData($data, $options);
- }
- protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void
- {
- $this->collectOptions($resolvedFormType);
- if ($options['show_deprecated']) {
- $this->filterOptionsByDeprecated($resolvedFormType);
- }
- $formOptions = [
- 'own' => $this->ownOptions,
- 'overridden' => $this->overriddenOptions,
- 'parent' => $this->parentOptions,
- 'extension' => $this->extensionOptions,
- 'required' => $this->requiredOptions,
- ];
- $this->sortOptions($formOptions);
- $data = [
- 'class' => $resolvedFormType->getInnerType()::class,
- 'block_prefix' => $resolvedFormType->getInnerType()->getBlockPrefix(),
- 'options' => $formOptions,
- 'parent_types' => $this->parents,
- 'type_extensions' => $this->extensions,
- ];
- $this->writeData($data, $options);
- }
- protected function describeOption(OptionsResolver $optionsResolver, array $options): void
- {
- $definition = $this->getOptionDefinition($optionsResolver, $options['option']);
- $map = [];
- if ($definition['deprecated']) {
- $map['deprecated'] = 'deprecated';
- if (\is_string($definition['deprecationMessage'])) {
- $map['deprecation_message'] = 'deprecationMessage';
- }
- }
- $map += [
- 'info' => 'info',
- 'required' => 'required',
- 'default' => 'default',
- 'allowed_types' => 'allowedTypes',
- 'allowed_values' => 'allowedValues',
- ];
- foreach ($map as $label => $name) {
- if (\array_key_exists($name, $definition)) {
- $data[$label] = $definition[$name];
- if ('default' === $name) {
- $data['is_lazy'] = isset($definition['lazy']);
- }
- }
- }
- $data['has_normalizer'] = isset($definition['normalizers']);
- $this->writeData($data, $options);
- }
- private function writeData(array $data, array $options): void
- {
- $flags = $options['json_encoding'] ?? 0;
- $this->output->write(json_encode($data, $flags | \JSON_PRETTY_PRINT)."\n");
- }
- private function sortOptions(array &$options): void
- {
- foreach ($options as &$opts) {
- $sorted = false;
- foreach ($opts as &$opt) {
- if (\is_array($opt)) {
- sort($opt);
- $sorted = true;
- }
- }
- if (!$sorted) {
- sort($opts);
- }
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Console/Descriptor/TextDescriptor.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Console\Descriptor;
- use Symfony\Component\Console\Helper\Dumper;
- use Symfony\Component\Console\Helper\TableSeparator;
- use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
- use Symfony\Component\Form\ResolvedFormTypeInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Yonel Ceruto <[email protected]>
- *
- * @internal
- */
- class TextDescriptor extends Descriptor
- {
- public function __construct(
- private readonly ?FileLinkFormatter $fileLinkFormatter = null,
- ) {
- }
- protected function describeDefaults(array $options): void
- {
- if ($options['core_types']) {
- $this->output->section('Built-in form types (Symfony\Component\Form\Extension\Core\Type)');
- $shortClassNames = array_map(fn ($fqcn) => $this->formatClassLink($fqcn, \array_slice(explode('\\', $fqcn), -1)[0]), $options['core_types']);
- for ($i = 0, $loopsMax = \count($shortClassNames); $i * 5 < $loopsMax; ++$i) {
- $this->output->writeln(' '.implode(', ', \array_slice($shortClassNames, $i * 5, 5)));
- }
- }
- if ($options['service_types']) {
- $this->output->section('Service form types');
- $this->output->listing(array_map($this->formatClassLink(...), $options['service_types']));
- }
- if (!$options['show_deprecated']) {
- if ($options['extensions']) {
- $this->output->section('Type extensions');
- $this->output->listing(array_map($this->formatClassLink(...), $options['extensions']));
- }
- if ($options['guessers']) {
- $this->output->section('Type guessers');
- $this->output->listing(array_map($this->formatClassLink(...), $options['guessers']));
- }
- }
- }
- protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void
- {
- $this->collectOptions($resolvedFormType);
- if ($options['show_deprecated']) {
- $this->filterOptionsByDeprecated($resolvedFormType);
- }
- $formOptions = $this->normalizeAndSortOptionsColumns(array_filter([
- 'own' => $this->ownOptions,
- 'overridden' => $this->overriddenOptions,
- 'parent' => $this->parentOptions,
- 'extension' => $this->extensionOptions,
- ]));
- // setting headers and column order
- $tableHeaders = array_intersect_key([
- 'own' => 'Options',
- 'overridden' => 'Overridden options',
- 'parent' => 'Parent options',
- 'extension' => 'Extension options',
- ], $formOptions);
- $this->output->title(\sprintf('%s (Block prefix: "%s")', $resolvedFormType->getInnerType()::class, $resolvedFormType->getInnerType()->getBlockPrefix()));
- if ($formOptions) {
- $this->output->table($tableHeaders, $this->buildTableRows($tableHeaders, $formOptions));
- }
- if ($this->parents) {
- $this->output->section('Parent types');
- $this->output->listing(array_map($this->formatClassLink(...), $this->parents));
- }
- if ($this->extensions) {
- $this->output->section('Type extensions');
- $this->output->listing(array_map($this->formatClassLink(...), $this->extensions));
- }
- }
- protected function describeOption(OptionsResolver $optionsResolver, array $options): void
- {
- $definition = $this->getOptionDefinition($optionsResolver, $options['option']);
- $dump = new Dumper($this->output);
- $map = [];
- if ($definition['deprecated']) {
- $map = [
- 'Deprecated' => 'deprecated',
- 'Deprecation package' => 'deprecationPackage',
- 'Deprecation version' => 'deprecationVersion',
- 'Deprecation message' => 'deprecationMessage',
- ];
- }
- $map += [
- 'Info' => 'info',
- 'Required' => 'required',
- 'Default' => 'default',
- 'Allowed types' => 'allowedTypes',
- 'Allowed values' => 'allowedValues',
- 'Normalizers' => 'normalizers',
- ];
- $rows = [];
- foreach ($map as $label => $name) {
- $value = \array_key_exists($name, $definition) ? $dump($definition[$name]) : '-';
- if ('default' === $name && isset($definition['lazy'])) {
- $value = "Value: $value\n\nClosure(s): ".$dump($definition['lazy']);
- }
- $rows[] = ["<info>$label</info>", $value];
- $rows[] = new TableSeparator();
- }
- array_pop($rows);
- $this->output->title(\sprintf('%s (%s)', $options['type']::class, $options['option']));
- $this->output->table([], $rows);
- }
- private function buildTableRows(array $headers, array $options): array
- {
- $tableRows = [];
- $count = \count(max($options));
- for ($i = 0; $i < $count; ++$i) {
- $cells = [];
- foreach (array_keys($headers) as $group) {
- $option = $options[$group][$i] ?? null;
- if (\is_string($option) && \in_array($option, $this->requiredOptions, true)) {
- $option .= ' <info>(required)</info>';
- }
- $cells[] = $option;
- }
- $tableRows[] = $cells;
- }
- return $tableRows;
- }
- private function normalizeAndSortOptionsColumns(array $options): array
- {
- foreach ($options as $group => $opts) {
- $sorted = false;
- foreach ($opts as $class => $opt) {
- if (\is_string($class)) {
- unset($options[$group][$class]);
- }
- if (!\is_array($opt) || 0 === \count($opt)) {
- continue;
- }
- if (!$sorted) {
- $options[$group] = [];
- } else {
- $options[$group][] = null;
- }
- $options[$group][] = \sprintf('<info>%s</info>', (new \ReflectionClass($class))->getShortName());
- $options[$group][] = new TableSeparator();
- sort($opt);
- $sorted = true;
- $options[$group] = array_merge($options[$group], $opt);
- }
- if (!$sorted) {
- sort($options[$group]);
- }
- }
- return $options;
- }
- private function formatClassLink(string $class, ?string $text = null): string
- {
- $text ??= $class;
- if ('' === $fileLink = $this->getFileLink($class)) {
- return $text;
- }
- return \sprintf('<href=%s>%s</>', $fileLink, $text);
- }
- private function getFileLink(string $class): string
- {
- if (null === $this->fileLinkFormatter) {
- return '';
- }
- try {
- $r = new \ReflectionClass($class);
- } catch (\ReflectionException) {
- return '';
- }
- return (string) $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine());
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Console/Descriptor/Descriptor.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Console\Descriptor;
- use Symfony\Component\Console\Descriptor\DescriptorInterface;
- use Symfony\Component\Console\Input\ArrayInput;
- use Symfony\Component\Console\Output\OutputInterface;
- use Symfony\Component\Console\Style\OutputStyle;
- use Symfony\Component\Console\Style\SymfonyStyle;
- use Symfony\Component\Form\ResolvedFormTypeInterface;
- use Symfony\Component\Form\Util\OptionsResolverWrapper;
- use Symfony\Component\OptionsResolver\Debug\OptionsResolverIntrospector;
- use Symfony\Component\OptionsResolver\Exception\NoConfigurationException;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Yonel Ceruto <[email protected]>
- *
- * @internal
- */
- abstract class Descriptor implements DescriptorInterface
- {
- protected OutputStyle $output;
- protected array $ownOptions = [];
- protected array $overriddenOptions = [];
- protected array $parentOptions = [];
- protected array $extensionOptions = [];
- protected array $requiredOptions = [];
- protected array $parents = [];
- protected array $extensions = [];
- public function describe(OutputInterface $output, ?object $object, array $options = []): void
- {
- $this->output = $output instanceof OutputStyle ? $output : new SymfonyStyle(new ArrayInput([]), $output);
- match (true) {
- null === $object => $this->describeDefaults($options),
- $object instanceof ResolvedFormTypeInterface => $this->describeResolvedFormType($object, $options),
- $object instanceof OptionsResolver => $this->describeOption($object, $options),
- default => throw new \InvalidArgumentException(\sprintf('Object of type "%s" is not describable.', get_debug_type($object))),
- };
- }
- abstract protected function describeDefaults(array $options): void;
- abstract protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []): void;
- abstract protected function describeOption(OptionsResolver $optionsResolver, array $options): void;
- protected function collectOptions(ResolvedFormTypeInterface $type): void
- {
- $this->parents = [];
- $this->extensions = [];
- if (null !== $type->getParent()) {
- $optionsResolver = clone $this->getParentOptionsResolver($type->getParent());
- } else {
- $optionsResolver = new OptionsResolver();
- }
- $type->getInnerType()->configureOptions($ownOptionsResolver = new OptionsResolverWrapper());
- $this->ownOptions = array_diff($ownOptionsResolver->getDefinedOptions(), $optionsResolver->getDefinedOptions());
- $overriddenOptions = array_intersect(array_merge($ownOptionsResolver->getDefinedOptions(), $ownOptionsResolver->getUndefinedOptions()), $optionsResolver->getDefinedOptions());
- $this->parentOptions = [];
- foreach ($this->parents as $class => $parentOptions) {
- $this->overriddenOptions[$class] = array_intersect($overriddenOptions, $parentOptions);
- $this->parentOptions[$class] = array_diff($parentOptions, $overriddenOptions);
- }
- $type->getInnerType()->configureOptions($optionsResolver);
- $this->collectTypeExtensionsOptions($type, $optionsResolver);
- $this->extensionOptions = [];
- foreach ($this->extensions as $class => $extensionOptions) {
- $this->overriddenOptions[$class] = array_intersect($overriddenOptions, $extensionOptions);
- $this->extensionOptions[$class] = array_diff($extensionOptions, $overriddenOptions);
- }
- $this->overriddenOptions = array_filter($this->overriddenOptions);
- $this->parentOptions = array_filter($this->parentOptions);
- $this->extensionOptions = array_filter($this->extensionOptions);
- $this->requiredOptions = $optionsResolver->getRequiredOptions();
- $this->parents = array_keys($this->parents);
- $this->extensions = array_keys($this->extensions);
- }
- protected function getOptionDefinition(OptionsResolver $optionsResolver, string $option): array
- {
- $definition = [];
- if ($info = $optionsResolver->getInfo($option)) {
- $definition = [
- 'info' => $info,
- ];
- }
- $definition += [
- 'required' => $optionsResolver->isRequired($option),
- 'deprecated' => $optionsResolver->isDeprecated($option),
- ];
- $introspector = new OptionsResolverIntrospector($optionsResolver);
- $map = [
- 'default' => 'getDefault',
- 'lazy' => 'getLazyClosures',
- 'allowedTypes' => 'getAllowedTypes',
- 'allowedValues' => 'getAllowedValues',
- 'normalizers' => 'getNormalizers',
- 'deprecation' => 'getDeprecation',
- ];
- foreach ($map as $key => $method) {
- try {
- $definition[$key] = $introspector->{$method}($option);
- } catch (NoConfigurationException) {
- // noop
- }
- }
- if (isset($definition['deprecation']['message']) && \is_string($definition['deprecation']['message'])) {
- $definition['deprecationMessage'] = strtr($definition['deprecation']['message'], ['%name%' => $option]);
- $definition['deprecationPackage'] = $definition['deprecation']['package'];
- $definition['deprecationVersion'] = $definition['deprecation']['version'];
- }
- return $definition;
- }
- protected function filterOptionsByDeprecated(ResolvedFormTypeInterface $type): void
- {
- $deprecatedOptions = [];
- $resolver = $type->getOptionsResolver();
- foreach ($resolver->getDefinedOptions() as $option) {
- if ($resolver->isDeprecated($option)) {
- $deprecatedOptions[] = $option;
- }
- }
- $filterByDeprecated = static function (array $options) use ($deprecatedOptions) {
- foreach ($options as $class => $opts) {
- if ($deprecated = array_intersect($deprecatedOptions, $opts)) {
- $options[$class] = $deprecated;
- } else {
- unset($options[$class]);
- }
- }
- return $options;
- };
- $this->ownOptions = array_intersect($deprecatedOptions, $this->ownOptions);
- $this->overriddenOptions = $filterByDeprecated($this->overriddenOptions);
- $this->parentOptions = $filterByDeprecated($this->parentOptions);
- $this->extensionOptions = $filterByDeprecated($this->extensionOptions);
- }
- private function getParentOptionsResolver(ResolvedFormTypeInterface $type): OptionsResolver
- {
- $this->parents[$class = $type->getInnerType()::class] = [];
- if (null !== $type->getParent()) {
- $optionsResolver = clone $this->getParentOptionsResolver($type->getParent());
- } else {
- $optionsResolver = new OptionsResolver();
- }
- $inheritedOptions = $optionsResolver->getDefinedOptions();
- $type->getInnerType()->configureOptions($optionsResolver);
- $this->parents[$class] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions);
- $this->collectTypeExtensionsOptions($type, $optionsResolver);
- return $optionsResolver;
- }
- private function collectTypeExtensionsOptions(ResolvedFormTypeInterface $type, OptionsResolver $optionsResolver): void
- {
- foreach ($type->getTypeExtensions() as $extension) {
- $inheritedOptions = $optionsResolver->getDefinedOptions();
- $extension->configureOptions($optionsResolver);
- $this->extensions[$extension::class] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions);
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ClickableInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * A clickable form element.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface ClickableInterface
- {
- /**
- * Returns whether this element was clicked.
- */
- public function isClicked(): bool;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ButtonTypeInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * A type that should be converted into a {@link Button} instance.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface ButtonTypeInterface extends FormTypeInterface
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/DependencyInjection/DependencyInjectionExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\DependencyInjection;
- use Psr\Container\ContainerInterface;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- use Symfony\Component\Form\FormExtensionInterface;
- use Symfony\Component\Form\FormTypeExtensionInterface;
- use Symfony\Component\Form\FormTypeGuesserChain;
- use Symfony\Component\Form\FormTypeGuesserInterface;
- use Symfony\Component\Form\FormTypeInterface;
- class DependencyInjectionExtension implements FormExtensionInterface
- {
- private ?FormTypeGuesserChain $guesser = null;
- private bool $guesserLoaded = false;
- /**
- * @param array<string, iterable<FormTypeExtensionInterface>> $typeExtensionServices
- */
- public function __construct(
- private ContainerInterface $typeContainer,
- private array $typeExtensionServices,
- private iterable $guesserServices,
- ) {
- }
- public function getType(string $name): FormTypeInterface
- {
- if (!$this->typeContainer->has($name)) {
- throw new InvalidArgumentException(\sprintf('The field type "%s" is not registered in the service container.', $name));
- }
- return $this->typeContainer->get($name);
- }
- public function hasType(string $name): bool
- {
- return $this->typeContainer->has($name);
- }
- public function getTypeExtensions(string $name): array
- {
- $extensions = [];
- if (isset($this->typeExtensionServices[$name])) {
- foreach ($this->typeExtensionServices[$name] as $extension) {
- $extensions[] = $extension;
- $extendedTypes = [];
- foreach ($extension::getExtendedTypes() as $extendedType) {
- $extendedTypes[] = $extendedType;
- }
- // validate the result of getExtendedTypes() to ensure it is consistent with the service definition
- if (!\in_array($name, $extendedTypes, true)) {
- 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)));
- }
- }
- }
- return $extensions;
- }
- public function hasTypeExtensions(string $name): bool
- {
- return isset($this->typeExtensionServices[$name]);
- }
- public function getTypeGuesser(): ?FormTypeGuesserInterface
- {
- if (!$this->guesserLoaded) {
- $this->guesserLoaded = true;
- $guessers = [];
- foreach ($this->guesserServices as $serviceId => $service) {
- $guessers[] = $service;
- }
- if ($guessers) {
- $this->guesser = new FormTypeGuesserChain($guessers);
- }
- }
- return $this->guesser;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/DataCollector/DataCollectorExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\DataCollector;
- use Symfony\Component\Form\AbstractExtension;
- /**
- * Extension for collecting data of the forms on a page.
- *
- * @author Robert Schönthal <[email protected]>
- * @author Bernhard Schussek <[email protected]>
- */
- class DataCollectorExtension extends AbstractExtension
- {
- public function __construct(
- private FormDataCollectorInterface $dataCollector,
- ) {
- }
- protected function loadTypeExtensions(): array
- {
- return [
- new Type\DataCollectorTypeExtension($this->dataCollector),
- ];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\DataCollector\Proxy;
- use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
- use Symfony\Component\Form\FormTypeInterface;
- use Symfony\Component\Form\ResolvedFormTypeFactoryInterface;
- use Symfony\Component\Form\ResolvedFormTypeInterface;
- /**
- * Proxy that wraps resolved types into {@link ResolvedTypeDataCollectorProxy}
- * instances.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ResolvedTypeFactoryDataCollectorProxy implements ResolvedFormTypeFactoryInterface
- {
- public function __construct(
- private ResolvedFormTypeFactoryInterface $proxiedFactory,
- private FormDataCollectorInterface $dataCollector,
- ) {
- }
- public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface
- {
- return new ResolvedTypeDataCollectorProxy(
- $this->proxiedFactory->createResolvedType($type, $typeExtensions, $parent),
- $this->dataCollector
- );
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\DataCollector\Proxy;
- use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormFactoryInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormTypeInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\Form\ResolvedFormTypeInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * Proxy that invokes a data collector when creating a form and its view.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ResolvedTypeDataCollectorProxy implements ResolvedFormTypeInterface
- {
- public function __construct(
- private ResolvedFormTypeInterface $proxiedType,
- private FormDataCollectorInterface $dataCollector,
- ) {
- }
- public function getBlockPrefix(): string
- {
- return $this->proxiedType->getBlockPrefix();
- }
- public function getParent(): ?ResolvedFormTypeInterface
- {
- return $this->proxiedType->getParent();
- }
- public function getInnerType(): FormTypeInterface
- {
- return $this->proxiedType->getInnerType();
- }
- public function getTypeExtensions(): array
- {
- return $this->proxiedType->getTypeExtensions();
- }
- public function createBuilder(FormFactoryInterface $factory, string $name, array $options = []): FormBuilderInterface
- {
- $builder = $this->proxiedType->createBuilder($factory, $name, $options);
- $builder->setAttribute('data_collector/passed_options', $options);
- $builder->setType($this);
- return $builder;
- }
- public function createView(FormInterface $form, ?FormView $parent = null): FormView
- {
- return $this->proxiedType->createView($form, $parent);
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $this->proxiedType->buildForm($builder, $options);
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $this->proxiedType->buildView($view, $form, $options);
- }
- public function finishView(FormView $view, FormInterface $form, array $options): void
- {
- $this->proxiedType->finishView($view, $form, $options);
- // Remember which view belongs to which form instance, so that we can
- // get the collected data for a view when its form instance is not
- // available (e.g. CSRF token)
- $this->dataCollector->associateFormWithView($form, $view);
- // Since the CSRF token is only present in the FormView tree, we also
- // need to check the FormView tree instead of calling isRoot() on the
- // FormInterface tree
- if (null === $view->parent) {
- $this->dataCollector->collectViewVariables($view);
- // Re-assemble data, in case FormView instances were added, for
- // which no FormInterface instances were present (e.g. CSRF token).
- // Since finishView() is called after finishing the views of all
- // children, we can safely assume that information has been
- // collected about the complete form tree.
- $this->dataCollector->buildFinalFormTree($form, $view);
- }
- }
- public function getOptionsResolver(): OptionsResolver
- {
- return $this->proxiedType->getOptionsResolver();
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/DataCollector/Type/DataCollectorTypeExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\DataCollector\Type;
- use Symfony\Component\Form\AbstractTypeExtension;
- use Symfony\Component\Form\Extension\Core\Type\FormType;
- use Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener;
- use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
- use Symfony\Component\Form\FormBuilderInterface;
- /**
- * Type extension for collecting data of a form with this type.
- *
- * @author Robert Schönthal <[email protected]>
- * @author Bernhard Schussek <[email protected]>
- */
- class DataCollectorTypeExtension extends AbstractTypeExtension
- {
- private DataCollectorListener $listener;
- public function __construct(FormDataCollectorInterface $dataCollector)
- {
- $this->listener = new DataCollectorListener($dataCollector);
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $builder->addEventSubscriber($this->listener);
- }
- public static function getExtendedTypes(): iterable
- {
- return [FormType::class];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/DataCollector/EventListener/DataCollectorListener.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\DataCollector\EventListener;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- /**
- * Listener that invokes a data collector for the {@link FormEvents::POST_SET_DATA}
- * and {@link FormEvents::POST_SUBMIT} events.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class DataCollectorListener implements EventSubscriberInterface
- {
- public function __construct(
- private FormDataCollectorInterface $dataCollector,
- ) {
- }
- public static function getSubscribedEvents(): array
- {
- return [
- // Low priority in order to be called as late as possible
- FormEvents::POST_SET_DATA => ['postSetData', -255],
- // Low priority in order to be called as late as possible
- FormEvents::POST_SUBMIT => ['postSubmit', -255],
- ];
- }
- /**
- * Listener for the {@link FormEvents::POST_SET_DATA} event.
- */
- public function postSetData(FormEvent $event): void
- {
- if ($event->getForm()->isRoot()) {
- // Collect basic information about each form
- $this->dataCollector->collectConfiguration($event->getForm());
- // Collect the default data
- $this->dataCollector->collectDefaultData($event->getForm());
- }
- }
- /**
- * Listener for the {@link FormEvents::POST_SUBMIT} event.
- */
- public function postSubmit(FormEvent $event): void
- {
- if ($event->getForm()->isRoot()) {
- // Collect the submitted data of each form
- $this->dataCollector->collectSubmittedData($event->getForm());
- // Assemble a form tree
- // This is done again after the view is built, but we need it here as the view is not always created.
- $this->dataCollector->buildPreliminaryFormTree($event->getForm());
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/DataCollector/FormDataExtractor.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\DataCollector;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\Validator\ConstraintViolationInterface;
- /**
- * Default implementation of {@link FormDataExtractorInterface}.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class FormDataExtractor implements FormDataExtractorInterface
- {
- public function extractConfiguration(FormInterface $form): array
- {
- $data = [
- 'id' => $this->buildId($form),
- 'name' => $form->getName(),
- 'type_class' => $form->getConfig()->getType()->getInnerType()::class,
- 'synchronized' => $form->isSynchronized(),
- 'passed_options' => [],
- 'resolved_options' => [],
- ];
- foreach ($form->getConfig()->getAttribute('data_collector/passed_options', []) as $option => $value) {
- $data['passed_options'][$option] = $value;
- }
- foreach ($form->getConfig()->getOptions() as $option => $value) {
- $data['resolved_options'][$option] = $value;
- }
- ksort($data['passed_options']);
- ksort($data['resolved_options']);
- return $data;
- }
- public function extractDefaultData(FormInterface $form): array
- {
- $data = [
- 'default_data' => [
- 'norm' => $form->getNormData(),
- ],
- 'submitted_data' => [],
- ];
- if ($form->getData() !== $form->getNormData()) {
- $data['default_data']['model'] = $form->getData();
- }
- if ($form->getViewData() !== $form->getNormData()) {
- $data['default_data']['view'] = $form->getViewData();
- }
- return $data;
- }
- public function extractSubmittedData(FormInterface $form): array
- {
- $data = [
- 'submitted_data' => [
- 'norm' => $form->getNormData(),
- ],
- 'errors' => [],
- ];
- if ($form->getViewData() !== $form->getNormData()) {
- $data['submitted_data']['view'] = $form->getViewData();
- }
- if ($form->getData() !== $form->getNormData()) {
- $data['submitted_data']['model'] = $form->getData();
- }
- foreach ($form->getErrors() as $error) {
- $errorData = [
- 'message' => $error->getMessage(),
- 'origin' => \is_object($error->getOrigin())
- ? spl_object_hash($error->getOrigin())
- : null,
- 'trace' => [],
- ];
- $cause = $error->getCause();
- while (null !== $cause) {
- if ($cause instanceof ConstraintViolationInterface) {
- $errorData['trace'][] = $cause;
- $cause = method_exists($cause, 'getCause') ? $cause->getCause() : null;
- continue;
- }
- $errorData['trace'][] = $cause;
- if ($cause instanceof \Exception) {
- $cause = $cause->getPrevious();
- continue;
- }
- break;
- }
- $data['errors'][] = $errorData;
- }
- $data['synchronized'] = $form->isSynchronized();
- return $data;
- }
- public function extractViewVariables(FormView $view): array
- {
- $data = [
- 'id' => $view->vars['id'] ?? null,
- 'name' => $view->vars['name'] ?? null,
- 'view_vars' => [],
- ];
- foreach ($view->vars as $varName => $value) {
- $data['view_vars'][$varName] = $value;
- }
- ksort($data['view_vars']);
- return $data;
- }
- /**
- * Recursively builds an HTML ID for a form.
- */
- private function buildId(FormInterface $form): string
- {
- $id = $form->getName();
- if (null !== $form->getParent()) {
- $id = $this->buildId($form->getParent()).'_'.$id;
- }
- return $id;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/DataCollector/FormDataCollectorInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\DataCollector;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
- use Symfony\Component\VarDumper\Cloner\Data;
- /**
- * Collects and structures information about forms.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormDataCollectorInterface extends DataCollectorInterface
- {
- /**
- * Stores configuration data of the given form and its children.
- */
- public function collectConfiguration(FormInterface $form): void;
- /**
- * Stores the default data of the given form and its children.
- */
- public function collectDefaultData(FormInterface $form): void;
- /**
- * Stores the submitted data of the given form and its children.
- */
- public function collectSubmittedData(FormInterface $form): void;
- /**
- * Stores the view variables of the given form view and its children.
- */
- public function collectViewVariables(FormView $view): void;
- /**
- * Specifies that the given objects represent the same conceptual form.
- */
- public function associateFormWithView(FormInterface $form, FormView $view): void;
- /**
- * Assembles the data collected about the given form and its children as
- * a tree-like data structure.
- *
- * The result can be queried using {@link getData()}.
- */
- public function buildPreliminaryFormTree(FormInterface $form): void;
- /**
- * Assembles the data collected about the given form and its children as
- * a tree-like data structure.
- *
- * The result can be queried using {@link getData()}.
- *
- * Contrary to {@link buildPreliminaryFormTree()}, a {@link FormView}
- * object has to be passed. The tree structure of this view object will be
- * used for structuring the resulting data. That means, if a child is
- * present in the view, but not in the form, it will be present in the final
- * data array anyway.
- *
- * When {@link FormView} instances are present in the view tree, for which
- * no corresponding {@link FormInterface} objects can be found in the form
- * tree, only the view data will be included in the result. If a
- * corresponding {@link FormInterface} exists otherwise, call
- * {@link associateFormWithView()} before calling this method.
- */
- public function buildFinalFormTree(FormInterface $form, FormView $view): void;
- /**
- * Returns all collected data.
- */
- public function getData(): array|Data;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/DataCollector/FormDataCollector.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\DataCollector;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\HttpFoundation\Request;
- use Symfony\Component\HttpFoundation\Response;
- use Symfony\Component\HttpKernel\DataCollector\DataCollector;
- use Symfony\Component\Validator\ConstraintViolationInterface;
- use Symfony\Component\VarDumper\Caster\Caster;
- use Symfony\Component\VarDumper\Caster\ClassStub;
- use Symfony\Component\VarDumper\Caster\StubCaster;
- use Symfony\Component\VarDumper\Cloner\Data;
- use Symfony\Component\VarDumper\Cloner\Stub;
- /**
- * Data collector for {@link FormInterface} instances.
- *
- * @author Robert Schönthal <[email protected]>
- * @author Bernhard Schussek <[email protected]>
- *
- * @final
- */
- class FormDataCollector extends DataCollector implements FormDataCollectorInterface
- {
- /**
- * Stores the collected data per {@link FormInterface} instance.
- *
- * Uses the hashes of the forms as keys. This is preferable over using
- * {@link \SplObjectStorage}, because in this way no references are kept
- * to the {@link FormInterface} instances.
- */
- private array $dataByForm;
- /**
- * Stores the collected data per {@link FormView} instance.
- *
- * Uses the hashes of the views as keys. This is preferable over using
- * {@link \SplObjectStorage}, because in this way no references are kept
- * to the {@link FormView} instances.
- */
- private array $dataByView;
- /**
- * Connects {@link FormView} with {@link FormInterface} instances.
- *
- * Uses the hashes of the views as keys and the hashes of the forms as
- * values. This is preferable over storing the objects directly, because
- * this way they can safely be discarded by the GC.
- */
- private array $formsByView;
- public function __construct(
- private FormDataExtractorInterface $dataExtractor,
- ) {
- if (!class_exists(ClassStub::class)) {
- throw new \LogicException(\sprintf('The VarDumper component is needed for using the "%s" class. Install symfony/var-dumper version 3.4 or above.', __CLASS__));
- }
- $this->reset();
- }
- /**
- * Does nothing. The data is collected during the form event listeners.
- */
- public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
- {
- }
- public function reset(): void
- {
- $this->data = [
- 'forms' => [],
- 'forms_by_hash' => [],
- 'nb_errors' => 0,
- ];
- }
- public function associateFormWithView(FormInterface $form, FormView $view): void
- {
- $this->formsByView[spl_object_hash($view)] = spl_object_hash($form);
- }
- public function collectConfiguration(FormInterface $form): void
- {
- $hash = spl_object_hash($form);
- if (!isset($this->dataByForm[$hash])) {
- $this->dataByForm[$hash] = [];
- }
- $this->dataByForm[$hash] = array_replace(
- $this->dataByForm[$hash],
- $this->dataExtractor->extractConfiguration($form)
- );
- foreach ($form as $child) {
- $this->collectConfiguration($child);
- }
- }
- public function collectDefaultData(FormInterface $form): void
- {
- $hash = spl_object_hash($form);
- if (!isset($this->dataByForm[$hash])) {
- // field was created by form event
- $this->collectConfiguration($form);
- }
- $this->dataByForm[$hash] = array_replace(
- $this->dataByForm[$hash],
- $this->dataExtractor->extractDefaultData($form)
- );
- foreach ($form as $child) {
- $this->collectDefaultData($child);
- }
- }
- public function collectSubmittedData(FormInterface $form): void
- {
- $hash = spl_object_hash($form);
- if (!isset($this->dataByForm[$hash])) {
- // field was created by form event
- $this->collectConfiguration($form);
- $this->collectDefaultData($form);
- }
- $this->dataByForm[$hash] = array_replace(
- $this->dataByForm[$hash],
- $this->dataExtractor->extractSubmittedData($form)
- );
- // Count errors
- if (isset($this->dataByForm[$hash]['errors'])) {
- $this->data['nb_errors'] += \count($this->dataByForm[$hash]['errors']);
- }
- foreach ($form as $child) {
- $this->collectSubmittedData($child);
- // Expand current form if there are children with errors
- if (empty($this->dataByForm[$hash]['has_children_error'])) {
- $childData = $this->dataByForm[spl_object_hash($child)];
- $this->dataByForm[$hash]['has_children_error'] = !empty($childData['has_children_error']) || !empty($childData['errors']);
- }
- }
- }
- public function collectViewVariables(FormView $view): void
- {
- $hash = spl_object_hash($view);
- if (!isset($this->dataByView[$hash])) {
- $this->dataByView[$hash] = [];
- }
- $this->dataByView[$hash] = array_replace(
- $this->dataByView[$hash],
- $this->dataExtractor->extractViewVariables($view)
- );
- foreach ($view->children as $child) {
- $this->collectViewVariables($child);
- }
- }
- public function buildPreliminaryFormTree(FormInterface $form): void
- {
- $this->data['forms'][$form->getName()] = &$this->recursiveBuildPreliminaryFormTree($form, $this->data['forms_by_hash']);
- }
- public function buildFinalFormTree(FormInterface $form, FormView $view): void
- {
- $this->data['forms'][$form->getName()] = &$this->recursiveBuildFinalFormTree($form, $view, $this->data['forms_by_hash']);
- }
- public function getName(): string
- {
- return 'form';
- }
- public function getData(): array|Data
- {
- return $this->data;
- }
- /**
- * @internal
- */
- public function __sleep(): array
- {
- foreach ($this->data['forms_by_hash'] as &$form) {
- if (isset($form['type_class']) && !$form['type_class'] instanceof ClassStub) {
- $form['type_class'] = new ClassStub($form['type_class']);
- }
- }
- $this->data = $this->cloneVar($this->data);
- return parent::__sleep();
- }
- protected function getCasters(): array
- {
- return parent::getCasters() + [
- \Exception::class => static function (\Exception $e, array $a, Stub $s) {
- foreach (["\0Exception\0previous", "\0Exception\0trace"] as $k) {
- if (isset($a[$k])) {
- unset($a[$k]);
- ++$s->cut;
- }
- }
- return $a;
- },
- FormInterface::class => static fn (FormInterface $f, array $a) => [
- Caster::PREFIX_VIRTUAL.'name' => $f->getName(),
- Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub($f->getConfig()->getType()->getInnerType()::class),
- ],
- FormView::class => StubCaster::cutInternals(...),
- ConstraintViolationInterface::class => static fn (ConstraintViolationInterface $v, array $a) => [
- Caster::PREFIX_VIRTUAL.'root' => $v->getRoot(),
- Caster::PREFIX_VIRTUAL.'path' => $v->getPropertyPath(),
- Caster::PREFIX_VIRTUAL.'value' => $v->getInvalidValue(),
- ],
- ];
- }
- private function &recursiveBuildPreliminaryFormTree(FormInterface $form, array &$outputByHash): array
- {
- $hash = spl_object_hash($form);
- $output = &$outputByHash[$hash];
- $output = $this->dataByForm[$hash]
- ?? [];
- $output['children'] = [];
- foreach ($form as $name => $child) {
- $output['children'][$name] = &$this->recursiveBuildPreliminaryFormTree($child, $outputByHash);
- }
- return $output;
- }
- private function &recursiveBuildFinalFormTree(?FormInterface $form, FormView $view, array &$outputByHash): array
- {
- $viewHash = spl_object_hash($view);
- $formHash = null;
- if (null !== $form) {
- $formHash = spl_object_hash($form);
- } elseif (isset($this->formsByView[$viewHash])) {
- // The FormInterface instance of the CSRF token is never contained in
- // the FormInterface tree of the form, so we need to get the
- // corresponding FormInterface instance for its view in a different way
- $formHash = $this->formsByView[$viewHash];
- }
- if (null !== $formHash) {
- $output = &$outputByHash[$formHash];
- }
- $output = $this->dataByView[$viewHash]
- ?? [];
- if (null !== $formHash) {
- $output = array_replace(
- $output,
- $this->dataByForm[$formHash]
- ?? []
- );
- }
- $output['children'] = [];
- foreach ($view->children as $name => $childView) {
- // The CSRF token, for example, is never added to the form tree.
- // It is only present in the view.
- $childForm = $form?->has($name) ? $form->get($name) : null;
- $output['children'][$name] = &$this->recursiveBuildFinalFormTree($childForm, $childView, $outputByHash);
- }
- return $output;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/DataCollector/FormDataExtractorInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\DataCollector;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- /**
- * Extracts arrays of information out of forms.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormDataExtractorInterface
- {
- /**
- * Extracts the configuration data of a form.
- */
- public function extractConfiguration(FormInterface $form): array;
- /**
- * Extracts the default data of a form.
- */
- public function extractDefaultData(FormInterface $form): array;
- /**
- * Extracts the submitted data of a form.
- */
- public function extractSubmittedData(FormInterface $form): array;
- /**
- * Extracts the view variables of a form.
- */
- public function extractViewVariables(FormView $view): array;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/HttpFoundation/HttpFoundationRequestHandler.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\HttpFoundation;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- use Symfony\Component\Form\FormError;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\RequestHandlerInterface;
- use Symfony\Component\Form\Util\FormUtil;
- use Symfony\Component\Form\Util\ServerParams;
- use Symfony\Component\HttpFoundation\File\File;
- use Symfony\Component\HttpFoundation\File\UploadedFile;
- use Symfony\Component\HttpFoundation\Request;
- /**
- * A request processor using the {@link Request} class of the HttpFoundation
- * component.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class HttpFoundationRequestHandler implements RequestHandlerInterface
- {
- private ServerParams $serverParams;
- public function __construct(?ServerParams $serverParams = null)
- {
- $this->serverParams = $serverParams ?? new ServerParams();
- }
- public function handleRequest(FormInterface $form, mixed $request = null): void
- {
- if (!$request instanceof Request) {
- throw new UnexpectedTypeException($request, Request::class);
- }
- $name = $form->getName();
- $method = $form->getConfig()->getMethod();
- if ($method !== $request->getMethod()) {
- return;
- }
- // For request methods that must not have a request body we fetch data
- // from the query string. Otherwise we look for data in the request body.
- if ('GET' === $method || 'HEAD' === $method || 'TRACE' === $method) {
- if ('' === $name) {
- $data = $request->query->all();
- } else {
- // Don't submit GET requests if the form's name does not exist
- // in the request
- if (!$request->query->has($name)) {
- return;
- }
- $data = $request->query->all()[$name];
- }
- } else {
- // Mark the form with an error if the uploaded size was too large
- // This is done here and not in FormValidator because $_POST is
- // empty when that error occurs. Hence the form is never submitted.
- if ($this->serverParams->hasPostMaxSizeBeenExceeded()) {
- // Submit the form, but don't clear the default values
- $form->submit(null, false);
- $form->addError(new FormError(
- $form->getConfig()->getOption('upload_max_size_message')(),
- null,
- ['{{ max }}' => $this->serverParams->getNormalizedIniPostMaxSize()]
- ));
- return;
- }
- if ('' === $name) {
- $params = $request->request->all();
- $files = $request->files->all();
- } elseif ($request->request->has($name) || $request->files->has($name)) {
- $default = $form->getConfig()->getCompound() ? [] : null;
- $params = $request->request->all()[$name] ?? $default;
- $files = $request->files->get($name, $default);
- } else {
- // Don't submit the form if it is not present in the request
- return;
- }
- if (\is_array($params) && \is_array($files)) {
- $data = FormUtil::mergeParamsAndFiles($params, $files);
- } else {
- $data = $params ?: $files;
- }
- }
- // Don't auto-submit the form unless at least one field is present.
- if ('' === $name && \count(array_intersect_key($data, $form->all())) <= 0) {
- return;
- }
- $form->submit($data, 'PATCH' !== $method);
- }
- public function isFileUpload(mixed $data): bool
- {
- return $data instanceof File;
- }
- public function getUploadFileError(mixed $data): ?int
- {
- if (!$data instanceof UploadedFile || $data->isValid()) {
- return null;
- }
- return $data->getError();
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/HttpFoundation/Type/FormTypeHttpFoundationExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\HttpFoundation\Type;
- use Symfony\Component\Form\AbstractTypeExtension;
- use Symfony\Component\Form\Extension\Core\Type\FormType;
- use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\RequestHandlerInterface;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class FormTypeHttpFoundationExtension extends AbstractTypeExtension
- {
- private RequestHandlerInterface $requestHandler;
- public function __construct(?RequestHandlerInterface $requestHandler = null)
- {
- $this->requestHandler = $requestHandler ?? new HttpFoundationRequestHandler();
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $builder->setRequestHandler($this->requestHandler);
- }
- public static function getExtendedTypes(): iterable
- {
- return [FormType::class];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/HttpFoundation/HttpFoundationExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\HttpFoundation;
- use Symfony\Component\Form\AbstractExtension;
- /**
- * Integrates the HttpFoundation component with the Form library.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class HttpFoundationExtension extends AbstractExtension
- {
- protected function loadTypeExtensions(): array
- {
- return [
- new Type\FormTypeHttpFoundationExtension(),
- ];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/HtmlSanitizer/Type/TextTypeHtmlSanitizerExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\HtmlSanitizer\Type;
- use Psr\Container\ContainerInterface;
- use Symfony\Component\Form\AbstractTypeExtension;
- use Symfony\Component\Form\Extension\Core\Type\TextType;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Titouan Galopin <[email protected]>
- */
- class TextTypeHtmlSanitizerExtension extends AbstractTypeExtension
- {
- public function __construct(
- private ContainerInterface $sanitizers,
- private string $defaultSanitizer = 'default',
- ) {
- }
- public static function getExtendedTypes(): iterable
- {
- return [TextType::class];
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver
- ->setDefaults(['sanitize_html' => false, 'sanitizer' => null])
- ->setAllowedTypes('sanitize_html', 'bool')
- ->setAllowedTypes('sanitizer', ['string', 'null'])
- ;
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- if (!$options['sanitize_html']) {
- return;
- }
- $sanitizers = $this->sanitizers;
- $sanitizer = $options['sanitizer'] ?? $this->defaultSanitizer;
- $builder->addEventListener(
- FormEvents::PRE_SUBMIT,
- static function (FormEvent $event) use ($sanitizers, $sanitizer) {
- if (\is_scalar($data = $event->getData()) && '' !== trim($data)) {
- $event->setData($sanitizers->get($sanitizer)->sanitize($data));
- }
- },
- 10000 /* as soon as possible */
- );
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/HtmlSanitizer/HtmlSanitizerExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\HtmlSanitizer;
- use Psr\Container\ContainerInterface;
- use Symfony\Component\Form\AbstractExtension;
- /**
- * Integrates the HtmlSanitizer component with the Form library.
- *
- * @author Nicolas Grekas <[email protected]>
- */
- class HtmlSanitizerExtension extends AbstractExtension
- {
- public function __construct(
- private ContainerInterface $sanitizers,
- private string $defaultSanitizer = 'default',
- ) {
- }
- protected function loadTypeExtensions(): array
- {
- return [
- new Type\TextTypeHtmlSanitizerExtension($this->sanitizers, $this->defaultSanitizer),
- ];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Csrf/Type/FormTypeCsrfExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Csrf\Type;
- use Symfony\Component\Form\AbstractTypeExtension;
- use Symfony\Component\Form\Extension\Core\Type\FormType;
- use Symfony\Component\Form\Extension\Core\Type\HiddenType;
- use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\Form\Util\ServerParams;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
- use Symfony\Contracts\Translation\TranslatorInterface;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class FormTypeCsrfExtension extends AbstractTypeExtension
- {
- public function __construct(
- private CsrfTokenManagerInterface $defaultTokenManager,
- private bool $defaultEnabled = true,
- private string $defaultFieldName = '_token',
- private ?TranslatorInterface $translator = null,
- private ?string $translationDomain = null,
- private ?ServerParams $serverParams = null,
- private array $fieldAttr = [],
- private string|array|null $defaultTokenId = null,
- ) {
- }
- /**
- * Adds a CSRF field to the form when the CSRF protection is enabled.
- */
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- if (!$options['csrf_protection']) {
- return;
- }
- $csrfTokenId = $options['csrf_token_id']
- ?: $this->defaultTokenId[$builder->getType()->getInnerType()::class]
- ?? $builder->getName()
- ?: $builder->getType()->getInnerType()::class;
- $builder->setAttribute('csrf_token_id', $csrfTokenId);
- $builder
- ->addEventSubscriber(new CsrfValidationListener(
- $options['csrf_field_name'],
- $options['csrf_token_manager'],
- $csrfTokenId,
- $options['csrf_message'],
- $this->translator,
- $this->translationDomain,
- $this->serverParams
- ))
- ;
- }
- /**
- * Adds a CSRF field to the root form view.
- */
- public function finishView(FormView $view, FormInterface $form, array $options): void
- {
- if ($options['csrf_protection'] && !$view->parent && $options['compound']) {
- $factory = $form->getConfig()->getFormFactory();
- $tokenId = $form->getConfig()->getAttribute('csrf_token_id');
- $data = (string) $options['csrf_token_manager']->getToken($tokenId);
- $csrfForm = $factory->createNamed($options['csrf_field_name'], HiddenType::class, $data, [
- 'block_prefix' => 'csrf_token',
- 'mapped' => false,
- 'attr' => $this->fieldAttr,
- ]);
- $view->children[$options['csrf_field_name']] = $csrfForm->createView($view);
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- if (\is_string($defaultTokenId = $this->defaultTokenId) && $defaultTokenId) {
- $defaultTokenManager = $this->defaultTokenManager;
- $defaultTokenId = static fn (Options $options) => $options['csrf_token_manager'] === $defaultTokenManager ? $defaultTokenId : null;
- } else {
- $defaultTokenId = null;
- }
- $resolver->setDefaults([
- 'csrf_protection' => $this->defaultEnabled,
- 'csrf_field_name' => $this->defaultFieldName,
- 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.',
- 'csrf_token_manager' => $this->defaultTokenManager,
- 'csrf_token_id' => $defaultTokenId,
- ]);
- $resolver->setAllowedTypes('csrf_protection', 'bool');
- $resolver->setAllowedTypes('csrf_field_name', 'string');
- $resolver->setAllowedTypes('csrf_message', 'string');
- $resolver->setAllowedTypes('csrf_token_manager', CsrfTokenManagerInterface::class);
- $resolver->setAllowedTypes('csrf_token_id', ['null', 'string']);
- }
- public static function getExtendedTypes(): iterable
- {
- return [FormType::class];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Csrf/CsrfExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Csrf;
- use Symfony\Component\Form\AbstractExtension;
- use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
- use Symfony\Contracts\Translation\TranslatorInterface;
- /**
- * This extension protects forms by using a CSRF token.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class CsrfExtension extends AbstractExtension
- {
- public function __construct(
- private CsrfTokenManagerInterface $tokenManager,
- private ?TranslatorInterface $translator = null,
- private ?string $translationDomain = null,
- ) {
- }
- protected function loadTypeExtensions(): array
- {
- return [
- new Type\FormTypeCsrfExtension($this->tokenManager, true, '_token', $this->translator, $this->translationDomain),
- ];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Csrf/EventListener/CsrfValidationListener.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Csrf\EventListener;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\Form\FormError;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\Form\Util\ServerParams;
- use Symfony\Component\Security\Csrf\CsrfToken;
- use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
- use Symfony\Contracts\Translation\TranslatorInterface;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class CsrfValidationListener implements EventSubscriberInterface
- {
- private ServerParams $serverParams;
- public static function getSubscribedEvents(): array
- {
- return [
- FormEvents::PRE_SUBMIT => 'preSubmit',
- ];
- }
- public function __construct(
- private string $fieldName,
- private CsrfTokenManagerInterface $tokenManager,
- private string $tokenId,
- private string $errorMessage,
- private ?TranslatorInterface $translator = null,
- private ?string $translationDomain = null,
- ?ServerParams $serverParams = null,
- ) {
- $this->serverParams = $serverParams ?? new ServerParams();
- }
- public function preSubmit(FormEvent $event): void
- {
- $form = $event->getForm();
- $postRequestSizeExceeded = 'POST' === $form->getConfig()->getMethod() && $this->serverParams->hasPostMaxSizeBeenExceeded();
- if ($form->isRoot() && $form->getConfig()->getOption('compound') && !$postRequestSizeExceeded) {
- $data = $event->getData();
- $csrfValue = \is_string($data[$this->fieldName] ?? null) ? $data[$this->fieldName] : null;
- $csrfToken = new CsrfToken($this->tokenId, $csrfValue);
- if (null === $csrfValue || !$this->tokenManager->isTokenValid($csrfToken)) {
- $errorMessage = $this->errorMessage;
- if (null !== $this->translator) {
- $errorMessage = $this->translator->trans($errorMessage, [], $this->translationDomain);
- }
- $form->addError(new FormError($errorMessage, $errorMessage, [], null, $csrfToken));
- }
- if (\is_array($data)) {
- unset($data[$this->fieldName]);
- $event->setData($data);
- }
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/EventListener/ResizeFormListener.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\EventListener;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\Form\Event\PostSetDataEvent;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\Form\FormInterface;
- /**
- * Resize a collection form element based on the data sent from the client.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ResizeFormListener implements EventSubscriberInterface
- {
- protected array $prototypeOptions;
- private \Closure|bool $deleteEmpty;
- // BC, to be removed in 8.0
- private bool $overridden = true;
- private bool $usePreSetData = false;
- public function __construct(
- private string $type,
- private array $options = [],
- private bool $allowAdd = false,
- private bool $allowDelete = false,
- bool|callable $deleteEmpty = false,
- ?array $prototypeOptions = null,
- private bool $keepAsList = false,
- ) {
- $this->deleteEmpty = \is_bool($deleteEmpty) ? $deleteEmpty : $deleteEmpty(...);
- $this->prototypeOptions = $prototypeOptions ?? $options;
- }
- public static function getSubscribedEvents(): array
- {
- return [
- FormEvents::PRE_SET_DATA => 'preSetData', // deprecated
- FormEvents::POST_SET_DATA => ['postSetData', 255], // as early as possible
- FormEvents::PRE_SUBMIT => 'preSubmit',
- // (MergeCollectionListener, MergeDoctrineCollectionListener)
- FormEvents::SUBMIT => ['onSubmit', 50],
- ];
- }
- /**
- * @deprecated Since Symfony 7.2, use {@see postSetData()} instead.
- */
- public function preSetData(FormEvent $event): void
- {
- if (__CLASS__ === static::class
- || __CLASS__ === (new \ReflectionClass($this))->getMethod('preSetData')->getDeclaringClass()->name
- ) {
- // not a child class, or child class does not overload PRE_SET_DATA
- return;
- }
- trigger_deprecation('symfony/form', '7.2', 'Calling "%s()" is deprecated, use "%s::postSetData()" instead.', __METHOD__, __CLASS__);
- // parent::preSetData() has been called
- $this->overridden = false;
- try {
- $this->postSetData($event);
- } finally {
- $this->usePreSetData = true;
- }
- }
- /**
- * Remove FormEvent type hint in 8.0.
- *
- * @final since Symfony 7.2
- */
- public function postSetData(FormEvent|PostSetDataEvent $event): void
- {
- if (__CLASS__ !== static::class) {
- if ($this->overridden) {
- trigger_deprecation('symfony/form', '7.2', 'Calling "%s::preSetData()" is deprecated, use "%s::postSetData()" instead.', static::class, __CLASS__);
- // parent::preSetData() has not been called, noop
- return;
- }
- if ($this->usePreSetData) {
- // nothing else to do
- return;
- }
- }
- $form = $event->getForm();
- $data = $event->getData() ?? [];
- if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
- throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
- }
- // First remove all rows
- foreach ($form as $name => $child) {
- $form->remove($name);
- }
- // Then add all rows again in the correct order
- foreach ($data as $name => $value) {
- $form->add($name, $this->type, array_replace([
- 'property_path' => '['.$name.']',
- ], $this->options));
- }
- }
- public function preSubmit(FormEvent $event): void
- {
- $form = $event->getForm();
- $data = $event->getData();
- if (!\is_array($data)) {
- $data = [];
- }
- // Remove all empty rows
- if ($this->allowDelete) {
- foreach ($form as $name => $child) {
- if (!isset($data[$name])) {
- $form->remove($name);
- }
- }
- }
- // Add all additional rows
- if ($this->allowAdd) {
- foreach ($data as $name => $value) {
- if (!$form->has($name)) {
- $form->add($name, $this->type, array_replace([
- 'property_path' => '['.$name.']',
- ], $this->prototypeOptions));
- }
- }
- }
- }
- public function onSubmit(FormEvent $event): void
- {
- $form = $event->getForm();
- $data = $event->getData() ?? [];
- // At this point, $data is an array or an array-like object that already contains the
- // new entries, which were added by the data mapper. The data mapper ignores existing
- // entries, so we need to manually unset removed entries in the collection.
- if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
- throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
- }
- if ($this->deleteEmpty) {
- $previousData = $form->getData();
- /** @var FormInterface $child */
- foreach ($form as $name => $child) {
- if (!$child->isValid() || !$child->isSynchronized()) {
- continue;
- }
- $isNew = !isset($previousData[$name]);
- $isEmpty = \is_callable($this->deleteEmpty) ? ($this->deleteEmpty)($child->getData()) : $child->isEmpty();
- // $isNew can only be true if allowAdd is true, so we don't
- // need to check allowAdd again
- if ($isEmpty && ($isNew || $this->allowDelete)) {
- unset($data[$name]);
- $form->remove($name);
- }
- }
- }
- // The data mapper only adds, but does not remove items, so do this
- // here
- if ($this->allowDelete) {
- $toDelete = [];
- foreach ($data as $name => $child) {
- if (!$form->has($name)) {
- $toDelete[] = $name;
- }
- }
- foreach ($toDelete as $name) {
- unset($data[$name]);
- }
- }
- if ($this->keepAsList) {
- $formReindex = [];
- foreach ($form as $name => $child) {
- $formReindex[] = $child;
- $form->remove($name);
- }
- foreach ($formReindex as $index => $child) {
- $form->add($index, $this->type, array_replace([
- 'property_path' => '['.$index.']',
- ], $this->options));
- }
- $data = array_values($data);
- }
- $event->setData($data);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/EventListener/TrimListener.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\EventListener;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\Form\Util\StringUtil;
- /**
- * Trims string data.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class TrimListener implements EventSubscriberInterface
- {
- public function preSubmit(FormEvent $event): void
- {
- $data = $event->getData();
- if (!\is_string($data)) {
- return;
- }
- $event->setData(StringUtil::trim($data));
- }
- public static function getSubscribedEvents(): array
- {
- return [FormEvents::PRE_SUBMIT => 'preSubmit'];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/EventListener/FixUrlProtocolListener.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\EventListener;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- /**
- * Adds a protocol to a URL if it doesn't already have one.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class FixUrlProtocolListener implements EventSubscriberInterface
- {
- /**
- * @param string|null $defaultProtocol The URL scheme to add when there is none or null to not modify the data
- */
- public function __construct(
- private ?string $defaultProtocol = 'http',
- ) {
- }
- public function onSubmit(FormEvent $event): void
- {
- $data = $event->getData();
- if ($this->defaultProtocol && $data && \is_string($data) && !preg_match('~^(?:[/.]|[\w+.-]+://|[^:/?@#]++@)~', $data)) {
- $event->setData($this->defaultProtocol.'://'.$data);
- }
- }
- public static function getSubscribedEvents(): array
- {
- return [FormEvents::SUBMIT => 'onSubmit'];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/EventListener/MergeCollectionListener.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\EventListener;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class MergeCollectionListener implements EventSubscriberInterface
- {
- /**
- * @param bool $allowAdd Whether values might be added to the collection
- * @param bool $allowDelete Whether values might be removed from the collection
- */
- public function __construct(
- private bool $allowAdd = false,
- private bool $allowDelete = false,
- ) {
- }
- public static function getSubscribedEvents(): array
- {
- return [
- FormEvents::SUBMIT => 'onSubmit',
- ];
- }
- public function onSubmit(FormEvent $event): void
- {
- $dataToMergeInto = $event->getForm()->getNormData();
- $data = $event->getData() ?? [];
- if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
- throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
- }
- if (null !== $dataToMergeInto && !\is_array($dataToMergeInto) && !($dataToMergeInto instanceof \Traversable && $dataToMergeInto instanceof \ArrayAccess)) {
- throw new UnexpectedTypeException($dataToMergeInto, 'array or (\Traversable and \ArrayAccess)');
- }
- // If we are not allowed to change anything, return immediately
- if ($data === $dataToMergeInto || (!$this->allowAdd && !$this->allowDelete)) {
- $event->setData($dataToMergeInto);
- return;
- }
- if (null === $dataToMergeInto) {
- // No original data was set. Set it if allowed
- if ($this->allowAdd) {
- $dataToMergeInto = $data;
- }
- } else {
- // Calculate delta
- $itemsToAdd = \is_object($data) ? clone $data : $data;
- $itemsToDelete = [];
- foreach ($dataToMergeInto as $beforeKey => $beforeItem) {
- foreach ($data as $afterKey => $afterItem) {
- if ($afterItem === $beforeItem) {
- // Item found, next original item
- unset($itemsToAdd[$afterKey]);
- continue 2;
- }
- }
- // Item not found, remember for deletion
- $itemsToDelete[] = $beforeKey;
- }
- // Remove deleted items before adding to free keys that are to be
- // replaced
- if ($this->allowDelete) {
- foreach ($itemsToDelete as $key) {
- unset($dataToMergeInto[$key]);
- }
- }
- // Add remaining items
- if ($this->allowAdd) {
- foreach ($itemsToAdd as $key => $item) {
- if (!isset($dataToMergeInto[$key])) {
- $dataToMergeInto[$key] = $item;
- } else {
- $dataToMergeInto[] = $item;
- }
- }
- }
- }
- $event->setData($dataToMergeInto);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/EventListener/TransformationFailureListener.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\EventListener;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\Form\FormError;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Contracts\Translation\TranslatorInterface;
- /**
- * @author Christian Flothmann <[email protected]>
- */
- class TransformationFailureListener implements EventSubscriberInterface
- {
- public function __construct(
- private ?TranslatorInterface $translator = null,
- ) {
- }
- public static function getSubscribedEvents(): array
- {
- return [
- FormEvents::POST_SUBMIT => ['convertTransformationFailureToFormError', -1024],
- ];
- }
- public function convertTransformationFailureToFormError(FormEvent $event): void
- {
- $form = $event->getForm();
- if (null === $form->getTransformationFailure() || !$form->isValid()) {
- return;
- }
- foreach ($form as $child) {
- if (!$child->isSynchronized()) {
- return;
- }
- }
- $clientDataAsString = \is_scalar($form->getViewData()) ? (string) $form->getViewData() : get_debug_type($form->getViewData());
- $messageTemplate = $form->getConfig()->getOption('invalid_message', 'The value {{ value }} is not valid.');
- $messageParameters = array_replace(['{{ value }}' => $clientDataAsString], $form->getConfig()->getOption('invalid_message_parameters', []));
- if (null !== $this->translator) {
- $message = $this->translator->trans($messageTemplate, $messageParameters);
- } else {
- $message = strtr($messageTemplate, $messageParameters);
- }
- $form->addError(new FormError($message, $messageTemplate, $messageParameters, null, $form->getTransformationFailure()));
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/CoreExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core;
- use Symfony\Component\Form\AbstractExtension;
- use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
- use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
- use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
- use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
- use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension;
- use Symfony\Component\PropertyAccess\PropertyAccess;
- use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
- use Symfony\Contracts\Translation\TranslatorInterface;
- /**
- * Represents the main form extension, which loads the core functionality.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class CoreExtension extends AbstractExtension
- {
- private PropertyAccessorInterface $propertyAccessor;
- private ChoiceListFactoryInterface $choiceListFactory;
- public function __construct(
- ?PropertyAccessorInterface $propertyAccessor = null,
- ?ChoiceListFactoryInterface $choiceListFactory = null,
- private ?TranslatorInterface $translator = null,
- ) {
- $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
- $this->choiceListFactory = $choiceListFactory ?? new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
- }
- protected function loadTypes(): array
- {
- return [
- new Type\FormType($this->propertyAccessor),
- new Type\BirthdayType(),
- new Type\CheckboxType(),
- new Type\ChoiceType($this->choiceListFactory, $this->translator),
- new Type\CollectionType(),
- new Type\CountryType(),
- new Type\DateIntervalType(),
- new Type\DateType(),
- new Type\DateTimeType(),
- new Type\EmailType(),
- new Type\HiddenType(),
- new Type\IntegerType(),
- new Type\LanguageType(),
- new Type\LocaleType(),
- new Type\MoneyType(),
- new Type\NumberType(),
- new Type\PasswordType(),
- new Type\PercentType(),
- new Type\RadioType(),
- new Type\RangeType(),
- new Type\RepeatedType(),
- new Type\SearchType(),
- new Type\TextareaType(),
- new Type\TextType(),
- new Type\TimeType(),
- new Type\TimezoneType(),
- new Type\UrlType(),
- new Type\FileType($this->translator),
- new Type\ButtonType(),
- new Type\SubmitType(),
- new Type\ResetType(),
- new Type\CurrencyType(),
- new Type\TelType(),
- new Type\ColorType($this->translator),
- new Type\WeekType(),
- ];
- }
- protected function loadTypeExtensions(): array
- {
- return [
- new TransformationFailureExtension($this->translator),
- ];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/IntegerType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class IntegerType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $builder->addViewTransformer(new IntegerToLocalizedStringTransformer($options['grouping'], $options['rounding_mode'], !$options['grouping'] ? 'en' : null));
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- if ($options['grouping']) {
- $view->vars['type'] = 'text';
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'grouping' => false,
- // Integer cast rounds towards 0, so do the same when displaying fractions
- 'rounding_mode' => \NumberFormatter::ROUND_DOWN,
- 'compound' => false,
- 'invalid_message' => 'Please enter an integer.',
- ]);
- $resolver->setAllowedValues('rounding_mode', [
- \NumberFormatter::ROUND_FLOOR,
- \NumberFormatter::ROUND_DOWN,
- \NumberFormatter::ROUND_HALFDOWN,
- \NumberFormatter::ROUND_HALFEVEN,
- \NumberFormatter::ROUND_HALFUP,
- \NumberFormatter::ROUND_UP,
- \NumberFormatter::ROUND_CEILING,
- ]);
- }
- public function getBlockPrefix(): string
- {
- return 'integer';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/NumberType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\StringToFloatTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class NumberType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $builder->addViewTransformer(new NumberToLocalizedStringTransformer(
- $options['scale'],
- $options['grouping'],
- $options['rounding_mode'],
- $options['html5'] ? 'en' : null
- ));
- if ('string' === $options['input']) {
- $builder->addModelTransformer(new StringToFloatTransformer($options['scale']));
- }
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- if ($options['html5']) {
- $view->vars['type'] = 'number';
- if (!isset($view->vars['attr']['step'])) {
- $view->vars['attr']['step'] = 'any';
- }
- } else {
- $view->vars['attr']['inputmode'] = 0 === $options['scale'] ? 'numeric' : 'decimal';
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- // default scale is locale specific (usually around 3)
- 'scale' => null,
- 'grouping' => false,
- 'rounding_mode' => \NumberFormatter::ROUND_HALFUP,
- 'compound' => false,
- 'input' => 'number',
- 'html5' => false,
- 'invalid_message' => 'Please enter a number.',
- ]);
- $resolver->setAllowedValues('rounding_mode', [
- \NumberFormatter::ROUND_FLOOR,
- \NumberFormatter::ROUND_DOWN,
- \NumberFormatter::ROUND_HALFDOWN,
- \NumberFormatter::ROUND_HALFEVEN,
- \NumberFormatter::ROUND_HALFUP,
- \NumberFormatter::ROUND_UP,
- \NumberFormatter::ROUND_CEILING,
- ]);
- $resolver->setAllowedValues('input', ['number', 'string']);
- $resolver->setAllowedTypes('scale', ['null', 'int']);
- $resolver->setAllowedTypes('html5', 'bool');
- $resolver->setNormalizer('grouping', static function (Options $options, $value) {
- if (true === $value && $options['html5']) {
- throw new LogicException('Cannot use the "grouping" option when the "html5" option is enabled.');
- }
- return $value;
- });
- }
- public function getBlockPrefix(): string
- {
- return 'number';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/TransformationFailureExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractTypeExtension;
- use Symfony\Component\Form\Extension\Core\EventListener\TransformationFailureListener;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Contracts\Translation\TranslatorInterface;
- /**
- * @author Christian Flothmann <[email protected]>
- */
- class TransformationFailureExtension extends AbstractTypeExtension
- {
- public function __construct(
- private ?TranslatorInterface $translator = null,
- ) {
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- if (!isset($options['constraints'])) {
- $builder->addEventSubscriber(new TransformationFailureListener($this->translator));
- }
- }
- public static function getExtendedTypes(): iterable
- {
- return [FormType::class];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/DateIntervalType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Exception\InvalidConfigurationException;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToArrayTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToStringTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\Form\ReversedTransformer;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Steffen Roßkamp <[email protected]>
- */
- class DateIntervalType extends AbstractType
- {
- private const TIME_PARTS = [
- 'years',
- 'months',
- 'weeks',
- 'days',
- 'hours',
- 'minutes',
- 'seconds',
- ];
- private const WIDGETS = [
- 'text' => TextType::class,
- 'integer' => IntegerType::class,
- 'choice' => ChoiceType::class,
- ];
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- if (!$options['with_years'] && !$options['with_months'] && !$options['with_weeks'] && !$options['with_days'] && !$options['with_hours'] && !$options['with_minutes'] && !$options['with_seconds']) {
- throw new InvalidConfigurationException('You must enable at least one interval field.');
- }
- if ($options['with_invert'] && 'single_text' === $options['widget']) {
- throw new InvalidConfigurationException('The single_text widget does not support invertible intervals.');
- }
- if ($options['with_weeks'] && $options['with_days']) {
- throw new InvalidConfigurationException('You cannot enable weeks and days fields together.');
- }
- $format = 'P';
- $parts = [];
- if ($options['with_years']) {
- $format .= '%yY';
- $parts[] = 'years';
- }
- if ($options['with_months']) {
- $format .= '%mM';
- $parts[] = 'months';
- }
- if ($options['with_weeks']) {
- $format .= '%wW';
- $parts[] = 'weeks';
- }
- if ($options['with_days']) {
- $format .= '%dD';
- $parts[] = 'days';
- }
- if ($options['with_hours'] || $options['with_minutes'] || $options['with_seconds']) {
- $format .= 'T';
- }
- if ($options['with_hours']) {
- $format .= '%hH';
- $parts[] = 'hours';
- }
- if ($options['with_minutes']) {
- $format .= '%iM';
- $parts[] = 'minutes';
- }
- if ($options['with_seconds']) {
- $format .= '%sS';
- $parts[] = 'seconds';
- }
- if ($options['with_invert']) {
- $parts[] = 'invert';
- }
- if ('single_text' === $options['widget']) {
- $builder->addViewTransformer(new DateIntervalToStringTransformer($format));
- } else {
- foreach (self::TIME_PARTS as $part) {
- if ($options['with_'.$part]) {
- $childOptions = [
- 'error_bubbling' => true,
- 'label' => $options['labels'][$part],
- // Append generic carry-along options
- 'required' => $options['required'],
- 'translation_domain' => $options['translation_domain'],
- // when compound the array entries are ignored, we need to cascade the configuration here
- 'empty_data' => $options['empty_data'][$part] ?? null,
- ];
- if ('choice' === $options['widget']) {
- $childOptions['choice_translation_domain'] = false;
- $childOptions['choices'] = $options[$part];
- $childOptions['placeholder'] = $options['placeholder'][$part];
- }
- $childForm = $builder->create($part, self::WIDGETS[$options['widget']], $childOptions);
- if ('integer' === $options['widget']) {
- $childForm->addModelTransformer(
- new ReversedTransformer(
- new IntegerToLocalizedStringTransformer()
- )
- );
- }
- $builder->add($childForm);
- }
- }
- if ($options['with_invert']) {
- $builder->add('invert', CheckboxType::class, [
- 'label' => $options['labels']['invert'],
- 'error_bubbling' => true,
- 'required' => false,
- 'translation_domain' => $options['translation_domain'],
- ]);
- }
- $builder->addViewTransformer(new DateIntervalToArrayTransformer($parts, 'text' === $options['widget']));
- }
- if ('string' === $options['input']) {
- $builder->addModelTransformer(
- new ReversedTransformer(
- new DateIntervalToStringTransformer($format)
- )
- );
- } elseif ('array' === $options['input']) {
- $builder->addModelTransformer(
- new ReversedTransformer(
- new DateIntervalToArrayTransformer($parts)
- )
- );
- }
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $vars = [
- 'widget' => $options['widget'],
- 'with_invert' => $options['with_invert'],
- ];
- foreach (self::TIME_PARTS as $part) {
- $vars['with_'.$part] = $options['with_'.$part];
- }
- $view->vars = array_replace($view->vars, $vars);
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $compound = static fn (Options $options) => 'single_text' !== $options['widget'];
- $emptyData = static fn (Options $options) => 'single_text' === $options['widget'] ? '' : [];
- $placeholderDefault = static fn (Options $options) => $options['required'] ? null : '';
- $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) {
- if (\is_array($placeholder)) {
- $default = $placeholderDefault($options);
- return array_merge(array_fill_keys(self::TIME_PARTS, $default), $placeholder);
- }
- return array_fill_keys(self::TIME_PARTS, $placeholder);
- };
- $labelsNormalizer = static fn (Options $options, array $labels) => array_replace([
- 'years' => null,
- 'months' => null,
- 'days' => null,
- 'weeks' => null,
- 'hours' => null,
- 'minutes' => null,
- 'seconds' => null,
- 'invert' => 'Negative interval',
- ], array_filter($labels, static fn ($label) => null !== $label));
- $resolver->setDefaults([
- 'with_years' => true,
- 'with_months' => true,
- 'with_days' => true,
- 'with_weeks' => false,
- 'with_hours' => false,
- 'with_minutes' => false,
- 'with_seconds' => false,
- 'with_invert' => false,
- 'years' => range(0, 100),
- 'months' => range(0, 12),
- 'weeks' => range(0, 52),
- 'days' => range(0, 31),
- 'hours' => range(0, 24),
- 'minutes' => range(0, 60),
- 'seconds' => range(0, 60),
- 'widget' => 'choice',
- 'input' => 'dateinterval',
- 'placeholder' => $placeholderDefault,
- 'by_reference' => true,
- 'error_bubbling' => false,
- // If initialized with a \DateInterval object, FormType initializes
- // this option to "\DateInterval". Since the internal, normalized
- // representation is not \DateInterval, but an array, we need to unset
- // this option.
- 'data_class' => null,
- 'compound' => $compound,
- 'empty_data' => $emptyData,
- 'labels' => [],
- 'invalid_message' => 'Please choose a valid date interval.',
- ]);
- $resolver->setNormalizer('placeholder', $placeholderNormalizer);
- $resolver->setNormalizer('labels', $labelsNormalizer);
- $resolver->setAllowedValues(
- 'input',
- [
- 'dateinterval',
- 'string',
- 'array',
- ]
- );
- $resolver->setAllowedValues(
- 'widget',
- [
- 'single_text',
- 'text',
- 'integer',
- 'choice',
- ]
- );
- // Don't clone \DateInterval classes, as i.e. format()
- // does not work after that
- $resolver->setAllowedValues('by_reference', true);
- $resolver->setAllowedTypes('years', 'array');
- $resolver->setAllowedTypes('months', 'array');
- $resolver->setAllowedTypes('weeks', 'array');
- $resolver->setAllowedTypes('days', 'array');
- $resolver->setAllowedTypes('hours', 'array');
- $resolver->setAllowedTypes('minutes', 'array');
- $resolver->setAllowedTypes('seconds', 'array');
- $resolver->setAllowedTypes('with_years', 'bool');
- $resolver->setAllowedTypes('with_months', 'bool');
- $resolver->setAllowedTypes('with_weeks', 'bool');
- $resolver->setAllowedTypes('with_days', 'bool');
- $resolver->setAllowedTypes('with_hours', 'bool');
- $resolver->setAllowedTypes('with_minutes', 'bool');
- $resolver->setAllowedTypes('with_seconds', 'bool');
- $resolver->setAllowedTypes('with_invert', 'bool');
- $resolver->setAllowedTypes('labels', 'array');
- }
- public function getBlockPrefix(): string
- {
- return 'dateinterval';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/EnumType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- use Symfony\Contracts\Translation\TranslatableInterface;
- /**
- * A choice type for native PHP enums.
- *
- * @author Alexander M. Turek <[email protected]>
- */
- final class EnumType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver
- ->setRequired(['class'])
- ->setAllowedTypes('class', 'string')
- ->setAllowedValues('class', enum_exists(...))
- ->setDefault('choices', static fn (Options $options): array => $options['class']::cases())
- ->setDefault('choice_label', static fn (\UnitEnum $choice) => $choice instanceof TranslatableInterface ? $choice : $choice->name)
- ->setDefault('choice_value', static function (Options $options): ?\Closure {
- if (!is_a($options['class'], \BackedEnum::class, true)) {
- return null;
- }
- return static function (?\BackedEnum $choice): ?string {
- if (null === $choice) {
- return null;
- }
- return (string) $choice->value;
- };
- })
- ;
- }
- public function getParent(): string
- {
- return ChoiceType::class;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/BaseType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractRendererEngine;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * Encapsulates common logic of {@link FormType} and {@link ButtonType}.
- *
- * This type does not appear in the form's type inheritance chain and as such
- * cannot be extended (via {@link \Symfony\Component\Form\FormExtensionInterface}) nor themed.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- abstract class BaseType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $builder->setDisabled($options['disabled']);
- $builder->setAutoInitialize($options['auto_initialize']);
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $name = $form->getName();
- $blockName = $options['block_name'] ?: $form->getName();
- $translationDomain = $options['translation_domain'];
- $labelTranslationParameters = $options['label_translation_parameters'];
- $attrTranslationParameters = $options['attr_translation_parameters'];
- $labelFormat = $options['label_format'];
- if ($view->parent) {
- if ('' !== ($parentFullName = $view->parent->vars['full_name'])) {
- $id = \sprintf('%s_%s', $view->parent->vars['id'], $name);
- $fullName = \sprintf('%s[%s]', $parentFullName, $name);
- $uniqueBlockPrefix = \sprintf('%s_%s', $view->parent->vars['unique_block_prefix'], $blockName);
- } else {
- $id = $name;
- $fullName = $name;
- $uniqueBlockPrefix = '_'.$blockName;
- }
- $translationDomain ??= $view->parent->vars['translation_domain'];
- $labelTranslationParameters = array_merge($view->parent->vars['label_translation_parameters'], $labelTranslationParameters);
- $attrTranslationParameters = array_merge($view->parent->vars['attr_translation_parameters'], $attrTranslationParameters);
- if (!$labelFormat) {
- $labelFormat = $view->parent->vars['label_format'];
- }
- $rootFormAttrOption = $form->getRoot()->getConfig()->getOption('form_attr');
- if ($options['form_attr'] || $rootFormAttrOption) {
- $options['attr']['form'] = \is_string($rootFormAttrOption) ? $rootFormAttrOption : $form->getRoot()->getName();
- if (empty($options['attr']['form'])) {
- throw new LogicException('"form_attr" option must be a string identifier on root form when it has no id.');
- }
- }
- } else {
- $id = \is_string($options['form_attr']) ? $options['form_attr'] : $name;
- $fullName = $name;
- $uniqueBlockPrefix = '_'.$blockName;
- // Strip leading underscores and digits. These are allowed in
- // form names, but not in HTML4 ID attributes.
- // https://www.w3.org/TR/html401/struct/global#adef-id
- $id = ltrim($id, '_0123456789');
- }
- $blockPrefixes = [];
- for ($type = $form->getConfig()->getType(); null !== $type; $type = $type->getParent()) {
- array_unshift($blockPrefixes, $type->getBlockPrefix());
- }
- if (null !== $options['block_prefix']) {
- $blockPrefixes[] = $options['block_prefix'];
- }
- $blockPrefixes[] = $uniqueBlockPrefix;
- $view->vars = array_replace($view->vars, [
- 'form' => $view,
- 'id' => $id,
- 'name' => $name,
- 'full_name' => $fullName,
- 'disabled' => $form->isDisabled(),
- 'label' => $options['label'],
- 'label_format' => $labelFormat,
- 'label_html' => $options['label_html'],
- 'multipart' => false,
- 'attr' => $options['attr'],
- 'block_prefixes' => $blockPrefixes,
- 'unique_block_prefix' => $uniqueBlockPrefix,
- 'row_attr' => $options['row_attr'],
- 'translation_domain' => $translationDomain,
- 'label_translation_parameters' => $labelTranslationParameters,
- 'attr_translation_parameters' => $attrTranslationParameters,
- 'priority' => $options['priority'],
- // Using the block name here speeds up performance in collection
- // forms, where each entry has the same full block name.
- // Including the type is important too, because if rows of a
- // collection form have different types (dynamically), they should
- // be rendered differently.
- // https://github.com/symfony/symfony/issues/5038
- AbstractRendererEngine::CACHE_KEY_VAR => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(),
- ]);
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'block_name' => null,
- 'block_prefix' => null,
- 'disabled' => false,
- 'label' => null,
- 'label_format' => null,
- 'row_attr' => [],
- 'label_html' => false,
- 'label_translation_parameters' => [],
- 'attr_translation_parameters' => [],
- 'attr' => [],
- 'translation_domain' => null,
- 'auto_initialize' => true,
- 'priority' => 0,
- 'form_attr' => false,
- ]);
- $resolver->setAllowedTypes('block_prefix', ['null', 'string']);
- $resolver->setAllowedTypes('attr', 'array');
- $resolver->setAllowedTypes('row_attr', 'array');
- $resolver->setAllowedTypes('label_html', 'bool');
- $resolver->setAllowedTypes('priority', 'int');
- $resolver->setAllowedTypes('form_attr', ['bool', 'string']);
- $resolver->setInfo('priority', 'The form rendering priority (higher priorities will be rendered first)');
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/UlidType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Extension\Core\DataTransformer\UlidToStringTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Pavel Dyakonov <[email protected]>
- */
- class UlidType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $builder
- ->addViewTransformer(new UlidToStringTransformer())
- ;
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'compound' => false,
- 'invalid_message' => 'Please enter a valid ULID.',
- ]);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/UrlType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Extension\Core\EventListener\FixUrlProtocolListener;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class UrlType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- if (null !== $options['default_protocol']) {
- $builder->addEventSubscriber(new FixUrlProtocolListener($options['default_protocol']));
- }
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- if ($options['default_protocol']) {
- $view->vars['attr']['inputmode'] = 'url';
- $view->vars['type'] = 'text';
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'default_protocol' => static function (Options $options) {
- 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.');
- return 'http';
- },
- 'invalid_message' => 'Please enter a valid URL.',
- ]);
- $resolver->setAllowedTypes('default_protocol', ['null', 'string']);
- }
- public function getParent(): ?string
- {
- return TextType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'url';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/RepeatedType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Extension\Core\DataTransformer\ValueToDuplicatesTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class RepeatedType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- // Overwrite required option for child fields
- $options['first_options']['required'] = $options['required'];
- $options['second_options']['required'] = $options['required'];
- if (!isset($options['options']['error_bubbling'])) {
- $options['options']['error_bubbling'] = $options['error_bubbling'];
- }
- // children fields must always be mapped
- $defaultOptions = ['mapped' => true];
- $builder
- ->addViewTransformer(new ValueToDuplicatesTransformer([
- $options['first_name'],
- $options['second_name'],
- ]))
- ->add($options['first_name'], $options['type'], array_merge($options['options'], $options['first_options'], $defaultOptions))
- ->add($options['second_name'], $options['type'], array_merge($options['options'], $options['second_options'], $defaultOptions))
- ;
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'type' => TextType::class,
- 'options' => [],
- 'first_options' => [],
- 'second_options' => [],
- 'first_name' => 'first',
- 'second_name' => 'second',
- 'error_bubbling' => false,
- 'invalid_message' => 'The values do not match.',
- ]);
- $resolver->setAllowedTypes('options', 'array');
- $resolver->setAllowedTypes('first_options', 'array');
- $resolver->setAllowedTypes('second_options', 'array');
- }
- public function getBlockPrefix(): string
- {
- return 'repeated';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/ResetType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\ButtonTypeInterface;
- /**
- * A reset button.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ResetType extends AbstractType implements ButtonTypeInterface
- {
- public function getParent(): ?string
- {
- return ButtonType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'reset';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/SubmitType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\Form\SubmitButtonTypeInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * A submit button.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class SubmitType extends AbstractType implements SubmitButtonTypeInterface
- {
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $view->vars['clicked'] = $form->isClicked();
- if (!$options['validate']) {
- $view->vars['attr']['formnovalidate'] = true;
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefault('validate', true);
- $resolver->setAllowedTypes('validate', 'bool');
- }
- public function getParent(): ?string
- {
- return ButtonType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'submit';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/TimeType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Exception\InvalidConfigurationException;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\Form\ReversedTransformer;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class TimeType extends AbstractType
- {
- private const WIDGETS = [
- 'text' => TextType::class,
- 'choice' => ChoiceType::class,
- ];
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $parts = ['hour'];
- $format = 'H';
- if ($options['with_seconds'] && !$options['with_minutes']) {
- throw new InvalidConfigurationException('You cannot disable minutes if you have enabled seconds.');
- }
- if (null !== $options['reference_date'] && $options['reference_date']->getTimezone()->getName() !== $options['model_timezone']) {
- 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()));
- }
- if ($options['with_minutes']) {
- $format .= ':i';
- $parts[] = 'minute';
- }
- if ($options['with_seconds']) {
- $format .= ':s';
- $parts[] = 'second';
- }
- if ('single_text' === $options['widget']) {
- $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $e) use ($options) {
- $data = $e->getData();
- if ($data && preg_match('/^(?P<hours>\d{2}):(?P<minutes>\d{2})(?::(?P<seconds>\d{2})(?:\.\d+)?)?$/', $data, $matches)) {
- if ($options['with_seconds']) {
- // handle seconds ignored by user's browser when with_seconds enabled
- // https://codereview.chromium.org/450533009/
- $e->setData(\sprintf('%s:%s:%s', $matches['hours'], $matches['minutes'], $matches['seconds'] ?? '00'));
- } else {
- $e->setData(\sprintf('%s:%s', $matches['hours'], $matches['minutes']));
- }
- }
- });
- $parseFormat = null;
- if (null !== $options['reference_date']) {
- $parseFormat = 'Y-m-d '.$format;
- $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($options) {
- $data = $event->getData();
- if (preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $data)) {
- $event->setData($options['reference_date']->format('Y-m-d ').$data);
- }
- });
- }
- $builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format, $parseFormat));
- } else {
- $hourOptions = $minuteOptions = $secondOptions = [
- 'error_bubbling' => true,
- 'empty_data' => '',
- ];
- // when the form is compound the entries of the array are ignored in favor of children data
- // so we need to handle the cascade setting here
- $emptyData = $builder->getEmptyData() ?: [];
- if ($emptyData instanceof \Closure) {
- $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) {
- $emptyData = $emptyData($form->getParent());
- return $emptyData[$option] ?? '';
- };
- $hourOptions['empty_data'] = $lazyEmptyData('hour');
- } elseif (isset($emptyData['hour'])) {
- $hourOptions['empty_data'] = $emptyData['hour'];
- }
- if (isset($options['invalid_message'])) {
- $hourOptions['invalid_message'] = $options['invalid_message'];
- $minuteOptions['invalid_message'] = $options['invalid_message'];
- $secondOptions['invalid_message'] = $options['invalid_message'];
- }
- if (isset($options['invalid_message_parameters'])) {
- $hourOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
- $minuteOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
- $secondOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
- }
- if ('choice' === $options['widget']) {
- $hours = $minutes = [];
- foreach ($options['hours'] as $hour) {
- $hours[str_pad($hour, 2, '0', \STR_PAD_LEFT)] = $hour;
- }
- // Only pass a subset of the options to children
- $hourOptions['choices'] = $hours;
- $hourOptions['placeholder'] = $options['placeholder']['hour'];
- $hourOptions['choice_translation_domain'] = $options['choice_translation_domain']['hour'];
- if ($options['with_minutes']) {
- foreach ($options['minutes'] as $minute) {
- $minutes[str_pad($minute, 2, '0', \STR_PAD_LEFT)] = $minute;
- }
- $minuteOptions['choices'] = $minutes;
- $minuteOptions['placeholder'] = $options['placeholder']['minute'];
- $minuteOptions['choice_translation_domain'] = $options['choice_translation_domain']['minute'];
- }
- if ($options['with_seconds']) {
- $seconds = [];
- foreach ($options['seconds'] as $second) {
- $seconds[str_pad($second, 2, '0', \STR_PAD_LEFT)] = $second;
- }
- $secondOptions['choices'] = $seconds;
- $secondOptions['placeholder'] = $options['placeholder']['second'];
- $secondOptions['choice_translation_domain'] = $options['choice_translation_domain']['second'];
- }
- // Append generic carry-along options
- foreach (['required', 'translation_domain'] as $passOpt) {
- $hourOptions[$passOpt] = $options[$passOpt];
- if ($options['with_minutes']) {
- $minuteOptions[$passOpt] = $options[$passOpt];
- }
- if ($options['with_seconds']) {
- $secondOptions[$passOpt] = $options[$passOpt];
- }
- }
- }
- $builder->add('hour', self::WIDGETS[$options['widget']], $hourOptions);
- if ($options['with_minutes']) {
- if ($emptyData instanceof \Closure) {
- $minuteOptions['empty_data'] = $lazyEmptyData('minute');
- } elseif (isset($emptyData['minute'])) {
- $minuteOptions['empty_data'] = $emptyData['minute'];
- }
- $builder->add('minute', self::WIDGETS[$options['widget']], $minuteOptions);
- }
- if ($options['with_seconds']) {
- if ($emptyData instanceof \Closure) {
- $secondOptions['empty_data'] = $lazyEmptyData('second');
- } elseif (isset($emptyData['second'])) {
- $secondOptions['empty_data'] = $emptyData['second'];
- }
- $builder->add('second', self::WIDGETS[$options['widget']], $secondOptions);
- }
- $builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget'], $options['reference_date']));
- }
- if ('datetime_immutable' === $options['input']) {
- $builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
- } elseif ('string' === $options['input']) {
- $builder->addModelTransformer(new ReversedTransformer(
- new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format'])
- ));
- } elseif ('timestamp' === $options['input']) {
- $builder->addModelTransformer(new ReversedTransformer(
- new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
- ));
- } elseif ('array' === $options['input']) {
- $builder->addModelTransformer(new ReversedTransformer(
- new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts, 'text' === $options['widget'], $options['reference_date'])
- ));
- }
- if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) {
- $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
- $date = $event->getData();
- if (!$date instanceof \DateTimeInterface) {
- return;
- }
- if ($date->getTimezone()->getName() !== $options['model_timezone']) {
- 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']));
- }
- });
- }
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $view->vars = array_replace($view->vars, [
- 'widget' => $options['widget'],
- 'with_minutes' => $options['with_minutes'],
- 'with_seconds' => $options['with_seconds'],
- ]);
- // Change the input to an HTML5 time input if
- // * the widget is set to "single_text"
- // * the html5 is set to true
- if ($options['html5'] && 'single_text' === $options['widget']) {
- $view->vars['type'] = 'time';
- // we need to force the browser to display the seconds by
- // adding the HTML attribute step if not already defined.
- // Otherwise the browser will not display and so not send the seconds
- // therefore the value will always be considered as invalid.
- if (!isset($view->vars['attr']['step'])) {
- if ($options['with_seconds']) {
- $view->vars['attr']['step'] = 1;
- } elseif (!$options['with_minutes']) {
- $view->vars['attr']['step'] = 3600;
- }
- }
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $compound = static fn (Options $options) => 'single_text' !== $options['widget'];
- $placeholderDefault = static fn (Options $options) => $options['required'] ? null : '';
- $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) {
- if (\is_array($placeholder)) {
- $default = $placeholderDefault($options);
- return array_merge(
- ['hour' => $default, 'minute' => $default, 'second' => $default],
- $placeholder
- );
- }
- return [
- 'hour' => $placeholder,
- 'minute' => $placeholder,
- 'second' => $placeholder,
- ];
- };
- $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) {
- if (\is_array($choiceTranslationDomain)) {
- return array_replace(
- ['hour' => false, 'minute' => false, 'second' => false],
- $choiceTranslationDomain
- );
- }
- return [
- 'hour' => $choiceTranslationDomain,
- 'minute' => $choiceTranslationDomain,
- 'second' => $choiceTranslationDomain,
- ];
- };
- $modelTimezone = static function (Options $options, $value): ?string {
- if (null !== $value) {
- return $value;
- }
- if (null !== $options['reference_date']) {
- return $options['reference_date']->getTimezone()->getName();
- }
- return null;
- };
- $viewTimezone = static function (Options $options, $value): ?string {
- if (null !== $value) {
- return $value;
- }
- if (null !== $options['model_timezone'] && null === $options['reference_date']) {
- return $options['model_timezone'];
- }
- return null;
- };
- $resolver->setDefaults([
- 'hours' => range(0, 23),
- 'minutes' => range(0, 59),
- 'seconds' => range(0, 59),
- 'widget' => 'single_text',
- 'input' => 'datetime',
- 'input_format' => 'H:i:s',
- 'with_minutes' => true,
- 'with_seconds' => false,
- 'model_timezone' => $modelTimezone,
- 'view_timezone' => $viewTimezone,
- 'reference_date' => null,
- 'placeholder' => $placeholderDefault,
- 'html5' => true,
- // Don't modify \DateTime classes by reference, we treat
- // them like immutable value objects
- 'by_reference' => false,
- 'error_bubbling' => false,
- // If initialized with a \DateTime object, FormType initializes
- // this option to "\DateTime". Since the internal, normalized
- // representation is not \DateTime, but an array, we need to unset
- // this option.
- 'data_class' => null,
- 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '',
- 'compound' => $compound,
- 'choice_translation_domain' => false,
- 'invalid_message' => 'Please enter a valid time.',
- ]);
- $resolver->setNormalizer('view_timezone', static function (Options $options, $viewTimezone): ?string {
- if (null !== $options['model_timezone'] && $viewTimezone !== $options['model_timezone'] && null === $options['reference_date']) {
- throw new LogicException('Using different values for the "model_timezone" and "view_timezone" options without configuring a reference date is not supported.');
- }
- return $viewTimezone;
- });
- $resolver->setNormalizer('placeholder', $placeholderNormalizer);
- $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
- $resolver->setAllowedValues('input', [
- 'datetime',
- 'datetime_immutable',
- 'string',
- 'timestamp',
- 'array',
- ]);
- $resolver->setAllowedValues('widget', [
- 'single_text',
- 'text',
- 'choice',
- ]);
- $resolver->setAllowedTypes('hours', 'array');
- $resolver->setAllowedTypes('minutes', 'array');
- $resolver->setAllowedTypes('seconds', 'array');
- $resolver->setAllowedTypes('input_format', 'string');
- $resolver->setAllowedTypes('model_timezone', ['null', 'string']);
- $resolver->setAllowedTypes('view_timezone', ['null', 'string']);
- $resolver->setAllowedTypes('reference_date', ['null', \DateTimeInterface::class]);
- }
- public function getBlockPrefix(): string
- {
- return 'time';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/SearchType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class SearchType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'invalid_message' => 'Please enter a valid search term.',
- ]);
- }
- public function getParent(): ?string
- {
- return TextType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'search';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/DateTimeType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToHtml5LocalDateTimeTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\Form\ReversedTransformer;
- use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class DateTimeType extends AbstractType
- {
- public const DEFAULT_DATE_FORMAT = \IntlDateFormatter::MEDIUM;
- public const DEFAULT_TIME_FORMAT = \IntlDateFormatter::MEDIUM;
- /**
- * The HTML5 datetime-local format as defined in
- * http://w3c.github.io/html-reference/datatypes.html#form.data.datetime-local.
- */
- public const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
- private const ACCEPTED_FORMATS = [
- \IntlDateFormatter::FULL,
- \IntlDateFormatter::LONG,
- \IntlDateFormatter::MEDIUM,
- \IntlDateFormatter::SHORT,
- ];
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $parts = ['year', 'month', 'day', 'hour'];
- $dateParts = ['year', 'month', 'day'];
- $timeParts = ['hour'];
- if ($options['with_minutes']) {
- $parts[] = 'minute';
- $timeParts[] = 'minute';
- }
- if ($options['with_seconds']) {
- $parts[] = 'second';
- $timeParts[] = 'second';
- }
- $dateFormat = \is_int($options['date_format']) ? $options['date_format'] : self::DEFAULT_DATE_FORMAT;
- $timeFormat = self::DEFAULT_TIME_FORMAT;
- $calendar = \IntlDateFormatter::GREGORIAN;
- $pattern = \is_string($options['format']) ? $options['format'] : null;
- if (!\in_array($dateFormat, self::ACCEPTED_FORMATS, true)) {
- 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.');
- }
- if ('single_text' === $options['widget']) {
- if (self::HTML5_FORMAT === $pattern) {
- $builder->addViewTransformer(new DateTimeToHtml5LocalDateTimeTransformer(
- $options['model_timezone'],
- $options['view_timezone'],
- $options['with_seconds']
- ));
- } else {
- $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
- $options['model_timezone'],
- $options['view_timezone'],
- $dateFormat,
- $timeFormat,
- $calendar,
- $pattern
- ));
- }
- } else {
- // when the form is compound the entries of the array are ignored in favor of children data
- // so we need to handle the cascade setting here
- $emptyData = $builder->getEmptyData() ?: [];
- // Only pass a subset of the options to children
- $dateOptions = array_intersect_key($options, array_flip([
- 'years',
- 'months',
- 'days',
- 'placeholder',
- 'choice_translation_domain',
- 'required',
- 'translation_domain',
- 'html5',
- 'invalid_message',
- 'invalid_message_parameters',
- ]));
- if ($emptyData instanceof \Closure) {
- $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) {
- $emptyData = $emptyData($form->getParent());
- return $emptyData[$option] ?? '';
- };
- $dateOptions['empty_data'] = $lazyEmptyData('date');
- } elseif (isset($emptyData['date'])) {
- $dateOptions['empty_data'] = $emptyData['date'];
- }
- $timeOptions = array_intersect_key($options, array_flip([
- 'hours',
- 'minutes',
- 'seconds',
- 'with_minutes',
- 'with_seconds',
- 'placeholder',
- 'choice_translation_domain',
- 'required',
- 'translation_domain',
- 'html5',
- 'invalid_message',
- 'invalid_message_parameters',
- ]));
- if ($emptyData instanceof \Closure) {
- $timeOptions['empty_data'] = $lazyEmptyData('time');
- } elseif (isset($emptyData['time'])) {
- $timeOptions['empty_data'] = $emptyData['time'];
- }
- if (false === $options['label']) {
- $dateOptions['label'] = false;
- $timeOptions['label'] = false;
- }
- $dateOptions['widget'] = $options['date_widget'] ?? $options['widget'] ?? 'choice';
- $timeOptions['widget'] = $options['time_widget'] ?? $options['widget'] ?? 'choice';
- if (null !== $options['date_label']) {
- $dateOptions['label'] = $options['date_label'];
- }
- if (null !== $options['time_label']) {
- $timeOptions['label'] = $options['time_label'];
- }
- if (null !== $options['date_format']) {
- $dateOptions['format'] = $options['date_format'];
- }
- $dateOptions['input'] = $timeOptions['input'] = 'array';
- $dateOptions['error_bubbling'] = $timeOptions['error_bubbling'] = true;
- $builder
- ->addViewTransformer(new DataTransformerChain([
- new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts),
- new ArrayToPartsTransformer([
- 'date' => $dateParts,
- 'time' => $timeParts,
- ]),
- ]))
- ->add('date', DateType::class, $dateOptions)
- ->add('time', TimeType::class, $timeOptions)
- ;
- }
- if ('datetime_immutable' === $options['input']) {
- $builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
- } elseif ('string' === $options['input']) {
- $builder->addModelTransformer(new ReversedTransformer(
- new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format'])
- ));
- } elseif ('timestamp' === $options['input']) {
- $builder->addModelTransformer(new ReversedTransformer(
- new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
- ));
- } elseif ('array' === $options['input']) {
- $builder->addModelTransformer(new ReversedTransformer(
- new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts)
- ));
- }
- if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) {
- $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
- $date = $event->getData();
- if (!$date instanceof \DateTimeInterface) {
- return;
- }
- if ($date->getTimezone()->getName() !== $options['model_timezone']) {
- 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']));
- }
- });
- }
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $view->vars['widget'] = $options['widget'];
- // Change the input to an HTML5 datetime input if
- // * the widget is set to "single_text"
- // * the format matches the one expected by HTML5
- // * the html5 is set to true
- if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
- $view->vars['type'] = 'datetime-local';
- // we need to force the browser to display the seconds by
- // adding the HTML attribute step if not already defined.
- // Otherwise the browser will not display and so not send the seconds
- // therefore the value will always be considered as invalid.
- if (!isset($view->vars['attr']['step'])) {
- if ($options['with_seconds']) {
- $view->vars['attr']['step'] = 1;
- } elseif (!$options['with_minutes']) {
- $view->vars['attr']['step'] = 3600;
- }
- }
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $compound = static fn (Options $options) => 'single_text' !== $options['widget'];
- $resolver->setDefaults([
- 'input' => 'datetime',
- 'model_timezone' => null,
- 'view_timezone' => null,
- 'format' => self::HTML5_FORMAT,
- 'date_format' => null,
- 'widget' => null,
- 'date_widget' => null,
- 'time_widget' => null,
- 'with_minutes' => true,
- 'with_seconds' => false,
- 'html5' => true,
- // Don't modify \DateTime classes by reference, we treat
- // them like immutable value objects
- 'by_reference' => false,
- 'error_bubbling' => false,
- // If initialized with a \DateTime object, FormType initializes
- // this option to "\DateTime". Since the internal, normalized
- // representation is not \DateTime, but an array, we need to unset
- // this option.
- 'data_class' => null,
- 'compound' => $compound,
- 'date_label' => null,
- 'time_label' => null,
- 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '',
- 'input_format' => 'Y-m-d H:i:s',
- 'invalid_message' => 'Please enter a valid date and time.',
- ]);
- // Don't add some defaults in order to preserve the defaults
- // set in DateType and TimeType
- $resolver->setDefined([
- 'placeholder',
- 'choice_translation_domain',
- 'years',
- 'months',
- 'days',
- 'hours',
- 'minutes',
- 'seconds',
- ]);
- $resolver->setAllowedValues('input', [
- 'datetime',
- 'datetime_immutable',
- 'string',
- 'timestamp',
- 'array',
- ]);
- $resolver->setAllowedValues('date_widget', [
- null, // inherit default from DateType
- 'single_text',
- 'text',
- 'choice',
- ]);
- $resolver->setAllowedValues('time_widget', [
- null, // inherit default from TimeType
- 'single_text',
- 'text',
- 'choice',
- ]);
- // This option will overwrite "date_widget" and "time_widget" options
- $resolver->setAllowedValues('widget', [
- null, // default, don't overwrite options
- 'single_text',
- 'text',
- 'choice',
- ]);
- $resolver->setAllowedTypes('input_format', 'string');
- $resolver->setNormalizer('date_format', static function (Options $options, $dateFormat) {
- if (null !== $dateFormat && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
- throw new LogicException(\sprintf('Cannot use the "date_format" option of the "%s" with an HTML5 date.', self::class));
- }
- return $dateFormat;
- });
- $resolver->setNormalizer('widget', static function (Options $options, $widget) {
- if ('single_text' === $widget) {
- if (null !== $options['date_widget']) {
- throw new LogicException(\sprintf('Cannot use the "date_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class));
- }
- if (null !== $options['time_widget']) {
- throw new LogicException(\sprintf('Cannot use the "time_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class));
- }
- } elseif (null === $widget && null === $options['date_widget'] && null === $options['time_widget']) {
- return 'single_text';
- }
- return $widget;
- });
- $resolver->setNormalizer('html5', static function (Options $options, $html5) {
- if ($html5 && self::HTML5_FORMAT !== $options['format']) {
- throw new LogicException(\sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class));
- }
- return $html5;
- });
- }
- public function getBlockPrefix(): string
- {
- return 'datetime';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/RadioType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class RadioType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'invalid_message' => 'Please select a valid option.',
- ]);
- }
- public function getParent(): ?string
- {
- return CheckboxType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'radio';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/TelType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class TelType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'invalid_message' => 'Please provide a valid phone number.',
- ]);
- }
- public function getParent(): ?string
- {
- return TextType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'tel';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/HiddenType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class HiddenType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- // hidden fields cannot have a required attribute
- 'required' => false,
- // Pass errors to the parent
- 'error_bubbling' => true,
- 'compound' => false,
- 'invalid_message' => 'The hidden field is invalid.',
- ]);
- }
- public function getBlockPrefix(): string
- {
- return 'hidden';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/ColorType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormError;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- use Symfony\Contracts\Translation\TranslatorInterface;
- class ColorType extends AbstractType
- {
- /**
- * @see https://www.w3.org/TR/html52/sec-forms.html#color-state-typecolor
- */
- private const HTML5_PATTERN = '/^#[0-9a-f]{6}$/i';
- public function __construct(
- private ?TranslatorInterface $translator = null,
- ) {
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- if (!$options['html5']) {
- return;
- }
- $translator = $this->translator;
- $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($translator): void {
- $value = $event->getData();
- if (null === $value || '' === $value) {
- return;
- }
- if (\is_string($value) && preg_match(self::HTML5_PATTERN, $value)) {
- return;
- }
- $messageTemplate = 'This value is not a valid HTML5 color.';
- $messageParameters = [
- '{{ value }}' => \is_scalar($value) ? (string) $value : \gettype($value),
- ];
- $message = $translator?->trans($messageTemplate, $messageParameters, 'validators') ?? $messageTemplate;
- $event->getForm()->addError(new FormError($message, $messageTemplate, $messageParameters));
- });
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'html5' => false,
- 'invalid_message' => 'Please select a valid color.',
- ]);
- $resolver->setAllowedTypes('html5', 'bool');
- }
- public function getParent(): ?string
- {
- return TextType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'color';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/TimezoneType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\ChoiceList\ChoiceList;
- use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeZoneToStringTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\IntlTimeZoneToStringTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Intl\Intl;
- use Symfony\Component\Intl\Timezones;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class TimezoneType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- if ('datetimezone' === $options['input']) {
- $builder->addModelTransformer(new DateTimeZoneToStringTransformer($options['multiple']));
- } elseif ('intltimezone' === $options['input']) {
- $builder->addModelTransformer(new IntlTimeZoneToStringTransformer($options['multiple']));
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'intl' => false,
- 'choice_loader' => function (Options $options) {
- $input = $options['input'];
- if ($options['intl']) {
- if (!class_exists(Intl::class)) {
- 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));
- }
- $choiceTranslationLocale = $options['choice_translation_locale'];
- return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => self::getIntlTimezones($input, $choiceTranslationLocale)), [$input, $choiceTranslationLocale]);
- }
- return ChoiceList::lazy($this, static fn () => self::getPhpTimezones($input), $input);
- },
- 'choice_translation_domain' => false,
- 'choice_translation_locale' => null,
- 'input' => 'string',
- 'invalid_message' => 'Please select a valid timezone.',
- 'regions' => \DateTimeZone::ALL,
- ]);
- $resolver->setAllowedTypes('intl', ['bool']);
- $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
- $resolver->setNormalizer('choice_translation_locale', static function (Options $options, $value) {
- if (null !== $value && !$options['intl']) {
- throw new LogicException('The "choice_translation_locale" option can only be used if the "intl" option is set to true.');
- }
- return $value;
- });
- $resolver->setAllowedValues('input', ['string', 'datetimezone', 'intltimezone']);
- $resolver->setNormalizer('input', static function (Options $options, $value) {
- if ('intltimezone' === $value && !class_exists(\IntlTimeZone::class)) {
- throw new LogicException('Cannot use "intltimezone" input because the PHP intl extension is not available.');
- }
- return $value;
- });
- }
- public function getParent(): ?string
- {
- return ChoiceType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'timezone';
- }
- private static function getPhpTimezones(string $input): array
- {
- $timezones = [];
- foreach (\DateTimeZone::listIdentifiers(\DateTimeZone::ALL) as $timezone) {
- if ('intltimezone' === $input && 'Etc/Unknown' === \IntlTimeZone::createTimeZone($timezone)->getID()) {
- continue;
- }
- $timezones[str_replace(['/', '_'], [' / ', ' '], $timezone)] = $timezone;
- }
- return $timezones;
- }
- private static function getIntlTimezones(string $input, ?string $locale = null): array
- {
- $timezones = array_flip(Timezones::getNames($locale));
- if ('intltimezone' === $input) {
- foreach ($timezones as $name => $timezone) {
- if ('Etc/Unknown' === \IntlTimeZone::createTimeZone($timezone)->getID()) {
- unset($timezones[$name]);
- }
- }
- }
- return $timezones;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/ChoiceType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFilter;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceTranslationParameters;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy;
- use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice;
- use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
- use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
- use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
- use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
- use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
- use Symfony\Component\Form\ChoiceList\Loader\LazyChoiceLoader;
- use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
- use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
- use Symfony\Component\Form\ChoiceList\View\ChoiceView;
- use Symfony\Component\Form\Event\PreSubmitEvent;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper;
- use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper;
- use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
- use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormError;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- use Symfony\Component\PropertyAccess\PropertyPath;
- use Symfony\Contracts\Translation\TranslatorInterface;
- class ChoiceType extends AbstractType
- {
- private ChoiceListFactoryInterface $choiceListFactory;
- public function __construct(
- ?ChoiceListFactoryInterface $choiceListFactory = null,
- private ?TranslatorInterface $translator = null,
- ) {
- $this->choiceListFactory = $choiceListFactory ?? new CachingFactoryDecorator(
- new PropertyAccessDecorator(
- new DefaultChoiceListFactory()
- )
- );
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $unknownValues = [];
- $choiceList = $this->createChoiceList($options);
- $builder->setAttribute('choice_list', $choiceList);
- if ($options['expanded']) {
- $builder->setDataMapper($options['multiple'] ? new CheckboxListMapper() : new RadioListMapper());
- // Initialize all choices before doing the index check below.
- // This helps in cases where index checks are optimized for non
- // initialized choice lists. For example, when using an SQL driver,
- // the index check would read in one SQL query and the initialization
- // requires another SQL query. When the initialization is done first,
- // one SQL query is sufficient.
- $choiceListView = $this->createChoiceListView($choiceList, $options);
- $builder->setAttribute('choice_list_view', $choiceListView);
- // Check if the choices already contain the empty value
- // Only add the placeholder option if this is not the case
- if (null !== $options['placeholder'] && 0 === \count($choiceList->getChoicesForValues(['']))) {
- $placeholderView = new ChoiceView(null, '', $options['placeholder'], $options['placeholder_attr']);
- // "placeholder" is a reserved name
- $this->addSubForm($builder, 'placeholder', $placeholderView, $options);
- }
- $this->addSubForms($builder, $choiceListView->preferredChoices, $options);
- $this->addSubForms($builder, $choiceListView->choices, $options);
- }
- if ($options['expanded'] || $options['multiple']) {
- // Make sure that scalar, submitted values are converted to arrays
- // which can be submitted to the checkboxes/radio buttons
- $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) use ($choiceList, $options, &$unknownValues) {
- /** @var PreSubmitEvent $event */
- $form = $event->getForm();
- $data = $event->getData();
- // Since the type always use mapper an empty array will not be
- // considered as empty in Form::submit(), we need to evaluate
- // empty data here so its value is submitted to sub forms
- if (null === $data) {
- $emptyData = $form->getConfig()->getEmptyData();
- $data = $emptyData instanceof \Closure ? $emptyData($form, $data) : $emptyData;
- }
- // Convert the submitted data to a string, if scalar, before
- // casting it to an array
- if (!\is_array($data)) {
- if ($options['multiple']) {
- throw new TransformationFailedException('Expected an array.');
- }
- $data = (array) (string) $data;
- }
- // A map from submitted values to integers
- $valueMap = array_flip($data);
- // Make a copy of the value map to determine whether any unknown
- // values were submitted
- $unknownValues = $valueMap;
- // Reconstruct the data as mapping from child names to values
- $knownValues = [];
- if ($options['expanded']) {
- /** @var FormInterface $child */
- foreach ($form as $child) {
- $value = $child->getConfig()->getOption('value');
- // Add the value to $data with the child's name as key
- if (isset($valueMap[$value])) {
- $knownValues[$child->getName()] = $value;
- unset($unknownValues[$value]);
- continue;
- }
- $knownValues[$child->getName()] = null;
- }
- } else {
- foreach ($choiceList->getChoicesForValues($data) as $key => $choice) {
- $knownValues[] = $data[$key];
- unset($unknownValues[$data[$key]]);
- }
- }
- // The empty value is always known, independent of whether a
- // field exists for it or not
- unset($unknownValues['']);
- // Throw exception if unknown values were submitted (multiple choices will be handled in a different event listener below)
- if (\count($unknownValues) > 0 && !$options['multiple']) {
- throw new TransformationFailedException(\sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues))));
- }
- $event->setData($knownValues);
- });
- }
- if ($options['multiple']) {
- $messageTemplate = $options['invalid_message'] ?? 'The value {{ value }} is not valid.';
- $translator = $this->translator;
- $builder->addEventListener(FormEvents::POST_SUBMIT, static function (FormEvent $event) use (&$unknownValues, $messageTemplate, $translator) {
- // Throw exception if unknown values were submitted
- if (\count($unknownValues) > 0) {
- $form = $event->getForm();
- $clientDataAsString = \is_scalar($form->getViewData()) ? (string) $form->getViewData() : (\is_array($form->getViewData()) ? implode('", "', array_keys($unknownValues)) : \gettype($form->getViewData()));
- if ($translator) {
- $message = $translator->trans($messageTemplate, ['{{ value }}' => $clientDataAsString], 'validators');
- } else {
- $message = strtr($messageTemplate, ['{{ value }}' => $clientDataAsString]);
- }
- $form->addError(new FormError($message, $messageTemplate, ['{{ value }}' => $clientDataAsString], null, new TransformationFailedException(\sprintf('The choices "%s" do not exist in the choice list.', $clientDataAsString))));
- }
- });
- // <select> tag with "multiple" option or list of checkbox inputs
- $builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));
- } else {
- // <select> tag without "multiple" option or list of radio inputs
- $builder->addViewTransformer(new ChoiceToValueTransformer($choiceList));
- }
- if ($options['multiple'] && $options['by_reference']) {
- // Make sure the collection created during the client->norm
- // transformation is merged back into the original collection
- $builder->addEventSubscriber(new MergeCollectionListener(true, true));
- }
- // To avoid issues when the submitted choices are arrays (i.e. array to string conversions),
- // we have to ensure that all elements of the submitted choice data are NULL, strings or ints.
- $builder->addEventListener(FormEvents::PRE_SUBMIT, static function (FormEvent $event) {
- $data = $event->getData();
- if (!\is_array($data)) {
- return;
- }
- foreach ($data as $v) {
- if (null !== $v && !\is_string($v) && !\is_int($v)) {
- throw new TransformationFailedException('All choices submitted must be NULL, strings or ints.');
- }
- }
- }, 256);
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $choiceTranslationDomain = $options['choice_translation_domain'];
- if ($view->parent && null === $choiceTranslationDomain) {
- $choiceTranslationDomain = $view->vars['translation_domain'];
- }
- /** @var ChoiceListInterface $choiceList */
- $choiceList = $form->getConfig()->getAttribute('choice_list');
- /** @var ChoiceListView $choiceListView */
- $choiceListView = $form->getConfig()->hasAttribute('choice_list_view')
- ? $form->getConfig()->getAttribute('choice_list_view')
- : $this->createChoiceListView($choiceList, $options);
- $view->vars = array_replace($view->vars, [
- 'multiple' => $options['multiple'],
- 'expanded' => $options['expanded'],
- 'preferred_choices' => $choiceListView->preferredChoices,
- 'choices' => $choiceListView->choices,
- 'separator' => $options['separator'],
- 'separator_html' => $options['separator_html'],
- 'placeholder' => null,
- 'placeholder_attr' => [],
- 'choice_translation_domain' => $choiceTranslationDomain,
- 'choice_translation_parameters' => $options['choice_translation_parameters'],
- ]);
- // The decision, whether a choice is selected, is potentially done
- // thousand of times during the rendering of a template. Provide a
- // closure here that is optimized for the value of the form, to
- // avoid making the type check inside the closure.
- if ($options['multiple']) {
- $view->vars['is_selected'] = static fn ($choice, array $values) => \in_array($choice, $values, true);
- } else {
- $view->vars['is_selected'] = static fn ($choice, $value) => $choice === $value;
- }
- // Check if the choices already contain the empty value
- $view->vars['placeholder_in_choices'] = $choiceListView->hasPlaceholder();
- // Only add the empty value option if this is not the case
- if (null !== $options['placeholder'] && !$view->vars['placeholder_in_choices']) {
- $view->vars['placeholder'] = $options['placeholder'];
- $view->vars['placeholder_attr'] = $options['placeholder_attr'];
- }
- if ($options['multiple'] && !$options['expanded']) {
- // Add "[]" to the name in case a select tag with multiple options is
- // displayed. Otherwise only one of the selected options is sent in the
- // POST request.
- $view->vars['full_name'] .= '[]';
- }
- }
- public function finishView(FormView $view, FormInterface $form, array $options): void
- {
- if ($options['expanded']) {
- // Radio buttons should have the same name as the parent
- $childName = $view->vars['full_name'];
- // Checkboxes should append "[]" to allow multiple selection
- if ($options['multiple']) {
- $childName .= '[]';
- }
- foreach ($view as $childView) {
- $childView->vars['full_name'] = $childName;
- }
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $emptyData = static function (Options $options) {
- if ($options['expanded'] && !$options['multiple']) {
- return null;
- }
- if ($options['multiple']) {
- return [];
- }
- return '';
- };
- $placeholderDefault = static fn (Options $options) => $options['required'] ? null : '';
- $placeholderNormalizer = static function (Options $options, $placeholder) {
- if ($options['multiple']) {
- // never use an empty value for this case
- return null;
- } elseif ($options['required'] && ($options['expanded'] || isset($options['attr']['size']) && $options['attr']['size'] > 1)) {
- // placeholder for required radio buttons or a select with size > 1 does not make sense
- return null;
- } elseif (false === $placeholder) {
- // an empty value should be added but the user decided otherwise
- return null;
- } elseif ($options['expanded'] && '' === $placeholder) {
- // never use an empty label for radio buttons
- return 'None';
- }
- // empty value has been set explicitly
- return $placeholder;
- };
- $compound = static fn (Options $options) => $options['expanded'];
- $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) {
- if (true === $choiceTranslationDomain) {
- return $options['translation_domain'];
- }
- return $choiceTranslationDomain;
- };
- $choiceLoaderNormalizer = static function (Options $options, ?ChoiceLoaderInterface $choiceLoader) {
- if (!$options['choice_lazy']) {
- return $choiceLoader;
- }
- if (null === $choiceLoader) {
- throw new LogicException('The "choice_lazy" option can only be used if the "choice_loader" option is set.');
- }
- return new LazyChoiceLoader($choiceLoader);
- };
- $resolver->setDefaults([
- 'multiple' => false,
- 'expanded' => false,
- 'choices' => [],
- 'choice_filter' => null,
- 'choice_lazy' => false,
- 'choice_loader' => null,
- 'choice_label' => null,
- 'choice_name' => null,
- 'choice_value' => null,
- 'choice_attr' => null,
- 'choice_translation_parameters' => [],
- 'preferred_choices' => [],
- 'separator' => '-------------------',
- 'separator_html' => false,
- 'duplicate_preferred_choices' => true,
- 'group_by' => null,
- 'empty_data' => $emptyData,
- 'placeholder' => $placeholderDefault,
- 'placeholder_attr' => [],
- 'error_bubbling' => false,
- 'compound' => $compound,
- // The view data is always a string or an array of strings,
- // even if the "data" option is manually set to an object.
- // See https://github.com/symfony/symfony/pull/5582
- 'data_class' => null,
- 'choice_translation_domain' => true,
- 'trim' => false,
- 'invalid_message' => 'The selected choice is invalid.',
- ]);
- $resolver->setNormalizer('placeholder', $placeholderNormalizer);
- $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
- $resolver->setNormalizer('choice_loader', $choiceLoaderNormalizer);
- $resolver->setAllowedTypes('choices', ['null', 'array', \Traversable::class]);
- $resolver->setAllowedTypes('choice_translation_domain', ['null', 'bool', 'string']);
- $resolver->setAllowedTypes('choice_lazy', 'bool');
- $resolver->setAllowedTypes('choice_loader', ['null', ChoiceLoaderInterface::class, ChoiceLoader::class]);
- $resolver->setAllowedTypes('choice_filter', ['null', 'callable', 'string', PropertyPath::class, ChoiceFilter::class]);
- $resolver->setAllowedTypes('choice_label', ['null', 'bool', 'callable', 'string', PropertyPath::class, ChoiceLabel::class]);
- $resolver->setAllowedTypes('choice_name', ['null', 'callable', 'string', PropertyPath::class, ChoiceFieldName::class]);
- $resolver->setAllowedTypes('choice_value', ['null', 'callable', 'string', PropertyPath::class, ChoiceValue::class]);
- $resolver->setAllowedTypes('choice_attr', ['null', 'array', 'callable', 'string', PropertyPath::class, ChoiceAttr::class]);
- $resolver->setAllowedTypes('choice_translation_parameters', ['null', 'array', 'callable', ChoiceTranslationParameters::class]);
- $resolver->setAllowedTypes('placeholder_attr', ['array']);
- $resolver->setAllowedTypes('preferred_choices', ['array', \Traversable::class, 'callable', 'string', PropertyPath::class, PreferredChoice::class]);
- $resolver->setAllowedTypes('separator', ['string']);
- $resolver->setAllowedTypes('separator_html', ['bool']);
- $resolver->setAllowedTypes('duplicate_preferred_choices', 'bool');
- $resolver->setAllowedTypes('group_by', ['null', 'callable', 'string', PropertyPath::class, GroupBy::class]);
- $resolver->setInfo('choice_lazy', 'Load choices on demand. When set to true, only the selected choices are loaded and rendered.');
- }
- public function getBlockPrefix(): string
- {
- return 'choice';
- }
- /**
- * Adds the sub fields for an expanded choice field.
- */
- private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options): void
- {
- foreach ($choiceViews as $name => $choiceView) {
- // Flatten groups
- if (\is_array($choiceView)) {
- $this->addSubForms($builder, $choiceView, $options);
- continue;
- }
- if ($choiceView instanceof ChoiceGroupView) {
- $this->addSubForms($builder, $choiceView->choices, $options);
- continue;
- }
- $this->addSubForm($builder, $name, $choiceView, $options);
- }
- }
- private function addSubForm(FormBuilderInterface $builder, string $name, ChoiceView $choiceView, array $options): void
- {
- $choiceOpts = [
- 'value' => $choiceView->value,
- 'label' => $choiceView->label,
- 'label_html' => $options['label_html'],
- 'attr' => $choiceView->attr,
- 'label_translation_parameters' => $choiceView->labelTranslationParameters,
- 'translation_domain' => $options['choice_translation_domain'],
- 'block_name' => 'entry',
- ];
- if ($options['multiple']) {
- $choiceType = CheckboxType::class;
- // The user can check 0 or more checkboxes. If required
- // is true, they are required to check all of them.
- $choiceOpts['required'] = false;
- } else {
- $choiceType = RadioType::class;
- }
- $builder->add($name, $choiceType, $choiceOpts);
- }
- private function createChoiceList(array $options): ChoiceListInterface
- {
- if (null !== $options['choice_loader']) {
- return $this->choiceListFactory->createListFromLoader(
- $options['choice_loader'],
- $options['choice_value'],
- $options['choice_filter']
- );
- }
- // Harden against NULL values (like in EntityType and ModelType)
- $choices = $options['choices'] ?? [];
- return $this->choiceListFactory->createListFromChoices(
- $choices,
- $options['choice_value'],
- $options['choice_filter']
- );
- }
- private function createChoiceListView(ChoiceListInterface $choiceList, array $options): ChoiceListView
- {
- return $this->choiceListFactory->createView(
- $choiceList,
- $options['preferred_choices'],
- $options['choice_label'],
- $options['choice_name'],
- $options['group_by'],
- $options['choice_attr'],
- $options['choice_translation_parameters'],
- $options['duplicate_preferred_choices'],
- );
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/LanguageType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\ChoiceList\ChoiceList;
- use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Intl\Exception\MissingResourceException;
- use Symfony\Component\Intl\Intl;
- use Symfony\Component\Intl\Languages;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class LanguageType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'choice_loader' => function (Options $options) {
- if (!class_exists(Intl::class)) {
- throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
- }
- $choiceTranslationLocale = $options['choice_translation_locale'];
- $useAlpha3Codes = $options['alpha3'];
- $choiceSelfTranslation = $options['choice_self_translation'];
- return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static function () use ($choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation) {
- if (true === $choiceSelfTranslation) {
- foreach (Languages::getLanguageCodes() as $alpha2Code) {
- try {
- $languageCode = $useAlpha3Codes ? Languages::getAlpha3Code($alpha2Code) : $alpha2Code;
- $languagesList[$languageCode] = Languages::getName($alpha2Code, $alpha2Code);
- } catch (MissingResourceException) {
- // ignore errors like "Couldn't read the indices for the locale 'meta'"
- }
- }
- } else {
- $languagesList = $useAlpha3Codes ? Languages::getAlpha3Names($choiceTranslationLocale) : Languages::getNames($choiceTranslationLocale);
- }
- return array_flip($languagesList);
- }), [$choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation]);
- },
- 'choice_translation_domain' => false,
- 'choice_translation_locale' => null,
- 'alpha3' => false,
- 'choice_self_translation' => false,
- 'invalid_message' => 'Please select a valid language.',
- ]);
- $resolver->setAllowedTypes('choice_self_translation', ['bool']);
- $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
- $resolver->setAllowedTypes('alpha3', 'bool');
- $resolver->setNormalizer('choice_self_translation', static function (Options $options, $value) {
- if (true === $value && $options['choice_translation_locale']) {
- throw new LogicException('Cannot use the "choice_self_translation" and "choice_translation_locale" options at the same time. Remove one of them.');
- }
- return $value;
- });
- }
- public function getParent(): ?string
- {
- return ChoiceType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'language';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/CurrencyType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\ChoiceList\ChoiceList;
- use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Intl\Currencies;
- use Symfony\Component\Intl\Intl;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class CurrencyType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'choice_loader' => function (Options $options) {
- if (!class_exists(Intl::class)) {
- throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
- }
- $choiceTranslationLocale = $options['choice_translation_locale'];
- return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip(Currencies::getNames($choiceTranslationLocale))), $choiceTranslationLocale);
- },
- 'choice_translation_domain' => false,
- 'choice_translation_locale' => null,
- 'invalid_message' => 'Please select a valid currency.',
- ]);
- $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
- }
- public function getParent(): ?string
- {
- return ChoiceType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'currency';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/WeekType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Extension\Core\DataTransformer\WeekToArrayTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\Form\ReversedTransformer;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class WeekType extends AbstractType
- {
- private const WIDGETS = [
- 'text' => IntegerType::class,
- 'choice' => ChoiceType::class,
- ];
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- if ('string' === $options['input']) {
- $builder->addModelTransformer(new WeekToArrayTransformer());
- }
- if ('single_text' === $options['widget']) {
- $builder->addViewTransformer(new ReversedTransformer(new WeekToArrayTransformer()));
- } else {
- $yearOptions = $weekOptions = [
- 'error_bubbling' => true,
- 'empty_data' => '',
- ];
- // when the form is compound the entries of the array are ignored in favor of children data
- // so we need to handle the cascade setting here
- $emptyData = $builder->getEmptyData() ?: [];
- $yearOptions['empty_data'] = $emptyData['year'] ?? '';
- $weekOptions['empty_data'] = $emptyData['week'] ?? '';
- if (isset($options['invalid_message'])) {
- $yearOptions['invalid_message'] = $options['invalid_message'];
- $weekOptions['invalid_message'] = $options['invalid_message'];
- }
- if (isset($options['invalid_message_parameters'])) {
- $yearOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
- $weekOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
- }
- if ('choice' === $options['widget']) {
- // Only pass a subset of the options to children
- $yearOptions['choices'] = array_combine($options['years'], $options['years']);
- $yearOptions['placeholder'] = $options['placeholder']['year'];
- $yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year'];
- $weekOptions['choices'] = array_combine($options['weeks'], $options['weeks']);
- $weekOptions['placeholder'] = $options['placeholder']['week'];
- $weekOptions['choice_translation_domain'] = $options['choice_translation_domain']['week'];
- // Append generic carry-along options
- foreach (['required', 'translation_domain'] as $passOpt) {
- $yearOptions[$passOpt] = $options[$passOpt];
- $weekOptions[$passOpt] = $options[$passOpt];
- }
- }
- $builder->add('year', self::WIDGETS[$options['widget']], $yearOptions);
- $builder->add('week', self::WIDGETS[$options['widget']], $weekOptions);
- }
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $view->vars['widget'] = $options['widget'];
- if ($options['html5']) {
- $view->vars['type'] = 'week';
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $compound = static fn (Options $options) => 'single_text' !== $options['widget'];
- $placeholderDefault = static fn (Options $options) => $options['required'] ? null : '';
- $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) {
- if (\is_array($placeholder)) {
- $default = $placeholderDefault($options);
- return array_merge(
- ['year' => $default, 'week' => $default],
- $placeholder
- );
- }
- return [
- 'year' => $placeholder,
- 'week' => $placeholder,
- ];
- };
- $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) {
- if (\is_array($choiceTranslationDomain)) {
- return array_replace(
- ['year' => false, 'week' => false],
- $choiceTranslationDomain
- );
- }
- return [
- 'year' => $choiceTranslationDomain,
- 'week' => $choiceTranslationDomain,
- ];
- };
- $resolver->setDefaults([
- 'years' => range(date('Y') - 10, date('Y') + 10),
- 'weeks' => array_combine(range(1, 53), range(1, 53)),
- 'widget' => 'single_text',
- 'input' => 'array',
- 'placeholder' => $placeholderDefault,
- 'html5' => static fn (Options $options) => 'single_text' === $options['widget'],
- 'error_bubbling' => false,
- 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '',
- 'compound' => $compound,
- 'choice_translation_domain' => false,
- 'invalid_message' => 'Please enter a valid week.',
- ]);
- $resolver->setNormalizer('placeholder', $placeholderNormalizer);
- $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
- $resolver->setNormalizer('html5', static function (Options $options, $html5) {
- if ($html5 && 'single_text' !== $options['widget']) {
- throw new LogicException(\sprintf('The "widget" option of "%s" must be set to "single_text" when the "html5" option is enabled.', self::class));
- }
- return $html5;
- });
- $resolver->setAllowedValues('input', [
- 'string',
- 'array',
- ]);
- $resolver->setAllowedValues('widget', [
- 'single_text',
- 'text',
- 'choice',
- ]);
- $resolver->setAllowedTypes('years', 'int[]');
- $resolver->setAllowedTypes('weeks', 'int[]');
- }
- public function getBlockPrefix(): string
- {
- return 'week';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/MoneyType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class MoneyType extends AbstractType
- {
- protected static array $patterns = [];
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- // Values used in HTML5 number inputs should be formatted as in "1234.5", ie. 'en' format without grouping,
- // according to https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats
- $builder
- ->addViewTransformer(new MoneyToLocalizedStringTransformer(
- $options['scale'],
- $options['grouping'],
- $options['rounding_mode'],
- $options['divisor'],
- $options['html5'] ? 'en' : null,
- $options['input'],
- ))
- ;
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $view->vars['money_pattern'] = self::getPattern($options['currency']);
- if ($options['html5']) {
- $view->vars['type'] = 'number';
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'scale' => 2,
- 'grouping' => false,
- 'rounding_mode' => \NumberFormatter::ROUND_HALFUP,
- 'divisor' => 1,
- 'currency' => 'EUR',
- 'compound' => false,
- 'html5' => false,
- 'invalid_message' => 'Please enter a valid money amount.',
- 'input' => 'float',
- ]);
- $resolver->setAllowedValues('rounding_mode', [
- \NumberFormatter::ROUND_FLOOR,
- \NumberFormatter::ROUND_DOWN,
- \NumberFormatter::ROUND_HALFDOWN,
- \NumberFormatter::ROUND_HALFEVEN,
- \NumberFormatter::ROUND_HALFUP,
- \NumberFormatter::ROUND_UP,
- \NumberFormatter::ROUND_CEILING,
- ]);
- $resolver->setAllowedTypes('scale', 'int');
- $resolver->setAllowedTypes('html5', 'bool');
- $resolver->setAllowedValues('input', ['float', 'integer']);
- $resolver->setNormalizer('grouping', static function (Options $options, $value) {
- if ($value && $options['html5']) {
- throw new LogicException('Cannot use the "grouping" option when the "html5" option is enabled.');
- }
- return $value;
- });
- }
- public function getBlockPrefix(): string
- {
- return 'money';
- }
- /**
- * Returns the pattern for this locale in UTF-8.
- *
- * The pattern contains the placeholder "{{ widget }}" where the HTML tag should
- * be inserted
- */
- protected static function getPattern(?string $currency): string
- {
- if (!$currency) {
- return '{{ widget }}';
- }
- $locale = \Locale::getDefault();
- if (!isset(self::$patterns[$locale])) {
- self::$patterns[$locale] = [];
- }
- if (!isset(self::$patterns[$locale][$currency])) {
- $format = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
- $pattern = $format->formatCurrency('123', $currency);
- // the spacings between currency symbol and number are ignored, because
- // a single space leads to better readability in combination with input
- // fields
- // the regex also considers non-break spaces (0xC2 or 0xA0 in UTF-8)
- preg_match('/^([^\s\xc2\xa0]*)[\s\xc2\xa0]*123(?:[,.]0+)?[\s\xc2\xa0]*([^\s\xc2\xa0]*)$/u', $pattern, $matches);
- if (!empty($matches[1])) {
- self::$patterns[$locale][$currency] = $matches[1].' {{ widget }}';
- } elseif (!empty($matches[2])) {
- self::$patterns[$locale][$currency] = '{{ widget }} '.$matches[2];
- } else {
- self::$patterns[$locale][$currency] = '{{ widget }}';
- }
- }
- return self::$patterns[$locale][$currency];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/EmailType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class EmailType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'invalid_message' => 'Please enter a valid email address.',
- ]);
- }
- public function getParent(): ?string
- {
- return TextType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'email';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/TextType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class TextType extends AbstractType implements DataTransformerInterface
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- // When empty_data is explicitly set to an empty string,
- // a string should always be returned when NULL is submitted
- // This gives more control and thus helps preventing some issues
- // with PHP 7 which allows type hinting strings in functions
- // See https://github.com/symfony/symfony/issues/5906#issuecomment-203189375
- if ('' === $options['empty_data']) {
- $builder->addViewTransformer($this);
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'compound' => false,
- ]);
- }
- public function getBlockPrefix(): string
- {
- return 'text';
- }
- public function transform(mixed $data): mixed
- {
- // Model data should not be transformed
- return $data;
- }
- public function reverseTransform(mixed $data): mixed
- {
- return $data ?? '';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/FormType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor;
- use Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor;
- use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
- use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
- use Symfony\Component\Form\Extension\Core\EventListener\TrimListener;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- use Symfony\Component\PropertyAccess\PropertyAccess;
- use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
- use Symfony\Contracts\Translation\TranslatableInterface;
- class FormType extends BaseType
- {
- private DataMapper $dataMapper;
- public function __construct(?PropertyAccessorInterface $propertyAccessor = null)
- {
- $this->dataMapper = new DataMapper(new ChainAccessor([
- new CallbackAccessor(),
- new PropertyPathAccessor($propertyAccessor ?? PropertyAccess::createPropertyAccessor()),
- ]));
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- parent::buildForm($builder, $options);
- $isDataOptionSet = \array_key_exists('data', $options);
- $builder
- ->setRequired($options['required'])
- ->setErrorBubbling($options['error_bubbling'])
- ->setEmptyData($options['empty_data'])
- ->setPropertyPath($options['property_path'])
- ->setMapped($options['mapped'])
- ->setByReference($options['by_reference'])
- ->setInheritData($options['inherit_data'])
- ->setCompound($options['compound'])
- ->setData($isDataOptionSet ? $options['data'] : null)
- ->setDataLocked($isDataOptionSet)
- ->setDataMapper($options['compound'] ? $this->dataMapper : null)
- ->setMethod($options['method'])
- ->setAction($options['action']);
- if ($options['trim']) {
- $builder->addEventSubscriber(new TrimListener());
- }
- $builder->setIsEmptyCallback($options['is_empty_callback']);
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- parent::buildView($view, $form, $options);
- $name = $form->getName();
- $helpTranslationParameters = $options['help_translation_parameters'];
- if ($view->parent) {
- if ('' === $name) {
- throw new LogicException('Form node with empty name can be used only as root form node.');
- }
- // Complex fields are read-only if they themselves or their parents are.
- if (!isset($view->vars['attr']['readonly']) && isset($view->parent->vars['attr']['readonly']) && false !== $view->parent->vars['attr']['readonly']) {
- $view->vars['attr']['readonly'] = true;
- }
- $helpTranslationParameters = array_merge($view->parent->vars['help_translation_parameters'], $helpTranslationParameters);
- }
- $formConfig = $form->getConfig();
- $view->vars = array_replace($view->vars, [
- 'errors' => $form->getErrors(),
- 'valid' => $form->isSubmitted() ? $form->isValid() : true,
- 'value' => $form->getViewData(),
- 'data' => $form->getNormData(),
- 'required' => $form->isRequired(),
- 'label_attr' => $options['label_attr'],
- 'help' => $options['help'],
- 'help_attr' => $options['help_attr'],
- 'help_html' => $options['help_html'],
- 'help_translation_parameters' => $helpTranslationParameters,
- 'compound' => $formConfig->getCompound(),
- 'method' => $formConfig->getMethod(),
- 'action' => $formConfig->getAction(),
- 'submitted' => $form->isSubmitted(),
- ]);
- }
- public function finishView(FormView $view, FormInterface $form, array $options): void
- {
- $multipart = false;
- foreach ($view->children as $child) {
- if ($child->vars['multipart']) {
- $multipart = true;
- break;
- }
- }
- $view->vars['multipart'] = $multipart;
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- parent::configureOptions($resolver);
- // Derive "data_class" option from passed "data" object
- $dataClass = static fn (Options $options) => isset($options['data']) && \is_object($options['data']) ? $options['data']::class : null;
- // Derive "empty_data" closure from "data_class" option
- $emptyData = static function (Options $options) {
- $class = $options['data_class'];
- if (null !== $class) {
- return static fn (FormInterface $form) => $form->isEmpty() && !$form->isRequired() ? null : new $class();
- }
- return static fn (FormInterface $form) => $form->getConfig()->getCompound() ? [] : '';
- };
- // Wrap "post_max_size_message" in a closure to translate it lazily
- $uploadMaxSizeMessage = static fn (Options $options) => static fn () => $options['post_max_size_message'];
- // For any form that is not represented by a single HTML control,
- // errors should bubble up by default
- $errorBubbling = static fn (Options $options) => $options['compound'] && !$options['inherit_data'];
- // If data is given, the form is locked to that data
- // (independent of its value)
- $resolver->setDefined([
- 'data',
- ]);
- $resolver->setDefaults([
- 'data_class' => $dataClass,
- 'empty_data' => $emptyData,
- 'trim' => true,
- 'required' => true,
- 'property_path' => null,
- 'mapped' => true,
- 'by_reference' => true,
- 'error_bubbling' => $errorBubbling,
- 'label_attr' => [],
- 'inherit_data' => false,
- 'compound' => true,
- 'method' => 'POST',
- // According to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt)
- // section 4.2., empty URIs are considered same-document references
- 'action' => '',
- 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.',
- 'upload_max_size_message' => $uploadMaxSizeMessage, // internal
- 'allow_file_upload' => false,
- 'help' => null,
- 'help_attr' => [],
- 'help_html' => false,
- 'help_translation_parameters' => [],
- 'invalid_message' => 'This value is not valid.',
- 'invalid_message_parameters' => [],
- 'is_empty_callback' => null,
- 'getter' => null,
- 'setter' => null,
- ]);
- $resolver->setAllowedTypes('label_attr', 'array');
- $resolver->setAllowedTypes('action', 'string');
- $resolver->setAllowedTypes('upload_max_size_message', ['callable']);
- $resolver->setAllowedTypes('help', ['string', 'null', TranslatableInterface::class]);
- $resolver->setAllowedTypes('help_attr', 'array');
- $resolver->setAllowedTypes('help_html', 'bool');
- $resolver->setAllowedTypes('is_empty_callback', ['null', 'callable']);
- $resolver->setAllowedTypes('getter', ['null', 'callable']);
- $resolver->setAllowedTypes('setter', ['null', 'callable']);
- $resolver->setInfo('getter', 'A callable that accepts two arguments (the view data and the current form field) and must return a value.');
- $resolver->setInfo('setter', 'A callable that accepts three arguments (a reference to the view data, the submitted value and the current form field).');
- }
- public function getParent(): ?string
- {
- return null;
- }
- public function getBlockPrefix(): string
- {
- return 'form';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/RangeType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class RangeType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'invalid_message' => 'Please choose a valid range.',
- ]);
- }
- public function getParent(): ?string
- {
- return TextType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'range';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/PasswordType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class PasswordType extends AbstractType
- {
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- if ($options['always_empty'] || !$form->isSubmitted()) {
- $view->vars['value'] = '';
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'always_empty' => true,
- 'trim' => false,
- 'invalid_message' => 'The password is invalid.',
- ]);
- }
- public function getParent(): ?string
- {
- return TextType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'password';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/FileType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Event\PreSubmitEvent;
- use Symfony\Component\Form\FileUploadError;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\HttpFoundation\File\File;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- use Symfony\Contracts\Translation\TranslatorInterface;
- class FileType extends AbstractType
- {
- public const KIB_BYTES = 1024;
- public const MIB_BYTES = 1048576;
- private const SUFFIXES = [
- 1 => 'bytes',
- self::KIB_BYTES => 'KiB',
- self::MIB_BYTES => 'MiB',
- ];
- public function __construct(
- private ?TranslatorInterface $translator = null,
- ) {
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- // Ensure that submitted data is always an uploaded file or an array of some
- $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
- /** @var PreSubmitEvent $event */
- $form = $event->getForm();
- $requestHandler = $form->getConfig()->getRequestHandler();
- if ($options['multiple']) {
- $data = [];
- $files = $event->getData();
- if (!\is_array($files)) {
- $files = [];
- }
- foreach ($files as $file) {
- if ($requestHandler->isFileUpload($file)) {
- $data[] = $file;
- if (method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($file)) {
- $form->addError($this->getFileUploadError($errorCode));
- }
- }
- }
- // Since the array is never considered empty in the view data format
- // on submission, we need to evaluate the configured empty data here
- if ([] === $data) {
- $emptyData = $form->getConfig()->getEmptyData();
- $data = $emptyData instanceof \Closure ? $emptyData($form, $data) : $emptyData;
- }
- $event->setData($data);
- } elseif ($requestHandler->isFileUpload($event->getData()) && method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($event->getData())) {
- $form->addError($this->getFileUploadError($errorCode));
- } elseif (!$requestHandler->isFileUpload($event->getData())) {
- $event->setData(null);
- }
- });
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- if ($options['multiple']) {
- $view->vars['full_name'] .= '[]';
- $view->vars['attr']['multiple'] = 'multiple';
- }
- $view->vars = array_replace($view->vars, [
- 'type' => 'file',
- 'value' => '',
- ]);
- }
- public function finishView(FormView $view, FormInterface $form, array $options): void
- {
- $view->vars['multipart'] = true;
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $dataClass = null;
- if (class_exists(File::class)) {
- $dataClass = static fn (Options $options) => $options['multiple'] ? null : File::class;
- }
- $emptyData = static fn (Options $options) => $options['multiple'] ? [] : null;
- $resolver->setDefaults([
- 'compound' => false,
- 'data_class' => $dataClass,
- 'empty_data' => $emptyData,
- 'multiple' => false,
- 'allow_file_upload' => true,
- 'invalid_message' => 'Please select a valid file.',
- ]);
- }
- public function getBlockPrefix(): string
- {
- return 'file';
- }
- private function getFileUploadError(int $errorCode): FileUploadError
- {
- $messageParameters = [];
- if (\UPLOAD_ERR_INI_SIZE === $errorCode) {
- [$limitAsString, $suffix] = $this->factorizeSizes(0, self::getMaxFilesize());
- $messageTemplate = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.';
- $messageParameters = [
- '{{ limit }}' => $limitAsString,
- '{{ suffix }}' => $suffix,
- ];
- } elseif (\UPLOAD_ERR_FORM_SIZE === $errorCode) {
- $messageTemplate = 'The file is too large.';
- } else {
- $messageTemplate = 'The file could not be uploaded.';
- }
- if (null !== $this->translator) {
- $message = $this->translator->trans($messageTemplate, $messageParameters, 'validators');
- } else {
- $message = strtr($messageTemplate, $messageParameters);
- }
- return new FileUploadError($message, $messageTemplate, $messageParameters);
- }
- /**
- * Returns the maximum size of an uploaded file as configured in php.ini.
- *
- * This method should be kept in sync with Symfony\Component\HttpFoundation\File\UploadedFile::getMaxFilesize().
- */
- private static function getMaxFilesize(): int|float
- {
- $iniMax = strtolower(\ini_get('upload_max_filesize'));
- if ('' === $iniMax) {
- return \PHP_INT_MAX;
- }
- $max = ltrim($iniMax, '+');
- if (str_starts_with($max, '0x')) {
- $max = \intval($max, 16);
- } elseif (str_starts_with($max, '0')) {
- $max = \intval($max, 8);
- } else {
- $max = (int) $max;
- }
- switch (substr($iniMax, -1)) {
- case 't': $max *= 1024;
- // no break
- case 'g': $max *= 1024;
- // no break
- case 'm': $max *= 1024;
- // no break
- case 'k': $max *= 1024;
- }
- return $max;
- }
- /**
- * Converts the limit to the smallest possible number
- * (i.e. try "MB", then "kB", then "bytes").
- *
- * This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::factorizeSizes().
- */
- private function factorizeSizes(int $size, int|float $limit): array
- {
- $coef = self::MIB_BYTES;
- $coefFactor = self::KIB_BYTES;
- $limitAsString = (string) ($limit / $coef);
- // Restrict the limit to 2 decimals (without rounding! we
- // need the precise value)
- while (self::moreDecimalsThan($limitAsString, 2)) {
- $coef /= $coefFactor;
- $limitAsString = (string) ($limit / $coef);
- }
- // Convert size to the same measure, but round to 2 decimals
- $sizeAsString = (string) round($size / $coef, 2);
- // If the size and limit produce the same string output
- // (due to rounding), reduce the coefficient
- while ($sizeAsString === $limitAsString) {
- $coef /= $coefFactor;
- $limitAsString = (string) ($limit / $coef);
- $sizeAsString = (string) round($size / $coef, 2);
- }
- return [$limitAsString, self::SUFFIXES[$coef]];
- }
- /**
- * This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::moreDecimalsThan().
- */
- private static function moreDecimalsThan(string $double, int $numberOfDecimals): bool
- {
- return \strlen($double) > \strlen(round($double, $numberOfDecimals));
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/CountryType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\ChoiceList\ChoiceList;
- use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Intl\Countries;
- use Symfony\Component\Intl\Intl;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class CountryType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'choice_loader' => function (Options $options) {
- if (!class_exists(Intl::class)) {
- throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
- }
- $choiceTranslationLocale = $options['choice_translation_locale'];
- $alpha3 = $options['alpha3'];
- return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip($alpha3 ? Countries::getAlpha3Names($choiceTranslationLocale) : Countries::getNames($choiceTranslationLocale))), [$choiceTranslationLocale, $alpha3]);
- },
- 'choice_translation_domain' => false,
- 'choice_translation_locale' => null,
- 'alpha3' => false,
- 'invalid_message' => 'Please select a valid country.',
- ]);
- $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
- $resolver->setAllowedTypes('alpha3', 'bool');
- }
- public function getParent(): ?string
- {
- return ChoiceType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'country';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/ButtonType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\ButtonTypeInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * A form button.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ButtonType extends BaseType implements ButtonTypeInterface
- {
- public function getParent(): ?string
- {
- return null;
- }
- public function getBlockPrefix(): string
- {
- return 'button';
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- parent::configureOptions($resolver);
- $resolver->setDefault('auto_initialize', false);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/PercentType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class PercentType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $builder->addViewTransformer(new PercentToLocalizedStringTransformer(
- $options['scale'],
- $options['type'],
- $options['rounding_mode'],
- $options['html5']
- ));
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $view->vars['symbol'] = $options['symbol'];
- if ($options['html5']) {
- $view->vars['type'] = 'number';
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'scale' => 0,
- 'rounding_mode' => \NumberFormatter::ROUND_HALFUP,
- 'symbol' => '%',
- 'type' => 'fractional',
- 'compound' => false,
- 'html5' => false,
- 'invalid_message' => 'Please enter a percentage value.',
- ]);
- $resolver->setAllowedValues('type', [
- 'fractional',
- 'integer',
- ]);
- $resolver->setAllowedValues('rounding_mode', [
- \NumberFormatter::ROUND_FLOOR,
- \NumberFormatter::ROUND_DOWN,
- \NumberFormatter::ROUND_HALFDOWN,
- \NumberFormatter::ROUND_HALFEVEN,
- \NumberFormatter::ROUND_HALFUP,
- \NumberFormatter::ROUND_UP,
- \NumberFormatter::ROUND_CEILING,
- ]);
- $resolver->setAllowedTypes('scale', 'int');
- $resolver->setAllowedTypes('symbol', ['bool', 'string']);
- $resolver->setAllowedTypes('html5', 'bool');
- }
- public function getBlockPrefix(): string
- {
- return 'percent';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/BirthdayType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class BirthdayType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'years' => range((int) date('Y') - 120, date('Y')),
- 'invalid_message' => 'Please enter a valid birthdate.',
- ]);
- $resolver->setAllowedTypes('years', 'array');
- }
- public function getParent(): ?string
- {
- return DateType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'birthday';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/DateType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
- use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\Form\ReversedTransformer;
- use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class DateType extends AbstractType
- {
- public const DEFAULT_FORMAT = \IntlDateFormatter::MEDIUM;
- public const HTML5_FORMAT = 'yyyy-MM-dd';
- private const ACCEPTED_FORMATS = [
- \IntlDateFormatter::FULL,
- \IntlDateFormatter::LONG,
- \IntlDateFormatter::MEDIUM,
- \IntlDateFormatter::SHORT,
- ];
- private const WIDGETS = [
- 'text' => TextType::class,
- 'choice' => ChoiceType::class,
- ];
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $dateFormat = \is_int($options['format']) ? $options['format'] : self::DEFAULT_FORMAT;
- $timeFormat = \IntlDateFormatter::NONE;
- $calendar = $options['calendar'] ?? \IntlDateFormatter::GREGORIAN;
- $pattern = \is_string($options['format']) ? $options['format'] : '';
- if (!\in_array($dateFormat, self::ACCEPTED_FORMATS, true)) {
- throw new InvalidOptionsException('The "format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
- }
- if ('single_text' === $options['widget']) {
- if ('' !== $pattern && !str_contains($pattern, 'y') && !str_contains($pattern, 'M') && !str_contains($pattern, 'd')) {
- throw new InvalidOptionsException(\sprintf('The "format" option should contain the letters "y", "M" or "d". Its current value is "%s".', $pattern));
- }
- $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
- $options['model_timezone'],
- $options['view_timezone'],
- $dateFormat,
- $timeFormat,
- $calendar,
- $pattern
- ));
- } else {
- if ('' !== $pattern && (!str_contains($pattern, 'y') || !str_contains($pattern, 'M') || !str_contains($pattern, 'd'))) {
- throw new InvalidOptionsException(\sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".', $pattern));
- }
- $yearOptions = $monthOptions = $dayOptions = [
- 'error_bubbling' => true,
- 'empty_data' => '',
- ];
- // when the form is compound the entries of the array are ignored in favor of children data
- // so we need to handle the cascade setting here
- $emptyData = $builder->getEmptyData() ?: [];
- if ($emptyData instanceof \Closure) {
- $lazyEmptyData = static fn ($option) => static function (FormInterface $form) use ($emptyData, $option) {
- $emptyData = $emptyData($form->getParent());
- return $emptyData[$option] ?? '';
- };
- $yearOptions['empty_data'] = $lazyEmptyData('year');
- $monthOptions['empty_data'] = $lazyEmptyData('month');
- $dayOptions['empty_data'] = $lazyEmptyData('day');
- } else {
- if (isset($emptyData['year'])) {
- $yearOptions['empty_data'] = $emptyData['year'];
- }
- if (isset($emptyData['month'])) {
- $monthOptions['empty_data'] = $emptyData['month'];
- }
- if (isset($emptyData['day'])) {
- $dayOptions['empty_data'] = $emptyData['day'];
- }
- }
- if (isset($options['invalid_message'])) {
- $dayOptions['invalid_message'] = $options['invalid_message'];
- $monthOptions['invalid_message'] = $options['invalid_message'];
- $yearOptions['invalid_message'] = $options['invalid_message'];
- }
- if (isset($options['invalid_message_parameters'])) {
- $dayOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
- $monthOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
- $yearOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
- }
- $formatter = new \IntlDateFormatter(
- \Locale::getDefault(),
- $dateFormat,
- $timeFormat,
- null,
- $calendar,
- $pattern
- );
- $formatter->setLenient(false);
- if ('choice' === $options['widget']) {
- // Only pass a subset of the options to children
- $yearOptions['choices'] = $this->formatTimestamps($formatter, '/y+/', $this->listYears($options['years']));
- $yearOptions['placeholder'] = $options['placeholder']['year'];
- $yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year'];
- $monthOptions['choices'] = $this->formatTimestamps($formatter, '/[M|L]+/', $this->listMonths($options['months']));
- $monthOptions['placeholder'] = $options['placeholder']['month'];
- $monthOptions['choice_translation_domain'] = $options['choice_translation_domain']['month'];
- $dayOptions['choices'] = $this->formatTimestamps($formatter, '/d+/', $this->listDays($options['days']));
- $dayOptions['placeholder'] = $options['placeholder']['day'];
- $dayOptions['choice_translation_domain'] = $options['choice_translation_domain']['day'];
- }
- // Append generic carry-along options
- foreach (['required', 'translation_domain'] as $passOpt) {
- $yearOptions[$passOpt] = $monthOptions[$passOpt] = $dayOptions[$passOpt] = $options[$passOpt];
- }
- $builder
- ->add('year', self::WIDGETS[$options['widget']], $yearOptions)
- ->add('month', self::WIDGETS[$options['widget']], $monthOptions)
- ->add('day', self::WIDGETS[$options['widget']], $dayOptions)
- ->addViewTransformer(new DateTimeToArrayTransformer(
- $options['model_timezone'], $options['view_timezone'], ['year', 'month', 'day']
- ))
- ->setAttribute('formatter', $formatter)
- ;
- }
- if ('datetime_immutable' === $options['input']) {
- $builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
- } elseif ('string' === $options['input']) {
- $builder->addModelTransformer(new ReversedTransformer(
- new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format'])
- ));
- } elseif ('timestamp' === $options['input']) {
- $builder->addModelTransformer(new ReversedTransformer(
- new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
- ));
- } elseif ('array' === $options['input']) {
- $builder->addModelTransformer(new ReversedTransformer(
- new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], ['year', 'month', 'day'])
- ));
- }
- if (\in_array($options['input'], ['datetime', 'datetime_immutable'], true) && null !== $options['model_timezone']) {
- $builder->addEventListener(FormEvents::POST_SET_DATA, static function (FormEvent $event) use ($options): void {
- $date = $event->getData();
- if (!$date instanceof \DateTimeInterface) {
- return;
- }
- if ($date->getTimezone()->getName() !== $options['model_timezone']) {
- 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']));
- }
- });
- }
- }
- public function finishView(FormView $view, FormInterface $form, array $options): void
- {
- $view->vars['widget'] = $options['widget'];
- // Change the input to an HTML5 date input if
- // * the widget is set to "single_text"
- // * the format matches the one expected by HTML5
- // * the html5 is set to true
- if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
- $view->vars['type'] = 'date';
- }
- if ($form->getConfig()->hasAttribute('formatter')) {
- $pattern = $form->getConfig()->getAttribute('formatter')->getPattern();
- // remove special characters unless the format was explicitly specified
- if (!\is_string($options['format'])) {
- // remove quoted strings first
- $pattern = preg_replace('/\'[^\']+\'/', '', $pattern);
- // remove remaining special chars
- $pattern = preg_replace('/[^yMd]+/', '', $pattern);
- }
- // set right order with respect to locale (e.g.: de_DE=dd.MM.yy; en_US=M/d/yy)
- // lookup various formats at http://userguide.icu-project.org/formatparse/datetime
- if (preg_match('/^([yMd]+)[^yMd]*([yMd]+)[^yMd]*([yMd]+)$/', $pattern)) {
- $pattern = preg_replace(['/y+/', '/M+/', '/d+/'], ['{{ year }}', '{{ month }}', '{{ day }}'], $pattern);
- } else {
- // default fallback
- $pattern = '{{ year }}{{ month }}{{ day }}';
- }
- $view->vars['date_pattern'] = $pattern;
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $compound = static fn (Options $options) => 'single_text' !== $options['widget'];
- $placeholderDefault = static fn (Options $options) => $options['required'] ? null : '';
- $placeholderNormalizer = static function (Options $options, $placeholder) use ($placeholderDefault) {
- if (\is_array($placeholder)) {
- $default = $placeholderDefault($options);
- return array_merge(
- ['year' => $default, 'month' => $default, 'day' => $default],
- $placeholder
- );
- }
- return [
- 'year' => $placeholder,
- 'month' => $placeholder,
- 'day' => $placeholder,
- ];
- };
- $choiceTranslationDomainNormalizer = static function (Options $options, $choiceTranslationDomain) {
- if (\is_array($choiceTranslationDomain)) {
- return array_replace(
- ['year' => false, 'month' => false, 'day' => false],
- $choiceTranslationDomain
- );
- }
- return [
- 'year' => $choiceTranslationDomain,
- 'month' => $choiceTranslationDomain,
- 'day' => $choiceTranslationDomain,
- ];
- };
- $format = static fn (Options $options) => 'single_text' === $options['widget'] ? self::HTML5_FORMAT : self::DEFAULT_FORMAT;
- $resolver->setDefaults([
- 'years' => range((int) date('Y') - 5, (int) date('Y') + 5),
- 'months' => range(1, 12),
- 'days' => range(1, 31),
- 'widget' => 'single_text',
- 'input' => 'datetime',
- 'format' => $format,
- 'model_timezone' => null,
- 'view_timezone' => null,
- 'calendar' => null,
- 'placeholder' => $placeholderDefault,
- 'html5' => true,
- // Don't modify \DateTime classes by reference, we treat
- // them like immutable value objects
- 'by_reference' => false,
- 'error_bubbling' => false,
- // If initialized with a \DateTime object, FormType initializes
- // this option to "\DateTime". Since the internal, normalized
- // representation is not \DateTime, but an array, we need to unset
- // this option.
- 'data_class' => null,
- 'compound' => $compound,
- 'empty_data' => static fn (Options $options) => $options['compound'] ? [] : '',
- 'choice_translation_domain' => false,
- 'input_format' => 'Y-m-d',
- 'invalid_message' => 'Please enter a valid date.',
- ]);
- $resolver->setNormalizer('placeholder', $placeholderNormalizer);
- $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
- $resolver->setAllowedValues('input', [
- 'datetime',
- 'datetime_immutable',
- 'string',
- 'timestamp',
- 'array',
- ]);
- $resolver->setAllowedValues('widget', [
- 'single_text',
- 'text',
- 'choice',
- ]);
- $resolver->setAllowedTypes('format', ['int', 'string']);
- $resolver->setAllowedTypes('years', 'array');
- $resolver->setAllowedTypes('months', 'array');
- $resolver->setAllowedTypes('days', 'array');
- $resolver->setAllowedTypes('input_format', 'string');
- $resolver->setAllowedTypes('calendar', ['null', 'int', \IntlCalendar::class]);
- $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.');
- $resolver->setNormalizer('html5', static function (Options $options, $html5) {
- if ($html5 && 'single_text' === $options['widget'] && self::HTML5_FORMAT !== $options['format']) {
- throw new LogicException(\sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class));
- }
- return $html5;
- });
- }
- public function getBlockPrefix(): string
- {
- return 'date';
- }
- private function formatTimestamps(\IntlDateFormatter $formatter, string $regex, array $timestamps): array
- {
- $pattern = $formatter->getPattern();
- $timezone = $formatter->getTimeZoneId();
- $formattedTimestamps = [];
- $formatter->setTimeZone('UTC');
- if (preg_match($regex, $pattern, $matches)) {
- $formatter->setPattern($matches[0]);
- foreach ($timestamps as $timestamp => $choice) {
- $formattedTimestamps[$formatter->format($timestamp)] = $choice;
- }
- // I'd like to clone the formatter above, but then we get a
- // segmentation fault, so let's restore the old state instead
- $formatter->setPattern($pattern);
- }
- $formatter->setTimeZone($timezone);
- return $formattedTimestamps;
- }
- private function listYears(array $years): array
- {
- $result = [];
- foreach ($years as $year) {
- $result[\PHP_INT_SIZE === 4 ? \DateTimeImmutable::createFromFormat('Y e', $year.' UTC')->format('U') : gmmktime(0, 0, 0, 6, 15, $year)] = $year;
- }
- return $result;
- }
- private function listMonths(array $months): array
- {
- $result = [];
- foreach ($months as $month) {
- $result[gmmktime(0, 0, 0, $month, 15)] = $month;
- }
- return $result;
- }
- private function listDays(array $days): array
- {
- $result = [];
- foreach ($days as $day) {
- $result[gmmktime(0, 0, 0, 5, $day)] = $day;
- }
- return $result;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/LocaleType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\ChoiceList\ChoiceList;
- use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Intl\Intl;
- use Symfony\Component\Intl\Locales;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class LocaleType extends AbstractType
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'choice_loader' => function (Options $options) {
- if (!class_exists(Intl::class)) {
- throw new LogicException(\sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
- }
- $choiceTranslationLocale = $options['choice_translation_locale'];
- return ChoiceList::loader($this, new IntlCallbackChoiceLoader(static fn () => array_flip(Locales::getNames($choiceTranslationLocale))), $choiceTranslationLocale);
- },
- 'choice_translation_domain' => false,
- 'choice_translation_locale' => null,
- 'invalid_message' => 'Please select a valid locale.',
- ]);
- $resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
- }
- public function getParent(): ?string
- {
- return ChoiceType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'locale';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/CheckboxType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Extension\Core\DataTransformer\BooleanToStringTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class CheckboxType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- // Unlike in other types, where the data is NULL by default, it
- // needs to be a Boolean here. setData(null) is not acceptable
- // for checkboxes and radio buttons (unless a custom model
- // transformer handles this case).
- // We cannot solve this case via overriding the "data" option, because
- // doing so also calls setDataLocked(true).
- $builder->setData($options['data'] ?? false);
- $builder->addViewTransformer(new BooleanToStringTransformer($options['value'], $options['false_values']));
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $view->vars = array_replace($view->vars, [
- 'value' => $options['value'],
- 'checked' => null !== $form->getViewData(),
- ]);
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $emptyData = static fn (FormInterface $form, $viewData) => $viewData;
- $resolver->setDefaults([
- 'value' => '1',
- 'empty_data' => $emptyData,
- 'compound' => false,
- 'false_values' => [null],
- 'invalid_message' => 'The checkbox has an invalid value.',
- 'is_empty_callback' => static fn ($modelData): bool => false === $modelData,
- ]);
- $resolver->setAllowedTypes('false_values', 'array');
- }
- public function getBlockPrefix(): string
- {
- return 'checkbox';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/TextareaType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- class TextareaType extends AbstractType
- {
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $view->vars['pattern'] = null;
- unset($view->vars['attr']['pattern']);
- }
- public function getParent(): ?string
- {
- return TextType::class;
- }
- public function getBlockPrefix(): string
- {
- return 'textarea';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/CollectionType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormView;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- class CollectionType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $resizePrototypeOptions = null;
- if ($options['allow_add'] && $options['prototype']) {
- $resizePrototypeOptions = array_replace($options['entry_options'], $options['prototype_options']);
- $prototypeOptions = array_replace([
- 'required' => $options['required'],
- 'label' => $options['prototype_name'].'label__',
- ], $resizePrototypeOptions);
- if (null !== $options['prototype_data']) {
- $prototypeOptions['data'] = $options['prototype_data'];
- }
- $prototype = $builder->create($options['prototype_name'], $options['entry_type'], $prototypeOptions);
- $builder->setAttribute('prototype', $prototype->getForm());
- }
- $resizeListener = new ResizeFormListener(
- $options['entry_type'],
- $options['entry_options'],
- $options['allow_add'],
- $options['allow_delete'],
- $options['delete_empty'],
- $resizePrototypeOptions,
- $options['keep_as_list']
- );
- $builder->addEventSubscriber($resizeListener);
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- $view->vars = array_replace($view->vars, [
- 'allow_add' => $options['allow_add'],
- 'allow_delete' => $options['allow_delete'],
- ]);
- if ($form->getConfig()->hasAttribute('prototype')) {
- $prototype = $form->getConfig()->getAttribute('prototype');
- $view->vars['prototype'] = $prototype->setParent($form)->createView($view);
- }
- }
- public function finishView(FormView $view, FormInterface $form, array $options): void
- {
- $prefixOffset = -2;
- // check if the entry type also defines a block prefix
- /** @var FormInterface $entry */
- foreach ($form as $entry) {
- if ($entry->getConfig()->getOption('block_prefix')) {
- --$prefixOffset;
- }
- break;
- }
- foreach ($view as $entryView) {
- array_splice($entryView->vars['block_prefixes'], $prefixOffset, 0, 'collection_entry');
- }
- /** @var FormInterface $prototype */
- if ($prototype = $form->getConfig()->getAttribute('prototype')) {
- if ($view->vars['prototype']->vars['multipart']) {
- $view->vars['multipart'] = true;
- }
- if ($prefixOffset > -3 && $prototype->getConfig()->getOption('block_prefix')) {
- --$prefixOffset;
- }
- array_splice($view->vars['prototype']->vars['block_prefixes'], $prefixOffset, 0, 'collection_entry');
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $entryOptionsNormalizer = static function (Options $options, $value) {
- $value['block_name'] = 'entry';
- return $value;
- };
- $resolver->setDefaults([
- 'allow_add' => false,
- 'allow_delete' => false,
- 'prototype' => true,
- 'prototype_data' => null,
- 'prototype_name' => '__name__',
- 'entry_type' => TextType::class,
- 'entry_options' => [],
- 'prototype_options' => [],
- 'delete_empty' => false,
- 'invalid_message' => 'The collection is invalid.',
- 'keep_as_list' => false,
- ]);
- $resolver->setNormalizer('entry_options', $entryOptionsNormalizer);
- $resolver->setAllowedTypes('delete_empty', ['bool', 'callable']);
- $resolver->setAllowedTypes('prototype_options', 'array');
- $resolver->setAllowedTypes('keep_as_list', ['bool']);
- }
- public function getBlockPrefix(): string
- {
- return 'collection';
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/Type/UuidType.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\Type;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\Extension\Core\DataTransformer\UuidToStringTransformer;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Pavel Dyakonov <[email protected]>
- */
- class UuidType extends AbstractType
- {
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $builder
- ->addViewTransformer(new UuidToStringTransformer())
- ;
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'compound' => false,
- 'invalid_message' => 'Please enter a valid UUID.',
- ]);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataMapper/RadioListMapper.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataMapper;
- use Symfony\Component\Form\DataMapperInterface;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- /**
- * Maps choices to/from radio forms.
- *
- * A {@link ChoiceListInterface} implementation is used to find the
- * corresponding string values for the choices. The radio form whose "value"
- * option corresponds to the selected value is marked as selected.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class RadioListMapper implements DataMapperInterface
- {
- public function mapDataToForms(mixed $choice, \Traversable $radios): void
- {
- if (!\is_string($choice)) {
- throw new UnexpectedTypeException($choice, 'string');
- }
- foreach ($radios as $radio) {
- $value = $radio->getConfig()->getOption('value');
- $radio->setData($choice === $value);
- }
- }
- public function mapFormsToData(\Traversable $radios, mixed &$choice): void
- {
- if (null !== $choice && !\is_string($choice)) {
- throw new UnexpectedTypeException($choice, 'null or string');
- }
- $choice = null;
- foreach ($radios as $radio) {
- if ($radio->getData()) {
- if ('placeholder' === $radio->getName()) {
- return;
- }
- $choice = $radio->getConfig()->getOption('value');
- return;
- }
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataMapper/DataMapper.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataMapper;
- use Symfony\Component\Form\DataAccessorInterface;
- use Symfony\Component\Form\DataMapperInterface;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor;
- use Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor;
- use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
- /**
- * Maps arrays/objects to/from forms using data accessors.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class DataMapper implements DataMapperInterface
- {
- private DataAccessorInterface $dataAccessor;
- public function __construct(?DataAccessorInterface $dataAccessor = null)
- {
- $this->dataAccessor = $dataAccessor ?? new ChainAccessor([
- new CallbackAccessor(),
- new PropertyPathAccessor(),
- ]);
- }
- public function mapDataToForms(mixed $data, \Traversable $forms): void
- {
- $empty = null === $data || [] === $data;
- if (!$empty && !\is_array($data) && !\is_object($data)) {
- throw new UnexpectedTypeException($data, 'object, array or empty');
- }
- foreach ($forms as $form) {
- $config = $form->getConfig();
- if (!$empty && $config->getMapped() && $this->dataAccessor->isReadable($data, $form)) {
- $form->setData($this->dataAccessor->getValue($data, $form));
- } else {
- $form->setData($config->getData());
- }
- }
- }
- public function mapFormsToData(\Traversable $forms, mixed &$data): void
- {
- if (null === $data) {
- return;
- }
- if (!\is_array($data) && !\is_object($data)) {
- throw new UnexpectedTypeException($data, 'object, array or empty');
- }
- foreach ($forms as $form) {
- $config = $form->getConfig();
- // Write-back is disabled if the form is not synchronized (transformation failed),
- // if the form was not submitted and if the form is disabled (modification not allowed)
- if ($config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled() && $this->dataAccessor->isWritable($data, $form)) {
- $this->dataAccessor->setValue($data, $form->getData(), $form);
- }
- }
- }
- /**
- * @internal
- */
- public function getDataAccessor(): DataAccessorInterface
- {
- return $this->dataAccessor;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataMapper/CheckboxListMapper.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataMapper;
- use Symfony\Component\Form\DataMapperInterface;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- /**
- * Maps choices to/from checkbox forms.
- *
- * A {@link ChoiceListInterface} implementation is used to find the
- * corresponding string values for the choices. Each checkbox form whose "value"
- * option corresponds to any of the selected values is marked as selected.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class CheckboxListMapper implements DataMapperInterface
- {
- public function mapDataToForms(mixed $choices, \Traversable $checkboxes): void
- {
- if (!\is_array($choices ??= [])) {
- throw new UnexpectedTypeException($choices, 'array');
- }
- foreach ($checkboxes as $checkbox) {
- $value = $checkbox->getConfig()->getOption('value');
- $checkbox->setData(\in_array($value, $choices, true));
- }
- }
- public function mapFormsToData(\Traversable $checkboxes, mixed &$choices): void
- {
- if (!\is_array($choices)) {
- throw new UnexpectedTypeException($choices, 'array');
- }
- $values = [];
- foreach ($checkboxes as $checkbox) {
- if ($checkbox->getData()) {
- // construct an array of choice values
- $values[] = $checkbox->getConfig()->getOption('value');
- }
- }
- $choices = $values;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataAccessor/CallbackAccessor.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataAccessor;
- use Symfony\Component\Form\DataAccessorInterface;
- use Symfony\Component\Form\Exception\AccessException;
- use Symfony\Component\Form\FormInterface;
- /**
- * Writes and reads values to/from an object or array using callback functions.
- *
- * @author Yonel Ceruto <[email protected]>
- */
- class CallbackAccessor implements DataAccessorInterface
- {
- public function getValue(object|array $data, FormInterface $form): mixed
- {
- if (null === $getter = $form->getConfig()->getOption('getter')) {
- throw new AccessException('Unable to read from the given form data as no getter is defined.');
- }
- return ($getter)($data, $form);
- }
- public function setValue(object|array &$data, mixed $value, FormInterface $form): void
- {
- if (null === $setter = $form->getConfig()->getOption('setter')) {
- throw new AccessException('Unable to write the given value as no setter is defined.');
- }
- ($setter)($data, $form->getData(), $form);
- }
- public function isReadable(object|array $data, FormInterface $form): bool
- {
- return null !== $form->getConfig()->getOption('getter');
- }
- public function isWritable(object|array $data, FormInterface $form): bool
- {
- return null !== $form->getConfig()->getOption('setter');
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataAccessor/PropertyPathAccessor.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataAccessor;
- use Symfony\Component\Form\DataAccessorInterface;
- use Symfony\Component\Form\DataMapperInterface;
- use Symfony\Component\Form\Exception\AccessException;
- use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\PropertyAccess\Exception\AccessException as PropertyAccessException;
- use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
- use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
- use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
- use Symfony\Component\PropertyAccess\PropertyAccess;
- use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
- use Symfony\Component\PropertyAccess\PropertyPathInterface;
- /**
- * Writes and reads values to/from an object or array using property path.
- *
- * @author Yonel Ceruto <[email protected]>
- * @author Bernhard Schussek <[email protected]>
- */
- class PropertyPathAccessor implements DataAccessorInterface
- {
- private PropertyAccessorInterface $propertyAccessor;
- public function __construct(?PropertyAccessorInterface $propertyAccessor = null)
- {
- $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
- }
- public function getValue(object|array $data, FormInterface $form): mixed
- {
- if (null === $propertyPath = $form->getPropertyPath()) {
- throw new AccessException('Unable to read from the given form data as no property path is defined.');
- }
- return $this->getPropertyValue($data, $propertyPath);
- }
- public function setValue(object|array &$data, mixed $value, FormInterface $form): void
- {
- if (null === $propertyPath = $form->getPropertyPath()) {
- throw new AccessException('Unable to write the given value as no property path is defined.');
- }
- $getValue = function () use ($data, $form, $propertyPath) {
- $dataMapper = $this->getDataMapper($form);
- if ($dataMapper instanceof DataMapper && null !== $dataAccessor = $dataMapper->getDataAccessor()) {
- return $dataAccessor->getValue($data, $form);
- }
- return $this->getPropertyValue($data, $propertyPath);
- };
- // If the field is of type DateTimeInterface and the data is the same skip the update to
- // keep the original object hash
- if ($value instanceof \DateTimeInterface && $value == $getValue()) {
- return;
- }
- // If the data is identical to the value in $data, we are
- // dealing with a reference
- if (!\is_object($data) || !$form->getConfig()->getByReference() || $value !== $getValue()) {
- try {
- $this->propertyAccessor->setValue($data, $propertyPath, $value);
- } catch (NoSuchPropertyException $e) {
- 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);
- }
- }
- }
- public function isReadable(object|array $data, FormInterface $form): bool
- {
- return null !== $form->getPropertyPath();
- }
- public function isWritable(object|array $data, FormInterface $form): bool
- {
- return null !== $form->getPropertyPath();
- }
- private function getPropertyValue(object|array $data, PropertyPathInterface $propertyPath): mixed
- {
- try {
- return $this->propertyAccessor->getValue($data, $propertyPath);
- } catch (PropertyAccessException $e) {
- if (\is_array($data) && $e instanceof NoSuchIndexException) {
- return null;
- }
- if (!$e instanceof UninitializedPropertyException
- // For versions without UninitializedPropertyException check the exception message
- && (class_exists(UninitializedPropertyException::class) || !str_contains($e->getMessage(), 'You should initialize it'))
- ) {
- throw $e;
- }
- return null;
- }
- }
- private function getDataMapper(FormInterface $form): ?DataMapperInterface
- {
- do {
- $dataMapper = $form->getConfig()->getDataMapper();
- } while (null === $dataMapper && null !== $form = $form->getParent());
- return $dataMapper;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataAccessor/ChainAccessor.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataAccessor;
- use Symfony\Component\Form\DataAccessorInterface;
- use Symfony\Component\Form\Exception\AccessException;
- use Symfony\Component\Form\FormInterface;
- /**
- * @author Yonel Ceruto <[email protected]>
- */
- class ChainAccessor implements DataAccessorInterface
- {
- /**
- * @param DataAccessorInterface[]|iterable $accessors
- */
- public function __construct(
- private iterable $accessors,
- ) {
- }
- public function getValue(object|array $data, FormInterface $form): mixed
- {
- foreach ($this->accessors as $accessor) {
- if ($accessor->isReadable($data, $form)) {
- return $accessor->getValue($data, $form);
- }
- }
- throw new AccessException('Unable to read from the given form data as no accessor in the chain is able to read the data.');
- }
- public function setValue(object|array &$data, mixed $value, FormInterface $form): void
- {
- foreach ($this->accessors as $accessor) {
- if ($accessor->isWritable($data, $form)) {
- $accessor->setValue($data, $value, $form);
- return;
- }
- }
- throw new AccessException('Unable to write the given value as no accessor in the chain is able to set the data.');
- }
- public function isReadable(object|array $data, FormInterface $form): bool
- {
- foreach ($this->accessors as $accessor) {
- if ($accessor->isReadable($data, $form)) {
- return true;
- }
- }
- return false;
- }
- public function isWritable(object|array $data, FormInterface $form): bool
- {
- foreach ($this->accessors as $accessor) {
- if ($accessor->isWritable($data, $form)) {
- return true;
- }
- }
- return false;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/WeekToArrayTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms between an ISO 8601 week date string and an array.
- *
- * @author Damien Fayet <[email protected]>
- *
- * @implements DataTransformerInterface<string, array{year: int|null, week: int|null}>
- */
- class WeekToArrayTransformer implements DataTransformerInterface
- {
- /**
- * Transforms a string containing an ISO 8601 week date into an array.
- *
- * @param string|null $value A week date string
- *
- * @return array{year: int|null, week: int|null}
- *
- * @throws TransformationFailedException If the given value is not a string,
- * or if the given value does not follow the right format
- */
- public function transform(mixed $value): array
- {
- if (null === $value) {
- return ['year' => null, 'week' => null];
- }
- if (!\is_string($value)) {
- throw new TransformationFailedException(\sprintf('Value is expected to be a string but was "%s".', get_debug_type($value)));
- }
- if (0 === preg_match('/^(?P<year>\d{4})-W(?P<week>\d{2})$/', $value, $matches)) {
- throw new TransformationFailedException('Given data does not follow the date format "Y-\WW".');
- }
- return [
- 'year' => (int) $matches['year'],
- 'week' => (int) $matches['week'],
- ];
- }
- /**
- * Transforms an array into a week date string.
- *
- * @param array{year: int|null, week: int|null} $value
- *
- * @return string|null A week date string following the format Y-\WW
- *
- * @throws TransformationFailedException If the given value cannot be merged in a valid week date string,
- * or if the obtained week date does not exists
- */
- public function reverseTransform(mixed $value): ?string
- {
- if (null === $value || [] === $value) {
- return null;
- }
- if (!\is_array($value)) {
- throw new TransformationFailedException(\sprintf('Value is expected to be an array, but was "%s".', get_debug_type($value)));
- }
- if (!\array_key_exists('year', $value)) {
- throw new TransformationFailedException('Key "year" is missing.');
- }
- if (!\array_key_exists('week', $value)) {
- throw new TransformationFailedException('Key "week" is missing.');
- }
- if ($additionalKeys = array_diff(array_keys($value), ['year', 'week'])) {
- throw new TransformationFailedException(\sprintf('Expected only keys "year" and "week" to be present, but also got ["%s"].', implode('", "', $additionalKeys)));
- }
- if (null === $value['year'] && null === $value['week']) {
- return null;
- }
- if (!\is_int($value['year'])) {
- throw new TransformationFailedException(\sprintf('Year is expected to be an integer, but was "%s".', get_debug_type($value['year'])));
- }
- if (!\is_int($value['week'])) {
- throw new TransformationFailedException(\sprintf('Week is expected to be an integer, but was "%s".', get_debug_type($value['week'])));
- }
- // The 28th December is always in the last week of the year
- if (date('W', strtotime('28th December '.$value['year'])) < $value['week']) {
- throw new TransformationFailedException(\sprintf('Week "%d" does not exist for year "%d".', $value['week'], $value['year']));
- }
- return \sprintf('%d-W%02d', $value['year'], $value['week']);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/DataTransformerChain.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Passes a value through multiple value transformers.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class DataTransformerChain implements DataTransformerInterface
- {
- /**
- * Uses the given value transformers to transform values.
- *
- * @param DataTransformerInterface[] $transformers
- */
- public function __construct(
- protected array $transformers,
- ) {
- }
- /**
- * Passes the value through the transform() method of all nested transformers.
- *
- * The transformers receive the value in the same order as they were passed
- * to the constructor. Each transformer receives the result of the previous
- * transformer as input. The output of the last transformer is returned
- * by this method.
- *
- * @param mixed $value The original value
- *
- * @throws TransformationFailedException
- */
- public function transform(mixed $value): mixed
- {
- foreach ($this->transformers as $transformer) {
- $value = $transformer->transform($value);
- }
- return $value;
- }
- /**
- * Passes the value through the reverseTransform() method of all nested
- * transformers.
- *
- * The transformers receive the value in the reverse order as they were passed
- * to the constructor. Each transformer receives the result of the previous
- * transformer as input. The output of the last transformer is returned
- * by this method.
- *
- * @param mixed $value The transformed value
- *
- * @throws TransformationFailedException
- */
- public function reverseTransform(mixed $value): mixed
- {
- for ($i = \count($this->transformers) - 1; $i >= 0; --$i) {
- $value = $this->transformers[$i]->reverseTransform($value);
- }
- return $value;
- }
- /**
- * @return DataTransformerInterface[]
- */
- public function getTransformers(): array
- {
- return $this->transformers;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/BaseDateTimeTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- /**
- * @template TTransformedValue
- *
- * @implements DataTransformerInterface<\DateTimeInterface, TTransformedValue>
- */
- abstract class BaseDateTimeTransformer implements DataTransformerInterface
- {
- protected static array $formats = [
- \IntlDateFormatter::NONE,
- \IntlDateFormatter::FULL,
- \IntlDateFormatter::LONG,
- \IntlDateFormatter::MEDIUM,
- \IntlDateFormatter::SHORT,
- ];
- protected string $inputTimezone;
- protected string $outputTimezone;
- /**
- * @param string|null $inputTimezone The name of the input timezone
- * @param string|null $outputTimezone The name of the output timezone
- *
- * @throws InvalidArgumentException if a timezone is not valid
- */
- public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null)
- {
- $this->inputTimezone = $inputTimezone ?: date_default_timezone_get();
- $this->outputTimezone = $outputTimezone ?: date_default_timezone_get();
- // Check if input and output timezones are valid
- try {
- new \DateTimeZone($this->inputTimezone);
- } catch (\Exception $e) {
- throw new InvalidArgumentException(\sprintf('Input timezone is invalid: "%s".', $this->inputTimezone), $e->getCode(), $e);
- }
- try {
- new \DateTimeZone($this->outputTimezone);
- } catch (\Exception $e) {
- throw new InvalidArgumentException(\sprintf('Output timezone is invalid: "%s".', $this->outputTimezone), $e->getCode(), $e);
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/DateTimeToRfc3339Transformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * @author Bernhard Schussek <[email protected]>
- *
- * @extends BaseDateTimeTransformer<string>
- */
- class DateTimeToRfc3339Transformer extends BaseDateTimeTransformer
- {
- /**
- * Transforms a normalized date into a localized date.
- *
- * @param \DateTimeInterface $dateTime A DateTimeInterface object
- *
- * @throws TransformationFailedException If the given value is not a \DateTimeInterface
- */
- public function transform(mixed $dateTime): string
- {
- if (null === $dateTime) {
- return '';
- }
- if (!$dateTime instanceof \DateTimeInterface) {
- throw new TransformationFailedException('Expected a \DateTimeInterface.');
- }
- if ($this->inputTimezone !== $this->outputTimezone) {
- $dateTime = \DateTimeImmutable::createFromInterface($dateTime);
- $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
- }
- return preg_replace('/\+00:00$/', 'Z', $dateTime->format('c'));
- }
- /**
- * Transforms a formatted string following RFC 3339 into a normalized date.
- *
- * @param string $rfc3339 Formatted string
- *
- * @throws TransformationFailedException If the given value is not a string,
- * if the value could not be transformed
- */
- public function reverseTransform(mixed $rfc3339): ?\DateTime
- {
- if (!\is_string($rfc3339)) {
- throw new TransformationFailedException('Expected a string.');
- }
- if ('' === $rfc3339) {
- return null;
- }
- if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|(?:(?:\+|-)\d{2}:\d{2}))$/', $rfc3339, $matches)) {
- throw new TransformationFailedException(\sprintf('The date "%s" is not a valid date.', $rfc3339));
- }
- try {
- $dateTime = new \DateTime($rfc3339);
- } catch (\Exception $e) {
- throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
- }
- if ($this->inputTimezone !== $dateTime->getTimezone()->getName()) {
- $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
- }
- if (!checkdate($matches[2], $matches[3], $matches[1])) {
- throw new TransformationFailedException(\sprintf('The date "%s-%s-%s" is not a valid date.', $matches[1], $matches[2], $matches[3]));
- }
- return $dateTime;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- /**
- * Transforms between a normalized date interval and an interval string/array.
- *
- * @author Steffen Roßkamp <[email protected]>
- *
- * @implements DataTransformerInterface<\DateInterval, array>
- */
- class DateIntervalToArrayTransformer implements DataTransformerInterface
- {
- public const YEARS = 'years';
- public const MONTHS = 'months';
- public const DAYS = 'days';
- public const HOURS = 'hours';
- public const MINUTES = 'minutes';
- public const SECONDS = 'seconds';
- public const INVERT = 'invert';
- private const AVAILABLE_FIELDS = [
- self::YEARS => 'y',
- self::MONTHS => 'm',
- self::DAYS => 'd',
- self::HOURS => 'h',
- self::MINUTES => 'i',
- self::SECONDS => 's',
- self::INVERT => 'r',
- ];
- private array $fields;
- /**
- * @param string[]|null $fields The date fields
- * @param bool $pad Whether to use padding
- */
- public function __construct(
- ?array $fields = null,
- private bool $pad = false,
- ) {
- $this->fields = $fields ?? ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'invert'];
- }
- /**
- * Transforms a normalized date interval into an interval array.
- *
- * @param \DateInterval $dateInterval Normalized date interval
- *
- * @throws UnexpectedTypeException if the given value is not a \DateInterval instance
- */
- public function transform(mixed $dateInterval): array
- {
- if (null === $dateInterval) {
- return array_intersect_key(
- [
- 'years' => '',
- 'months' => '',
- 'weeks' => '',
- 'days' => '',
- 'hours' => '',
- 'minutes' => '',
- 'seconds' => '',
- 'invert' => false,
- ],
- array_flip($this->fields)
- );
- }
- if (!$dateInterval instanceof \DateInterval) {
- throw new UnexpectedTypeException($dateInterval, \DateInterval::class);
- }
- $result = [];
- foreach (self::AVAILABLE_FIELDS as $field => $char) {
- $result[$field] = $dateInterval->format('%'.($this->pad ? strtoupper($char) : $char));
- }
- if (\in_array('weeks', $this->fields, true)) {
- $result['weeks'] = '0';
- if (isset($result['days']) && (int) $result['days'] >= 7) {
- $result['weeks'] = (string) floor($result['days'] / 7);
- $result['days'] = (string) ($result['days'] % 7);
- }
- }
- $result['invert'] = '-' === $result['invert'];
- return array_intersect_key($result, array_flip($this->fields));
- }
- /**
- * Transforms an interval array into a normalized date interval.
- *
- * @param array $value Interval array
- *
- * @throws UnexpectedTypeException if the given value is not an array
- * @throws TransformationFailedException if the value could not be transformed
- */
- public function reverseTransform(mixed $value): ?\DateInterval
- {
- if (null === $value) {
- return null;
- }
- if (!\is_array($value)) {
- throw new UnexpectedTypeException($value, 'array');
- }
- if ('' === implode('', $value)) {
- return null;
- }
- $emptyFields = [];
- foreach ($this->fields as $field) {
- if (!isset($value[$field])) {
- $emptyFields[] = $field;
- }
- }
- if (\count($emptyFields) > 0) {
- throw new TransformationFailedException(\sprintf('The fields "%s" should not be empty.', implode('", "', $emptyFields)));
- }
- if (isset($value['invert']) && !\is_bool($value['invert'])) {
- throw new TransformationFailedException('The value of "invert" must be boolean.');
- }
- foreach (self::AVAILABLE_FIELDS as $field => $char) {
- if ('invert' !== $field && isset($value[$field]) && !ctype_digit((string) $value[$field])) {
- throw new TransformationFailedException(\sprintf('This amount of "%s" is invalid.', $field));
- }
- }
- try {
- if (!empty($value['weeks'])) {
- $interval = \sprintf(
- 'P%sY%sM%sWT%sH%sM%sS',
- empty($value['years']) ? '0' : $value['years'],
- empty($value['months']) ? '0' : $value['months'],
- $value['weeks'],
- empty($value['hours']) ? '0' : $value['hours'],
- empty($value['minutes']) ? '0' : $value['minutes'],
- empty($value['seconds']) ? '0' : $value['seconds']
- );
- } else {
- $interval = \sprintf(
- 'P%sY%sM%sDT%sH%sM%sS',
- empty($value['years']) ? '0' : $value['years'],
- empty($value['months']) ? '0' : $value['months'],
- empty($value['days']) ? '0' : $value['days'],
- empty($value['hours']) ? '0' : $value['hours'],
- empty($value['minutes']) ? '0' : $value['minutes'],
- empty($value['seconds']) ? '0' : $value['seconds']
- );
- }
- $dateInterval = new \DateInterval($interval);
- if (isset($value['invert'])) {
- $dateInterval->invert = $value['invert'] ? 1 : 0;
- }
- } catch (\Exception $e) {
- throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
- }
- return $dateInterval;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/DateTimeToTimestampTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms between a timestamp and a DateTime object.
- *
- * @author Bernhard Schussek <[email protected]>
- * @author Florian Eckerstorfer <[email protected]>
- *
- * @extends BaseDateTimeTransformer<int|numeric-string>
- */
- class DateTimeToTimestampTransformer extends BaseDateTimeTransformer
- {
- /**
- * Transforms a DateTime object into a timestamp in the configured timezone.
- *
- * @param \DateTimeInterface $dateTime A DateTimeInterface object
- *
- * @throws TransformationFailedException If the given value is not a \DateTimeInterface
- */
- public function transform(mixed $dateTime): ?int
- {
- if (null === $dateTime) {
- return null;
- }
- if (!$dateTime instanceof \DateTimeInterface) {
- throw new TransformationFailedException('Expected a \DateTimeInterface.');
- }
- return $dateTime->getTimestamp();
- }
- /**
- * Transforms a timestamp in the configured timezone into a DateTime object.
- *
- * @param string $value A timestamp
- *
- * @throws TransformationFailedException If the given value is not a timestamp
- * or if the given timestamp is invalid
- */
- public function reverseTransform(mixed $value): ?\DateTime
- {
- if (null === $value) {
- return null;
- }
- if (!is_numeric($value)) {
- throw new TransformationFailedException('Expected a numeric.');
- }
- try {
- $dateTime = new \DateTime();
- $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
- $dateTime->setTimestamp($value);
- if ($this->inputTimezone !== $this->outputTimezone) {
- $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
- }
- } catch (\Exception $e) {
- throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
- }
- return $dateTime;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/DateTimeToArrayTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms between a normalized time and a localized time string/array.
- *
- * @author Bernhard Schussek <[email protected]>
- * @author Florian Eckerstorfer <[email protected]>
- *
- * @extends BaseDateTimeTransformer<array>
- */
- class DateTimeToArrayTransformer extends BaseDateTimeTransformer
- {
- private array $fields;
- private \DateTimeInterface $referenceDate;
- /**
- * @param string|null $inputTimezone The input timezone
- * @param string|null $outputTimezone The output timezone
- * @param string[]|null $fields The date fields
- * @param bool $pad Whether to use padding
- */
- public function __construct(
- ?string $inputTimezone = null,
- ?string $outputTimezone = null,
- ?array $fields = null,
- private bool $pad = false,
- ?\DateTimeInterface $referenceDate = null,
- ) {
- parent::__construct($inputTimezone, $outputTimezone);
- $this->fields = $fields ?? ['year', 'month', 'day', 'hour', 'minute', 'second'];
- $this->referenceDate = $referenceDate ?? new \DateTimeImmutable('1970-01-01 00:00:00');
- }
- /**
- * Transforms a normalized date into a localized date.
- *
- * @param \DateTimeInterface $dateTime A DateTimeInterface object
- *
- * @throws TransformationFailedException If the given value is not a \DateTimeInterface
- */
- public function transform(mixed $dateTime): array
- {
- if (null === $dateTime) {
- return array_intersect_key([
- 'year' => '',
- 'month' => '',
- 'day' => '',
- 'hour' => '',
- 'minute' => '',
- 'second' => '',
- ], array_flip($this->fields));
- }
- if (!$dateTime instanceof \DateTimeInterface) {
- throw new TransformationFailedException('Expected a \DateTimeInterface.');
- }
- if ($this->inputTimezone !== $this->outputTimezone) {
- $dateTime = \DateTimeImmutable::createFromInterface($dateTime);
- $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
- }
- $result = array_intersect_key([
- 'year' => $dateTime->format('Y'),
- 'month' => $dateTime->format('m'),
- 'day' => $dateTime->format('d'),
- 'hour' => $dateTime->format('H'),
- 'minute' => $dateTime->format('i'),
- 'second' => $dateTime->format('s'),
- ], array_flip($this->fields));
- if (!$this->pad) {
- foreach ($result as &$entry) {
- // remove leading zeros
- $entry = (string) (int) $entry;
- }
- // unset reference to keep scope clear
- unset($entry);
- }
- return $result;
- }
- /**
- * Transforms a localized date into a normalized date.
- *
- * @param array $value Localized date
- *
- * @throws TransformationFailedException If the given value is not an array,
- * if the value could not be transformed
- */
- public function reverseTransform(mixed $value): ?\DateTime
- {
- if (null === $value) {
- return null;
- }
- if (!\is_array($value)) {
- throw new TransformationFailedException('Expected an array.');
- }
- if ('' === implode('', $value)) {
- return null;
- }
- $emptyFields = [];
- foreach ($this->fields as $field) {
- if (!isset($value[$field])) {
- $emptyFields[] = $field;
- }
- }
- if (\count($emptyFields) > 0) {
- throw new TransformationFailedException(\sprintf('The fields "%s" should not be empty.', implode('", "', $emptyFields)));
- }
- if (isset($value['month']) && !ctype_digit((string) $value['month'])) {
- throw new TransformationFailedException('This month is invalid.');
- }
- if (isset($value['day']) && !ctype_digit((string) $value['day'])) {
- throw new TransformationFailedException('This day is invalid.');
- }
- if (isset($value['year']) && !ctype_digit((string) $value['year'])) {
- throw new TransformationFailedException('This year is invalid.');
- }
- if (!empty($value['month']) && !empty($value['day']) && !empty($value['year']) && false === checkdate($value['month'], $value['day'], $value['year'])) {
- throw new TransformationFailedException('This is an invalid date.');
- }
- if (isset($value['hour']) && !ctype_digit((string) $value['hour'])) {
- throw new TransformationFailedException('This hour is invalid.');
- }
- if (isset($value['minute']) && !ctype_digit((string) $value['minute'])) {
- throw new TransformationFailedException('This minute is invalid.');
- }
- if (isset($value['second']) && !ctype_digit((string) $value['second'])) {
- throw new TransformationFailedException('This second is invalid.');
- }
- try {
- $dateTime = new \DateTime(\sprintf(
- '%s-%s-%s %s:%s:%s',
- empty($value['year']) ? $this->referenceDate->format('Y') : $value['year'],
- empty($value['month']) ? $this->referenceDate->format('m') : $value['month'],
- empty($value['day']) ? $this->referenceDate->format('d') : $value['day'],
- $value['hour'] ?? $this->referenceDate->format('H'),
- $value['minute'] ?? $this->referenceDate->format('i'),
- $value['second'] ?? $this->referenceDate->format('s')
- ),
- new \DateTimeZone($this->outputTimezone)
- );
- if ($this->inputTimezone !== $this->outputTimezone) {
- $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
- }
- } catch (\Exception $e) {
- throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
- }
- return $dateTime;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/ValueToDuplicatesTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * @author Bernhard Schussek <[email protected]>
- *
- * @implements DataTransformerInterface<mixed, array>
- */
- class ValueToDuplicatesTransformer implements DataTransformerInterface
- {
- public function __construct(
- private array $keys,
- ) {
- }
- /**
- * Duplicates the given value through the array.
- */
- public function transform(mixed $value): array
- {
- $result = [];
- foreach ($this->keys as $key) {
- $result[$key] = $value;
- }
- return $result;
- }
- /**
- * Extracts the duplicated value from an array.
- *
- * @throws TransformationFailedException if the given value is not an array or
- * if the given array cannot be transformed
- */
- public function reverseTransform(mixed $array): mixed
- {
- if (!\is_array($array)) {
- throw new TransformationFailedException('Expected an array.');
- }
- $result = current($array);
- $emptyKeys = [];
- foreach ($this->keys as $key) {
- if (isset($array[$key]) && false !== $array[$key] && [] !== $array[$key]) {
- if ($array[$key] !== $result) {
- throw new TransformationFailedException('All values in the array should be the same.');
- }
- } else {
- $emptyKeys[] = $key;
- }
- }
- if (\count($emptyKeys) > 0) {
- if (\count($emptyKeys) == \count($this->keys)) {
- // All keys empty
- return null;
- }
- throw new TransformationFailedException(\sprintf('The keys "%s" should not be empty.', implode('", "', $emptyKeys)));
- }
- return $result;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms between an integer and a localized number with grouping
- * (each thousand) and comma separators.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class IntegerToLocalizedStringTransformer extends NumberToLocalizedStringTransformer
- {
- /**
- * Constructs a transformer.
- *
- * @param bool $grouping Whether thousands should be grouped
- * @param int|null $roundingMode One of the ROUND_ constants in this class
- * @param string|null $locale locale used for transforming
- */
- public function __construct(?bool $grouping = false, ?int $roundingMode = \NumberFormatter::ROUND_DOWN, ?string $locale = null)
- {
- parent::__construct(0, $grouping, $roundingMode, $locale);
- }
- public function reverseTransform(mixed $value): int|float|null
- {
- $decimalSeparator = $this->getNumberFormatter()->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
- if (\is_string($value) && str_contains($value, $decimalSeparator)) {
- throw new TransformationFailedException(\sprintf('The value "%s" is not a valid integer.', $value));
- }
- $result = parent::reverseTransform($value);
- return null !== $result ? (int) $result : null;
- }
- /**
- * @internal
- */
- protected function castParsedValue(int|float $value): int|float
- {
- return $value;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/BooleanToStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms between a Boolean and a string.
- *
- * @author Bernhard Schussek <[email protected]>
- * @author Florian Eckerstorfer <[email protected]>
- *
- * @implements DataTransformerInterface<bool, string>
- */
- class BooleanToStringTransformer implements DataTransformerInterface
- {
- /**
- * @param string $trueValue The value emitted upon transform if the input is true
- */
- public function __construct(
- private string $trueValue,
- private array $falseValues = [null],
- ) {
- if (\in_array($this->trueValue, $this->falseValues, true)) {
- throw new InvalidArgumentException('The specified "true" value is contained in the false-values.');
- }
- }
- /**
- * Transforms a Boolean into a string.
- *
- * @param bool $value Boolean value
- *
- * @throws TransformationFailedException if the given value is not a Boolean
- */
- public function transform(mixed $value): ?string
- {
- if (null === $value) {
- return null;
- }
- if (!\is_bool($value)) {
- throw new TransformationFailedException('Expected a Boolean.');
- }
- return $value ? $this->trueValue : null;
- }
- /**
- * Transforms a string into a Boolean.
- *
- * @param string $value String value
- *
- * @throws TransformationFailedException if the given value is not a string
- */
- public function reverseTransform(mixed $value): bool
- {
- if (\in_array($value, $this->falseValues, true)) {
- return false;
- }
- if (!\is_string($value)) {
- throw new TransformationFailedException('Expected a string.');
- }
- return true;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/ChoicesToValuesTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * @author Bernhard Schussek <[email protected]>
- *
- * @implements DataTransformerInterface<array, array>
- */
- class ChoicesToValuesTransformer implements DataTransformerInterface
- {
- public function __construct(
- private ChoiceListInterface $choiceList,
- ) {
- }
- /**
- * @throws TransformationFailedException if the given value is not an array
- */
- public function transform(mixed $array): array
- {
- if (null === $array) {
- return [];
- }
- if (!\is_array($array)) {
- throw new TransformationFailedException('Expected an array.');
- }
- return $this->choiceList->getValuesForChoices($array);
- }
- /**
- * @throws TransformationFailedException if the given value is not an array
- * or if no matching choice could be
- * found for some given value
- */
- public function reverseTransform(mixed $array): array
- {
- if (null === $array) {
- return [];
- }
- if (!\is_array($array)) {
- throw new TransformationFailedException('Expected an array.');
- }
- $choices = $this->choiceList->getChoicesForValues($array);
- if (\count($choices) !== \count($array)) {
- throw new TransformationFailedException('Could not find all matching choices for the given values.');
- }
- return $choices;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * @author Franz Wilding <[email protected]>
- * @author Bernhard Schussek <[email protected]>
- * @author Fred Cox <[email protected]>
- *
- * @extends BaseDateTimeTransformer<string>
- */
- class DateTimeToHtml5LocalDateTimeTransformer extends BaseDateTimeTransformer
- {
- public const HTML5_FORMAT = 'Y-m-d\\TH:i:s';
- public const HTML5_FORMAT_NO_SECONDS = 'Y-m-d\\TH:i';
- public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, private bool $withSeconds = false)
- {
- parent::__construct($inputTimezone, $outputTimezone);
- }
- /**
- * Transforms a \DateTime into a local date and time string.
- *
- * According to the HTML standard, the input string of a datetime-local
- * input is an RFC3339 date followed by 'T', followed by an RFC3339 time.
- * https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-local-date-and-time-string
- *
- * @param \DateTimeInterface $dateTime
- *
- * @throws TransformationFailedException If the given value is not an
- * instance of \DateTime or \DateTimeInterface
- */
- public function transform(mixed $dateTime): string
- {
- if (null === $dateTime) {
- return '';
- }
- if (!$dateTime instanceof \DateTimeInterface) {
- throw new TransformationFailedException('Expected a \DateTimeInterface.');
- }
- if ($this->inputTimezone !== $this->outputTimezone) {
- $dateTime = \DateTimeImmutable::createFromInterface($dateTime);
- $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
- }
- return $dateTime->format($this->withSeconds ? self::HTML5_FORMAT : self::HTML5_FORMAT_NO_SECONDS);
- }
- /**
- * Transforms a local date and time string into a \DateTime.
- *
- * When transforming back to DateTime the regex is slightly laxer, taking into
- * account rules for parsing a local date and time string
- * https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-local-date-and-time-string
- *
- * @param string $dateTimeLocal Formatted string
- *
- * @throws TransformationFailedException If the given value is not a string,
- * if the value could not be transformed
- */
- public function reverseTransform(mixed $dateTimeLocal): ?\DateTime
- {
- if (!\is_string($dateTimeLocal)) {
- throw new TransformationFailedException('Expected a string.');
- }
- if ('' === $dateTimeLocal) {
- return null;
- }
- // to maintain backwards compatibility we do not strictly validate the submitted date
- // see https://github.com/symfony/symfony/issues/28699
- if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})[T ]\d{2}:\d{2}(?::\d{2})?/', $dateTimeLocal, $matches)) {
- throw new TransformationFailedException(\sprintf('The date "%s" is not a valid date.', $dateTimeLocal));
- }
- try {
- $dateTime = new \DateTime($dateTimeLocal, new \DateTimeZone($this->outputTimezone));
- } catch (\Exception $e) {
- throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
- }
- if ($this->inputTimezone !== $dateTime->getTimezone()->getName()) {
- $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
- }
- if (!checkdate($matches[2], $matches[3], $matches[1])) {
- throw new TransformationFailedException(\sprintf('The date "%s-%s-%s" is not a valid date.', $matches[1], $matches[2], $matches[3]));
- }
- return $dateTime;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/UlidToStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- use Symfony\Component\Uid\Ulid;
- /**
- * Transforms between a ULID string and a Ulid object.
- *
- * @author Pavel Dyakonov <[email protected]>
- *
- * @implements DataTransformerInterface<Ulid, string>
- */
- class UlidToStringTransformer implements DataTransformerInterface
- {
- /**
- * Transforms a Ulid object into a string.
- *
- * @param Ulid $value A Ulid object
- *
- * @throws TransformationFailedException If the given value is not a Ulid object
- */
- public function transform(mixed $value): ?string
- {
- if (null === $value) {
- return null;
- }
- if (!$value instanceof Ulid) {
- throw new TransformationFailedException('Expected a Ulid.');
- }
- return (string) $value;
- }
- /**
- * Transforms a ULID string into a Ulid object.
- *
- * @param string $value A ULID string
- *
- * @throws TransformationFailedException If the given value is not a string,
- * or could not be transformed
- */
- public function reverseTransform(mixed $value): ?Ulid
- {
- if (null === $value || '' === $value) {
- return null;
- }
- if (!\is_string($value)) {
- throw new TransformationFailedException('Expected a string.');
- }
- try {
- $ulid = new Ulid($value);
- } catch (\InvalidArgumentException $e) {
- throw new TransformationFailedException(\sprintf('The value "%s" is not a valid ULID.', $value), $e->getCode(), $e);
- }
- return $ulid;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/ChoiceToValueTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * @author Bernhard Schussek <[email protected]>
- *
- * @implements DataTransformerInterface<mixed, string>
- */
- class ChoiceToValueTransformer implements DataTransformerInterface
- {
- public function __construct(
- private ChoiceListInterface $choiceList,
- ) {
- }
- public function transform(mixed $choice): mixed
- {
- return (string) current($this->choiceList->getValuesForChoices([$choice]));
- }
- public function reverseTransform(mixed $value): mixed
- {
- if (null !== $value && !\is_string($value)) {
- throw new TransformationFailedException('Expected a string or null.');
- }
- $choices = $this->choiceList->getChoicesForValues([(string) $value]);
- if (1 !== \count($choices)) {
- if (null === $value || '' === $value) {
- return null;
- }
- throw new TransformationFailedException(\sprintf('The choice "%s" does not exist or is not unique.', $value));
- }
- return current($choices);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/DateTimeToStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms between a date string and a DateTime object.
- *
- * @author Bernhard Schussek <[email protected]>
- * @author Florian Eckerstorfer <[email protected]>
- *
- * @extends BaseDateTimeTransformer<string>
- */
- class DateTimeToStringTransformer extends BaseDateTimeTransformer
- {
- /**
- * Format used for generating strings.
- */
- private string $generateFormat;
- /**
- * Format used for parsing strings.
- *
- * Different than the {@link $generateFormat} because formats for parsing
- * support additional characters in PHP that are not supported for
- * generating strings.
- */
- private string $parseFormat;
- /**
- * Transforms a \DateTime instance to a string.
- *
- * @see \DateTime::format() for supported formats
- *
- * @param string|null $inputTimezone The name of the input timezone
- * @param string|null $outputTimezone The name of the output timezone
- * @param string $format The date format
- * @param string|null $parseFormat The parse format when different from $format
- */
- public function __construct(?string $inputTimezone = null, ?string $outputTimezone = null, string $format = 'Y-m-d H:i:s', ?string $parseFormat = null)
- {
- parent::__construct($inputTimezone, $outputTimezone);
- $this->generateFormat = $format;
- $this->parseFormat = $parseFormat ?? $format;
- // See https://php.net/datetime.createfromformat
- // The character "|" in the format makes sure that the parts of a date
- // that are *not* specified in the format are reset to the corresponding
- // values from 1970-01-01 00:00:00 instead of the current time.
- // Without "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 12:32:47",
- // where the time corresponds to the current server time.
- // With "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 00:00:00",
- // which is at least deterministic and thus used here.
- if (!str_contains($this->parseFormat, '|')) {
- $this->parseFormat .= '|';
- }
- }
- /**
- * Transforms a DateTime object into a date string with the configured format
- * and timezone.
- *
- * @param \DateTimeInterface $dateTime A DateTimeInterface object
- *
- * @throws TransformationFailedException If the given value is not a \DateTimeInterface
- */
- public function transform(mixed $dateTime): string
- {
- if (null === $dateTime) {
- return '';
- }
- if (!$dateTime instanceof \DateTimeInterface) {
- throw new TransformationFailedException('Expected a \DateTimeInterface.');
- }
- $dateTime = \DateTimeImmutable::createFromInterface($dateTime);
- $dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
- return $dateTime->format($this->generateFormat);
- }
- /**
- * Transforms a date string in the configured timezone into a DateTime object.
- *
- * @param string $value A value as produced by PHP's date() function
- *
- * @throws TransformationFailedException If the given value is not a string,
- * or could not be transformed
- */
- public function reverseTransform(mixed $value): ?\DateTime
- {
- if (!$value) {
- return null;
- }
- if (!\is_string($value)) {
- throw new TransformationFailedException('Expected a string.');
- }
- if (str_contains($value, "\0")) {
- throw new TransformationFailedException('Null bytes not allowed');
- }
- $outputTz = new \DateTimeZone($this->outputTimezone);
- $dateTime = \DateTime::createFromFormat($this->parseFormat, $value, $outputTz);
- $lastErrors = \DateTime::getLastErrors() ?: ['error_count' => 0, 'warning_count' => 0];
- if (0 < $lastErrors['warning_count'] || 0 < $lastErrors['error_count']) {
- throw new TransformationFailedException(implode(', ', array_merge(array_values($lastErrors['warnings']), array_values($lastErrors['errors']))));
- }
- try {
- if ($this->inputTimezone !== $this->outputTimezone) {
- $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
- }
- } catch (\Exception $e) {
- throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
- }
- return $dateTime;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/StringToFloatTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * @implements DataTransformerInterface<numeric-string, float>
- */
- class StringToFloatTransformer implements DataTransformerInterface
- {
- public function __construct(
- private ?int $scale = null,
- ) {
- }
- public function transform(mixed $value): ?float
- {
- if (null === $value) {
- return null;
- }
- if (!\is_string($value) || !is_numeric($value)) {
- throw new TransformationFailedException('Expected a numeric string.');
- }
- return (float) $value;
- }
- public function reverseTransform(mixed $value): ?string
- {
- if (null === $value) {
- return null;
- }
- if (!\is_int($value) && !\is_float($value)) {
- throw new TransformationFailedException('Expected a numeric.');
- }
- if ($this->scale > 0) {
- return number_format((float) $value, $this->scale, '.', '');
- }
- return (string) $value;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/ArrayToPartsTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * @author Bernhard Schussek <[email protected]>
- *
- * @implements DataTransformerInterface<array, array>
- */
- class ArrayToPartsTransformer implements DataTransformerInterface
- {
- public function __construct(
- private array $partMapping,
- ) {
- }
- public function transform(mixed $array): mixed
- {
- if (!\is_array($array ??= [])) {
- throw new TransformationFailedException('Expected an array.');
- }
- $result = [];
- foreach ($this->partMapping as $partKey => $originalKeys) {
- if (!$array) {
- $result[$partKey] = null;
- } else {
- $result[$partKey] = array_intersect_key($array, array_flip($originalKeys));
- }
- }
- return $result;
- }
- public function reverseTransform(mixed $array): mixed
- {
- if (!\is_array($array)) {
- throw new TransformationFailedException('Expected an array.');
- }
- $result = [];
- $emptyKeys = [];
- foreach ($this->partMapping as $partKey => $originalKeys) {
- if (!empty($array[$partKey])) {
- foreach ($originalKeys as $originalKey) {
- if (isset($array[$partKey][$originalKey])) {
- $result[$originalKey] = $array[$partKey][$originalKey];
- }
- }
- } else {
- $emptyKeys[] = $partKey;
- }
- }
- if (\count($emptyKeys) > 0) {
- if (\count($emptyKeys) === \count($this->partMapping)) {
- // All parts empty
- return null;
- }
- throw new TransformationFailedException(\sprintf('The keys "%s" should not be empty.', implode('", "', $emptyKeys)));
- }
- return $result;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- /**
- * Transforms between a normalized time and a localized time string.
- *
- * @author Bernhard Schussek <[email protected]>
- * @author Florian Eckerstorfer <[email protected]>
- *
- * @extends BaseDateTimeTransformer<string>
- */
- class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
- {
- private int $dateFormat;
- private int $timeFormat;
- /**
- * @see BaseDateTimeTransformer::formats for available format options
- *
- * @param string|null $inputTimezone The name of the input timezone
- * @param string|null $outputTimezone The name of the output timezone
- * @param int|null $dateFormat The date format
- * @param int|null $timeFormat The time format
- * @param int|\IntlCalendar $calendar One of the \IntlDateFormatter calendar constants or an \IntlCalendar instance
- * @param string|null $pattern A pattern to pass to \IntlDateFormatter
- *
- * @throws UnexpectedTypeException If a format is not supported or if a timezone is not a string
- */
- public function __construct(
- ?string $inputTimezone = null,
- ?string $outputTimezone = null,
- ?int $dateFormat = null,
- ?int $timeFormat = null,
- private int|\IntlCalendar $calendar = \IntlDateFormatter::GREGORIAN,
- private ?string $pattern = null,
- ) {
- parent::__construct($inputTimezone, $outputTimezone);
- $dateFormat ??= \IntlDateFormatter::MEDIUM;
- $timeFormat ??= \IntlDateFormatter::SHORT;
- if (!\in_array($dateFormat, self::$formats, true)) {
- throw new UnexpectedTypeException($dateFormat, implode('", "', self::$formats));
- }
- if (!\in_array($timeFormat, self::$formats, true)) {
- throw new UnexpectedTypeException($timeFormat, implode('", "', self::$formats));
- }
- if (\is_int($calendar) && !\in_array($calendar, [\IntlDateFormatter::GREGORIAN, \IntlDateFormatter::TRADITIONAL], true)) {
- throw new InvalidArgumentException('The "calendar" option should be either an \IntlDateFormatter constant or an \IntlCalendar instance.');
- }
- $this->dateFormat = $dateFormat;
- $this->timeFormat = $timeFormat;
- }
- /**
- * Transforms a normalized date into a localized date string/array.
- *
- * @param \DateTimeInterface $dateTime A DateTimeInterface object
- *
- * @throws TransformationFailedException if the given value is not a \DateTimeInterface
- * or if the date could not be transformed
- */
- public function transform(mixed $dateTime): string
- {
- if (null === $dateTime) {
- return '';
- }
- if (!$dateTime instanceof \DateTimeInterface) {
- throw new TransformationFailedException('Expected a \DateTimeInterface.');
- }
- $value = $this->getIntlDateFormatter()->format($dateTime->getTimestamp());
- if (0 != intl_get_error_code()) {
- throw new TransformationFailedException(intl_get_error_message());
- }
- return $value;
- }
- /**
- * Transforms a localized date string/array into a normalized date.
- *
- * @param string $value Localized date string
- *
- * @throws TransformationFailedException if the given value is not a string,
- * if the date could not be parsed
- */
- public function reverseTransform(mixed $value): ?\DateTime
- {
- if (!\is_string($value)) {
- throw new TransformationFailedException('Expected a string.');
- }
- if ('' === $value) {
- return null;
- }
- // date-only patterns require parsing to be done in UTC, as midnight might not exist in the local timezone due
- // to DST changes
- $dateOnly = $this->isPatternDateOnly();
- $dateFormatter = $this->getIntlDateFormatter($dateOnly);
- try {
- $timestamp = @$dateFormatter->parse($value);
- } catch (\IntlException $e) {
- throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
- }
- if (0 != intl_get_error_code()) {
- throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code());
- } elseif ($timestamp > 253402214400) {
- // This timestamp represents UTC midnight of 9999-12-31 to prevent 5+ digit years
- throw new TransformationFailedException('Years beyond 9999 are not supported.');
- } elseif (false === $timestamp) {
- // the value couldn't be parsed but the Intl extension didn't report an error code, this
- // could be the case when the Intl polyfill is used which always returns 0 as the error code
- throw new TransformationFailedException(\sprintf('"%s" could not be parsed as a date.', $value));
- }
- try {
- if ($dateOnly) {
- // we only care about year-month-date, which has been delivered as a timestamp pointing to UTC midnight
- $dateTime = new \DateTime(gmdate('Y-m-d', $timestamp), new \DateTimeZone($this->outputTimezone));
- } else {
- // read timestamp into DateTime object - the formatter delivers a timestamp
- $dateTime = new \DateTime(\sprintf('@%s', $timestamp));
- }
- // set timezone separately, as it would be ignored if set via the constructor,
- // see https://php.net/datetime.construct
- $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
- } catch (\Exception $e) {
- throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
- }
- if ($this->outputTimezone !== $this->inputTimezone) {
- $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
- }
- return $dateTime;
- }
- /**
- * Returns a preconfigured IntlDateFormatter instance.
- *
- * @param bool $ignoreTimezone Use UTC regardless of the configured timezone
- */
- protected function getIntlDateFormatter(bool $ignoreTimezone = false): \IntlDateFormatter
- {
- $dateFormat = $this->dateFormat;
- $timeFormat = $this->timeFormat;
- $timezone = new \DateTimeZone($ignoreTimezone ? 'UTC' : $this->outputTimezone);
- $calendar = $this->calendar;
- $pattern = $this->pattern;
- $intlDateFormatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormat, $timeFormat, $timezone, $calendar, $pattern ?? '');
- $intlDateFormatter->setLenient(false);
- return $intlDateFormatter;
- }
- /**
- * Checks if the pattern contains only a date.
- */
- protected function isPatternDateOnly(): bool
- {
- if (null === $this->pattern) {
- return false;
- }
- // strip escaped text
- $pattern = preg_replace("#'(.*?)'#", '', $this->pattern);
- // check for the absence of time-related placeholders
- return 0 === preg_match('#[ahHkKmsSAzZOvVxX]#', $pattern);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms between a normalized format and a localized money string.
- *
- * @author Bernhard Schussek <[email protected]>
- * @author Florian Eckerstorfer <[email protected]>
- */
- class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransformer
- {
- private int $divisor;
- public function __construct(
- ?int $scale = 2,
- ?bool $grouping = true,
- ?int $roundingMode = \NumberFormatter::ROUND_HALFUP,
- ?int $divisor = 1,
- ?string $locale = null,
- private readonly string $input = 'float',
- ) {
- parent::__construct($scale ?? 2, $grouping ?? true, $roundingMode, $locale);
- $this->divisor = $divisor ?? 1;
- }
- /**
- * Transforms a normalized format into a localized money string.
- *
- * @param int|float|null $value Normalized number
- *
- * @throws TransformationFailedException if the given value is not numeric or
- * if the value cannot be transformed
- */
- public function transform(mixed $value): string
- {
- if (null !== $value && 1 !== $this->divisor) {
- if (!is_numeric($value)) {
- throw new TransformationFailedException('Expected a numeric.');
- }
- $value /= $this->divisor;
- }
- return parent::transform($value);
- }
- /**
- * Transforms a localized money string into a normalized format.
- *
- * @param string $value Localized money string
- *
- * @throws TransformationFailedException if the given value is not a string
- * or if the value cannot be transformed
- */
- public function reverseTransform(mixed $value): int|float|null
- {
- $value = parent::reverseTransform($value);
- if (null !== $value) {
- $value = (string) ($value * $this->divisor);
- if ('float' === $this->input) {
- return (float) $value;
- }
- if ($value > \PHP_INT_MAX || $value < \PHP_INT_MIN) {
- throw new TransformationFailedException(\sprintf('Cannot cast "%s" to an integer. Try setting the input to "float" instead.', $value));
- }
- $value = (int) $value;
- }
- return $value;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/DateIntervalToStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- /**
- * Transforms between a date string and a DateInterval object.
- *
- * @author Steffen Roßkamp <[email protected]>
- *
- * @implements DataTransformerInterface<\DateInterval, string>
- */
- class DateIntervalToStringTransformer implements DataTransformerInterface
- {
- /**
- * Transforms a \DateInterval instance to a string.
- *
- * @see \DateInterval::format() for supported formats
- *
- * @param string $format The date format
- */
- public function __construct(
- private string $format = 'P%yY%mM%dDT%hH%iM%sS',
- ) {
- }
- /**
- * Transforms a DateInterval object into a date string with the configured format.
- *
- * @param \DateInterval|null $value A DateInterval object
- *
- * @throws UnexpectedTypeException if the given value is not a \DateInterval instance
- */
- public function transform(mixed $value): string
- {
- if (null === $value) {
- return '';
- }
- if (!$value instanceof \DateInterval) {
- throw new UnexpectedTypeException($value, \DateInterval::class);
- }
- return $value->format($this->format);
- }
- /**
- * Transforms a date string in the configured format into a DateInterval object.
- *
- * @param string $value An ISO 8601 or date string like date interval presentation
- *
- * @throws UnexpectedTypeException if the given value is not a string
- * @throws TransformationFailedException if the date interval could not be parsed
- */
- public function reverseTransform(mixed $value): ?\DateInterval
- {
- if (null === $value) {
- return null;
- }
- if (!\is_string($value)) {
- throw new UnexpectedTypeException($value, 'string');
- }
- if ('' === $value) {
- return null;
- }
- if (!$this->isISO8601($value)) {
- throw new TransformationFailedException('Non ISO 8601 date strings are not supported yet.');
- }
- $valuePattern = '/^'.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?P<$1>\d+)$2', $this->format).'$/';
- if (!preg_match($valuePattern, $value)) {
- throw new TransformationFailedException(\sprintf('Value "%s" contains intervals not accepted by format "%s".', $value, $this->format));
- }
- try {
- $dateInterval = new \DateInterval($value);
- } catch (\Exception $e) {
- throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
- }
- return $dateInterval;
- }
- private function isISO8601(string $string): bool
- {
- 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);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- use Symfony\Component\Form\Exception\UnexpectedTypeException;
- /**
- * Transforms between a normalized format (integer or float) and a percentage value.
- *
- * @author Bernhard Schussek <[email protected]>
- * @author Florian Eckerstorfer <[email protected]>
- *
- * @implements DataTransformerInterface<int|float, string>
- */
- class PercentToLocalizedStringTransformer implements DataTransformerInterface
- {
- public const FRACTIONAL = 'fractional';
- public const INTEGER = 'integer';
- protected static array $types = [
- self::FRACTIONAL,
- self::INTEGER,
- ];
- private string $type;
- private int $scale;
- /**
- * @see self::$types for a list of supported types
- *
- * @param int $roundingMode A value from \NumberFormatter, such as \NumberFormatter::ROUND_HALFUP
- * @param bool $html5Format Use an HTML5 specific format, see https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats
- *
- * @throws UnexpectedTypeException if the given value of type is unknown
- */
- public function __construct(
- ?int $scale = null,
- ?string $type = null,
- private int $roundingMode = \NumberFormatter::ROUND_HALFUP,
- private bool $html5Format = false,
- ) {
- $type ??= self::FRACTIONAL;
- if (!\in_array($type, self::$types, true)) {
- throw new UnexpectedTypeException($type, implode('", "', self::$types));
- }
- $this->type = $type;
- $this->scale = $scale ?? 0;
- }
- /**
- * Transforms between a normalized format (integer or float) into a percentage value.
- *
- * @param int|float $value Normalized value
- *
- * @throws TransformationFailedException if the given value is not numeric or
- * if the value could not be transformed
- */
- public function transform(mixed $value): string
- {
- if (null === $value) {
- return '';
- }
- if (!is_numeric($value)) {
- throw new TransformationFailedException('Expected a numeric.');
- }
- if (self::FRACTIONAL == $this->type) {
- $value *= 100;
- }
- $formatter = $this->getNumberFormatter();
- $value = $formatter->format($value);
- if (intl_is_failure($formatter->getErrorCode())) {
- throw new TransformationFailedException($formatter->getErrorMessage());
- }
- // replace the UTF-8 non break spaces
- return $value;
- }
- /**
- * Transforms between a percentage value into a normalized format (integer or float).
- *
- * @param string $value Percentage value
- *
- * @throws TransformationFailedException if the given value is not a string or
- * if the value could not be transformed
- */
- public function reverseTransform(mixed $value): int|float|null
- {
- if (!\is_string($value)) {
- throw new TransformationFailedException('Expected a string.');
- }
- if ('' === $value) {
- return null;
- }
- $position = 0;
- $formatter = $this->getNumberFormatter();
- $groupSep = $formatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL);
- $decSep = $formatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
- $grouping = $formatter->getAttribute(\NumberFormatter::GROUPING_USED);
- if ('.' !== $decSep && (!$grouping || '.' !== $groupSep)) {
- $value = str_replace('.', $decSep, $value);
- }
- if (',' !== $decSep && (!$grouping || ',' !== $groupSep)) {
- $value = str_replace(',', $decSep, $value);
- }
- if (str_contains($value, $decSep)) {
- $type = \NumberFormatter::TYPE_DOUBLE;
- } else {
- $type = \PHP_INT_SIZE === 8 ? \NumberFormatter::TYPE_INT64 : \NumberFormatter::TYPE_INT32;
- }
- try {
- // replace normal spaces so that the formatter can read them
- $result = @$formatter->parse(str_replace(' ', "\xc2\xa0", $value), $type, $position);
- } catch (\IntlException $e) {
- throw new TransformationFailedException($e->getMessage(), 0, $e);
- }
- if (intl_is_failure($formatter->getErrorCode())) {
- throw new TransformationFailedException($formatter->getErrorMessage(), $formatter->getErrorCode());
- }
- if (self::FRACTIONAL == $this->type) {
- $result /= 100;
- }
- if (\function_exists('mb_detect_encoding') && false !== $encoding = mb_detect_encoding($value, null, true)) {
- $length = mb_strlen($value, $encoding);
- $remainder = mb_substr($value, $position, $length, $encoding);
- } else {
- $length = \strlen($value);
- $remainder = substr($value, $position, $length);
- }
- // After parsing, position holds the index of the character where the
- // parsing stopped
- if ($position < $length) {
- // Check if there are unrecognized characters at the end of the
- // number (excluding whitespace characters)
- $remainder = trim($remainder, " \t\n\r\0\x0b\xc2\xa0");
- if ('' !== $remainder) {
- throw new TransformationFailedException(\sprintf('The number contains unrecognized characters: "%s".', $remainder));
- }
- }
- return $this->round($result);
- }
- /**
- * Returns a preconfigured \NumberFormatter instance.
- */
- protected function getNumberFormatter(): \NumberFormatter
- {
- // Values used in HTML5 number inputs should be formatted as in "1234.5", ie. 'en' format without grouping,
- // according to https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats
- $formatter = new \NumberFormatter($this->html5Format ? 'en' : \Locale::getDefault(), \NumberFormatter::DECIMAL);
- if ($this->html5Format) {
- $formatter->setAttribute(\NumberFormatter::GROUPING_USED, 0);
- }
- $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
- $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
- return $formatter;
- }
- /**
- * Rounds a number according to the configured scale and rounding mode.
- */
- private function round(int|float $number): int|float
- {
- // shift number to maintain the correct scale during rounding
- $roundingCoef = 10 ** $this->scale;
- if (self::FRACTIONAL === $this->type) {
- $roundingCoef *= 100;
- }
- // string representation to avoid rounding errors, similar to bcmul()
- $number = (string) ($number * $roundingCoef);
- $number = match ($this->roundingMode) {
- \NumberFormatter::ROUND_CEILING => ceil($number),
- \NumberFormatter::ROUND_FLOOR => floor($number),
- \NumberFormatter::ROUND_UP => $number > 0 ? ceil($number) : floor($number),
- \NumberFormatter::ROUND_DOWN => $number > 0 ? floor($number) : ceil($number),
- \NumberFormatter::ROUND_HALFEVEN => round($number, 0, \PHP_ROUND_HALF_EVEN),
- \NumberFormatter::ROUND_HALFUP => round($number, 0, \PHP_ROUND_HALF_UP),
- \NumberFormatter::ROUND_HALFDOWN => round($number, 0, \PHP_ROUND_HALF_DOWN),
- };
- return 1 === $roundingCoef ? (int) $number : $number / $roundingCoef;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/DateTimeImmutableToDateTimeTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms between a DateTimeImmutable object and a DateTime object.
- *
- * @author Valentin Udaltsov <[email protected]>
- *
- * @implements DataTransformerInterface<\DateTimeImmutable, \DateTime>
- */
- final class DateTimeImmutableToDateTimeTransformer implements DataTransformerInterface
- {
- /**
- * Transforms a DateTimeImmutable into a DateTime object.
- *
- * @param \DateTimeImmutable|null $value A DateTimeImmutable object
- *
- * @throws TransformationFailedException If the given value is not a \DateTimeImmutable
- */
- public function transform(mixed $value): ?\DateTime
- {
- if (null === $value) {
- return null;
- }
- if (!$value instanceof \DateTimeImmutable) {
- throw new TransformationFailedException('Expected a \DateTimeImmutable.');
- }
- return \DateTime::createFromImmutable($value);
- }
- /**
- * Transforms a DateTime object into a DateTimeImmutable object.
- *
- * @param \DateTime|null $value A DateTime object
- *
- * @throws TransformationFailedException If the given value is not a \DateTime
- */
- public function reverseTransform(mixed $value): ?\DateTimeImmutable
- {
- if (null === $value) {
- return null;
- }
- if (!$value instanceof \DateTime) {
- throw new TransformationFailedException('Expected a \DateTime.');
- }
- return \DateTimeImmutable::createFromMutable($value);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/DateTimeZoneToStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms between a timezone identifier string and a DateTimeZone object.
- *
- * @author Roland Franssen <[email protected]>
- *
- * @implements DataTransformerInterface<\DateTimeZone|array<\DateTimeZone>, string|array<string>>
- */
- class DateTimeZoneToStringTransformer implements DataTransformerInterface
- {
- public function __construct(
- private bool $multiple = false,
- ) {
- }
- public function transform(mixed $dateTimeZone): mixed
- {
- if (null === $dateTimeZone) {
- return null;
- }
- if ($this->multiple) {
- if (!\is_array($dateTimeZone)) {
- throw new TransformationFailedException('Expected an array of \DateTimeZone objects.');
- }
- return array_map([new self(), 'transform'], $dateTimeZone);
- }
- if (!$dateTimeZone instanceof \DateTimeZone) {
- throw new TransformationFailedException('Expected a \DateTimeZone object.');
- }
- return $dateTimeZone->getName();
- }
- public function reverseTransform(mixed $value): mixed
- {
- if (null === $value) {
- return null;
- }
- if ($this->multiple) {
- if (!\is_array($value)) {
- throw new TransformationFailedException('Expected an array of timezone identifier strings.');
- }
- return array_map([new self(), 'reverseTransform'], $value);
- }
- if (!\is_string($value)) {
- throw new TransformationFailedException('Expected a timezone identifier string.');
- }
- try {
- return new \DateTimeZone($value);
- } catch (\Exception $e) {
- throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms between a number type and a localized number with grouping
- * (each thousand) and comma separators.
- *
- * @author Bernhard Schussek <[email protected]>
- * @author Florian Eckerstorfer <[email protected]>
- *
- * @implements DataTransformerInterface<int|float, string>
- */
- class NumberToLocalizedStringTransformer implements DataTransformerInterface
- {
- protected bool $grouping;
- protected int $roundingMode;
- public function __construct(
- private ?int $scale = null,
- ?bool $grouping = false,
- ?int $roundingMode = \NumberFormatter::ROUND_HALFUP,
- private ?string $locale = null,
- ) {
- $this->grouping = $grouping ?? false;
- $this->roundingMode = $roundingMode ?? \NumberFormatter::ROUND_HALFUP;
- }
- /**
- * Transforms a number type into localized number.
- *
- * @param int|float|null $value Number value
- *
- * @throws TransformationFailedException if the given value is not numeric
- * or if the value cannot be transformed
- */
- public function transform(mixed $value): string
- {
- if (null === $value) {
- return '';
- }
- if (!is_numeric($value)) {
- throw new TransformationFailedException('Expected a numeric.');
- }
- $formatter = $this->getNumberFormatter();
- $value = $formatter->format($value);
- if (intl_is_failure($formatter->getErrorCode())) {
- throw new TransformationFailedException($formatter->getErrorMessage());
- }
- // Convert non-breaking and narrow non-breaking spaces to normal ones
- return str_replace(["\xc2\xa0", "\xe2\x80\xaf"], ' ', $value);
- }
- /**
- * Transforms a localized number into an integer or float.
- *
- * @param string $value The localized value
- *
- * @throws TransformationFailedException if the given value is not a string
- * or if the value cannot be transformed
- */
- public function reverseTransform(mixed $value): int|float|null
- {
- if (null !== $value && !\is_string($value)) {
- throw new TransformationFailedException('Expected a string.');
- }
- if (null === $value || '' === $value) {
- return null;
- }
- if (\in_array($value, ['NaN', 'NAN', 'nan'], true)) {
- throw new TransformationFailedException('"NaN" is not a valid number.');
- }
- $position = 0;
- $formatter = $this->getNumberFormatter();
- $groupSep = $formatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL);
- $decSep = $formatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
- if ('.' !== $decSep && (!$this->grouping || '.' !== $groupSep)) {
- $value = str_replace('.', $decSep, $value);
- }
- if (',' !== $decSep && (!$this->grouping || ',' !== $groupSep)) {
- $value = str_replace(',', $decSep, $value);
- }
- // If the value is in exponential notation with a negative exponent, we end up with a float value too
- if (str_contains($value, $decSep) || false !== stripos($value, 'e-')) {
- $type = \NumberFormatter::TYPE_DOUBLE;
- } else {
- $type = \PHP_INT_SIZE === 8
- ? \NumberFormatter::TYPE_INT64
- : \NumberFormatter::TYPE_INT32;
- }
- try {
- $result = @$formatter->parse($value, $type, $position);
- } catch (\IntlException $e) {
- throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
- }
- if (intl_is_failure($formatter->getErrorCode())) {
- throw new TransformationFailedException($formatter->getErrorMessage(), $formatter->getErrorCode());
- }
- if ($result >= \PHP_INT_MAX || $result <= -\PHP_INT_MAX) {
- throw new TransformationFailedException('I don\'t have a clear idea what infinity looks like.');
- }
- $result = $this->castParsedValue($result);
- if (false !== $encoding = mb_detect_encoding($value, null, true)) {
- $length = mb_strlen($value, $encoding);
- $remainder = mb_substr($value, $position, $length, $encoding);
- } else {
- $length = \strlen($value);
- $remainder = substr($value, $position, $length);
- }
- // After parsing, position holds the index of the character where the
- // parsing stopped
- if ($position < $length) {
- // Check if there are unrecognized characters at the end of the
- // number (excluding whitespace characters)
- $remainder = trim($remainder, " \t\n\r\0\x0b\xc2\xa0");
- if ('' !== $remainder) {
- throw new TransformationFailedException(\sprintf('The number contains unrecognized characters: "%s".', $remainder));
- }
- }
- // NumberFormatter::parse() does not round
- return $this->round($result);
- }
- /**
- * Returns a preconfigured \NumberFormatter instance.
- */
- protected function getNumberFormatter(): \NumberFormatter
- {
- $formatter = new \NumberFormatter($this->locale ?? \Locale::getDefault(), \NumberFormatter::DECIMAL);
- if (null !== $this->scale) {
- $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
- $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
- }
- $formatter->setAttribute(\NumberFormatter::GROUPING_USED, $this->grouping);
- return $formatter;
- }
- /**
- * @internal
- */
- protected function castParsedValue(int|float $value): int|float
- {
- if (\is_int($value) && $value === (int) $float = (float) $value) {
- return $float;
- }
- return $value;
- }
- /**
- * Rounds a number according to the configured scale and rounding mode.
- */
- private function round(int|float $number): int|float
- {
- if (null !== $this->scale) {
- // shift number to maintain the correct scale during rounding
- $roundingCoef = 10 ** $this->scale;
- // string representation to avoid rounding errors, similar to bcmul()
- $number = (string) ($number * $roundingCoef);
- $number = match ($this->roundingMode) {
- \NumberFormatter::ROUND_CEILING => ceil($number),
- \NumberFormatter::ROUND_FLOOR => floor($number),
- \NumberFormatter::ROUND_UP => $number > 0 ? ceil($number) : floor($number),
- \NumberFormatter::ROUND_DOWN => $number > 0 ? floor($number) : ceil($number),
- \NumberFormatter::ROUND_HALFEVEN => round($number, 0, \PHP_ROUND_HALF_EVEN),
- \NumberFormatter::ROUND_HALFUP => round($number, 0, \PHP_ROUND_HALF_UP),
- \NumberFormatter::ROUND_HALFDOWN => round($number, 0, \PHP_ROUND_HALF_DOWN),
- };
- $number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef;
- }
- return $number;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/IntlTimeZoneToStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- /**
- * Transforms between a timezone identifier string and a IntlTimeZone object.
- *
- * @author Roland Franssen <[email protected]>
- *
- * @implements DataTransformerInterface<\IntlTimeZone|array<\IntlTimeZone>, string|array<string>>
- */
- class IntlTimeZoneToStringTransformer implements DataTransformerInterface
- {
- public function __construct(
- private bool $multiple = false,
- ) {
- }
- public function transform(mixed $intlTimeZone): mixed
- {
- if (null === $intlTimeZone) {
- return null;
- }
- if ($this->multiple) {
- if (!\is_array($intlTimeZone)) {
- throw new TransformationFailedException('Expected an array of \IntlTimeZone objects.');
- }
- return array_map([new self(), 'transform'], $intlTimeZone);
- }
- if (!$intlTimeZone instanceof \IntlTimeZone) {
- throw new TransformationFailedException('Expected a \IntlTimeZone object.');
- }
- return $intlTimeZone->getID();
- }
- public function reverseTransform(mixed $value): mixed
- {
- if (null === $value) {
- return null;
- }
- if ($this->multiple) {
- if (!\is_array($value)) {
- throw new TransformationFailedException('Expected an array of timezone identifier strings.');
- }
- return array_map([new self(), 'reverseTransform'], $value);
- }
- if (!\is_string($value)) {
- throw new TransformationFailedException('Expected a timezone identifier string.');
- }
- $intlTimeZone = \IntlTimeZone::createTimeZone($value);
- if ('Etc/Unknown' === $intlTimeZone->getID()) {
- throw new TransformationFailedException(\sprintf('Unknown timezone identifier "%s".', $value));
- }
- return $intlTimeZone;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Core/DataTransformer/UuidToStringTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Core\DataTransformer;
- use Symfony\Component\Form\DataTransformerInterface;
- use Symfony\Component\Form\Exception\TransformationFailedException;
- use Symfony\Component\Uid\Uuid;
- /**
- * Transforms between a UUID string and a Uuid object.
- *
- * @author Pavel Dyakonov <[email protected]>
- *
- * @implements DataTransformerInterface<Uuid, string>
- */
- class UuidToStringTransformer implements DataTransformerInterface
- {
- /**
- * Transforms a Uuid object into a string.
- *
- * @param Uuid $value A Uuid object
- *
- * @throws TransformationFailedException If the given value is not a Uuid object
- */
- public function transform(mixed $value): ?string
- {
- if (null === $value) {
- return null;
- }
- if (!$value instanceof Uuid) {
- throw new TransformationFailedException('Expected a Uuid.');
- }
- return (string) $value;
- }
- /**
- * Transforms a UUID string into a Uuid object.
- *
- * @param string $value A UUID string
- *
- * @throws TransformationFailedException If the given value is not a string,
- * or could not be transformed
- */
- public function reverseTransform(mixed $value): ?Uuid
- {
- if (null === $value || '' === $value) {
- return null;
- }
- if (!\is_string($value)) {
- throw new TransformationFailedException('Expected a string.');
- }
- if (!Uuid::isValid($value)) {
- throw new TransformationFailedException(\sprintf('The value "%s" is not a valid UUID.', $value));
- }
- try {
- return Uuid::fromString($value);
- } catch (\InvalidArgumentException $e) {
- throw new TransformationFailedException(\sprintf('The value "%s" is not a valid UUID.', $value), $e->getCode(), $e);
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/ValidatorTypeGuesser.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator;
- use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
- use Symfony\Component\Form\Extension\Core\Type\CollectionType;
- use Symfony\Component\Form\Extension\Core\Type\CountryType;
- use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
- use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
- use Symfony\Component\Form\Extension\Core\Type\DateType;
- use Symfony\Component\Form\Extension\Core\Type\EmailType;
- use Symfony\Component\Form\Extension\Core\Type\FileType;
- use Symfony\Component\Form\Extension\Core\Type\IntegerType;
- use Symfony\Component\Form\Extension\Core\Type\LanguageType;
- use Symfony\Component\Form\Extension\Core\Type\LocaleType;
- use Symfony\Component\Form\Extension\Core\Type\NumberType;
- use Symfony\Component\Form\Extension\Core\Type\TextType;
- use Symfony\Component\Form\Extension\Core\Type\TimeType;
- use Symfony\Component\Form\Extension\Core\Type\UrlType;
- use Symfony\Component\Form\FormTypeGuesserInterface;
- use Symfony\Component\Form\Guess\Guess;
- use Symfony\Component\Form\Guess\TypeGuess;
- use Symfony\Component\Form\Guess\ValueGuess;
- use Symfony\Component\Validator\Constraint;
- use Symfony\Component\Validator\Constraints\Count;
- use Symfony\Component\Validator\Constraints\Country;
- use Symfony\Component\Validator\Constraints\Currency;
- use Symfony\Component\Validator\Constraints\Date;
- use Symfony\Component\Validator\Constraints\DateTime;
- use Symfony\Component\Validator\Constraints\Email;
- use Symfony\Component\Validator\Constraints\File;
- use Symfony\Component\Validator\Constraints\Image;
- use Symfony\Component\Validator\Constraints\Ip;
- use Symfony\Component\Validator\Constraints\IsFalse;
- use Symfony\Component\Validator\Constraints\IsTrue;
- use Symfony\Component\Validator\Constraints\Language;
- use Symfony\Component\Validator\Constraints\Length;
- use Symfony\Component\Validator\Constraints\Locale;
- use Symfony\Component\Validator\Constraints\NotBlank;
- use Symfony\Component\Validator\Constraints\NotNull;
- use Symfony\Component\Validator\Constraints\Range;
- use Symfony\Component\Validator\Constraints\Regex;
- use Symfony\Component\Validator\Constraints\Time;
- use Symfony\Component\Validator\Constraints\Type;
- use Symfony\Component\Validator\Constraints\Url;
- use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
- use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
- class ValidatorTypeGuesser implements FormTypeGuesserInterface
- {
- public function __construct(
- private MetadataFactoryInterface $metadataFactory,
- ) {
- }
- public function guessType(string $class, string $property): ?TypeGuess
- {
- return $this->guess($class, $property, $this->guessTypeForConstraint(...));
- }
- public function guessRequired(string $class, string $property): ?ValueGuess
- {
- // If we don't find any constraint telling otherwise, we can assume
- // that a field is not required (with LOW_CONFIDENCE)
- return $this->guess($class, $property, $this->guessRequiredForConstraint(...), false);
- }
- public function guessMaxLength(string $class, string $property): ?ValueGuess
- {
- return $this->guess($class, $property, $this->guessMaxLengthForConstraint(...));
- }
- public function guessPattern(string $class, string $property): ?ValueGuess
- {
- return $this->guess($class, $property, $this->guessPatternForConstraint(...));
- }
- /**
- * Guesses a field class name for a given constraint.
- */
- public function guessTypeForConstraint(Constraint $constraint): ?TypeGuess
- {
- switch ($constraint::class) {
- case Type::class:
- switch ($constraint->type) {
- case 'array':
- return new TypeGuess(CollectionType::class, [], Guess::MEDIUM_CONFIDENCE);
- case 'boolean':
- case 'bool':
- return new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE);
- case 'double':
- case 'float':
- case 'numeric':
- case 'real':
- return new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE);
- case 'integer':
- case 'int':
- case 'long':
- return new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE);
- case \DateTime::class:
- case '\DateTime':
- return new TypeGuess(DateType::class, [], Guess::MEDIUM_CONFIDENCE);
- case \DateTimeImmutable::class:
- case '\DateTimeImmutable':
- case \DateTimeInterface::class:
- case '\DateTimeInterface':
- return new TypeGuess(DateType::class, ['input' => 'datetime_immutable'], Guess::MEDIUM_CONFIDENCE);
- case 'string':
- return new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE);
- }
- break;
- case Country::class:
- return new TypeGuess(CountryType::class, [], Guess::HIGH_CONFIDENCE);
- case Currency::class:
- return new TypeGuess(CurrencyType::class, [], Guess::HIGH_CONFIDENCE);
- case Date::class:
- return new TypeGuess(DateType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE);
- case DateTime::class:
- return new TypeGuess(DateTimeType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE);
- case Email::class:
- return new TypeGuess(EmailType::class, [], Guess::HIGH_CONFIDENCE);
- case File::class:
- case Image::class:
- $options = [];
- if ($constraint->mimeTypes) {
- $options = ['attr' => ['accept' => implode(',', (array) $constraint->mimeTypes)]];
- }
- return new TypeGuess(FileType::class, $options, Guess::HIGH_CONFIDENCE);
- case Language::class:
- return new TypeGuess(LanguageType::class, [], Guess::HIGH_CONFIDENCE);
- case Locale::class:
- return new TypeGuess(LocaleType::class, [], Guess::HIGH_CONFIDENCE);
- case Time::class:
- return new TypeGuess(TimeType::class, ['input' => 'string'], Guess::HIGH_CONFIDENCE);
- case Url::class:
- return new TypeGuess(UrlType::class, [], Guess::HIGH_CONFIDENCE);
- case Ip::class:
- return new TypeGuess(TextType::class, [], Guess::MEDIUM_CONFIDENCE);
- case Length::class:
- case Regex::class:
- return new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE);
- case Range::class:
- return new TypeGuess(NumberType::class, [], Guess::LOW_CONFIDENCE);
- case Count::class:
- return new TypeGuess(CollectionType::class, [], Guess::LOW_CONFIDENCE);
- case IsTrue::class:
- case IsFalse::class:
- return new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE);
- }
- return null;
- }
- /**
- * Guesses whether a field is required based on the given constraint.
- */
- public function guessRequiredForConstraint(Constraint $constraint): ?ValueGuess
- {
- return match ($constraint::class) {
- NotNull::class,
- NotBlank::class,
- IsTrue::class => new ValueGuess(true, Guess::HIGH_CONFIDENCE),
- default => null,
- };
- }
- /**
- * Guesses a field's maximum length based on the given constraint.
- */
- public function guessMaxLengthForConstraint(Constraint $constraint): ?ValueGuess
- {
- switch ($constraint::class) {
- case Length::class:
- if (is_numeric($constraint->max)) {
- return new ValueGuess($constraint->max, Guess::HIGH_CONFIDENCE);
- }
- break;
- case Type::class:
- if (\in_array($constraint->type, ['double', 'float', 'numeric', 'real'])) {
- return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
- }
- break;
- case Range::class:
- if (is_numeric($constraint->max)) {
- return new ValueGuess(\strlen((string) $constraint->max), Guess::LOW_CONFIDENCE);
- }
- break;
- }
- return null;
- }
- /**
- * Guesses a field's pattern based on the given constraint.
- */
- public function guessPatternForConstraint(Constraint $constraint): ?ValueGuess
- {
- switch ($constraint::class) {
- case Length::class:
- if (is_numeric($constraint->min)) {
- return new ValueGuess(\sprintf('.{%s,}', (string) $constraint->min), Guess::LOW_CONFIDENCE);
- }
- break;
- case Regex::class:
- $htmlPattern = $constraint->getHtmlPattern();
- if (null !== $htmlPattern) {
- return new ValueGuess($htmlPattern, Guess::HIGH_CONFIDENCE);
- }
- break;
- case Range::class:
- if (is_numeric($constraint->min)) {
- return new ValueGuess(\sprintf('.{%s,}', \strlen((string) $constraint->min)), Guess::LOW_CONFIDENCE);
- }
- break;
- case Type::class:
- if (\in_array($constraint->type, ['double', 'float', 'numeric', 'real'])) {
- return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
- }
- break;
- }
- return null;
- }
- /**
- * Iterates over the constraints of a property, executes a constraints on
- * them and returns the best guess.
- *
- * @param \Closure $closure The closure that returns a guess
- * for a given constraint
- * @param mixed $defaultValue The default value assumed if no other value
- * can be guessed
- */
- protected function guess(string $class, string $property, \Closure $closure, mixed $defaultValue = null): ?Guess
- {
- $guesses = [];
- $classMetadata = $this->metadataFactory->getMetadataFor($class);
- if ($classMetadata instanceof ClassMetadataInterface && $classMetadata->hasPropertyMetadata($property)) {
- foreach ($classMetadata->getPropertyMetadata($property) as $memberMetadata) {
- foreach ($memberMetadata->getConstraints() as $constraint) {
- if ($guess = $closure($constraint)) {
- $guesses[] = $guess;
- }
- }
- }
- }
- if (null !== $defaultValue) {
- $guesses[] = new ValueGuess($defaultValue, Guess::LOW_CONFIDENCE);
- }
- return Guess::getBestGuess($guesses);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/ValidatorExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator;
- use Symfony\Component\Form\AbstractExtension;
- use Symfony\Component\Form\Extension\Validator\Constraints\Form;
- use Symfony\Component\Form\FormRendererInterface;
- use Symfony\Component\Form\FormTypeGuesserInterface;
- use Symfony\Component\Validator\Constraints\Traverse;
- use Symfony\Component\Validator\Mapping\ClassMetadata;
- use Symfony\Component\Validator\Validator\ValidatorInterface;
- use Symfony\Contracts\Translation\TranslatorInterface;
- /**
- * Extension supporting the Symfony Validator component in forms.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class ValidatorExtension extends AbstractExtension
- {
- public function __construct(
- private ValidatorInterface $validator,
- private bool $legacyErrorMessages = true,
- private ?FormRendererInterface $formRenderer = null,
- private ?TranslatorInterface $translator = null,
- ) {
- $metadata = $validator->getMetadataFor(\Symfony\Component\Form\Form::class);
- // Register the form constraints in the validator programmatically.
- // This functionality is required when using the Form component without
- // the DIC, where the XML file is loaded automatically. Thus the following
- // code must be kept synchronized with validation.xml
- /* @var ClassMetadata $metadata */
- $metadata->addConstraint(new Form());
- $metadata->addConstraint(new Traverse(false));
- }
- public function loadTypeGuesser(): ?FormTypeGuesserInterface
- {
- return new ValidatorTypeGuesser($this->validator);
- }
- protected function loadTypeExtensions(): array
- {
- return [
- new Type\FormTypeValidatorExtension($this->validator, $this->legacyErrorMessages, $this->formRenderer, $this->translator),
- new Type\RepeatedTypeValidatorExtension(),
- new Type\SubmitTypeValidatorExtension(),
- ];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/ViolationMapper/ViolationPath.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
- use Symfony\Component\Form\Exception\OutOfBoundsException;
- use Symfony\Component\PropertyAccess\PropertyPath;
- use Symfony\Component\PropertyAccess\PropertyPathInterface;
- /**
- * @author Bernhard Schussek <[email protected]>
- *
- * @implements \IteratorAggregate<int, string>
- */
- class ViolationPath implements \IteratorAggregate, PropertyPathInterface
- {
- /** @var list<string> */
- private array $elements = [];
- private array $isIndex = [];
- private array $mapsForm = [];
- private string $pathAsString = '';
- private int $length = 0;
- /**
- * Creates a new violation path from a string.
- *
- * @param string $violationPath The property path of a {@link \Symfony\Component\Validator\ConstraintViolation} object
- */
- public function __construct(string $violationPath)
- {
- $path = new PropertyPath($violationPath);
- $elements = $path->getElements();
- $data = false;
- for ($i = 0, $l = \count($elements); $i < $l; ++$i) {
- if (!$data) {
- // The element "data" has not yet been passed
- if ('children' === $elements[$i] && $path->isProperty($i)) {
- // Skip element "children"
- ++$i;
- // Next element must exist and must be an index
- // Otherwise consider this the end of the path
- if ($i >= $l || !$path->isIndex($i)) {
- break;
- }
- // All the following index items (regardless if .children is
- // explicitly used) are children and grand-children
- for (; $i < $l && $path->isIndex($i); ++$i) {
- $this->elements[] = $elements[$i];
- $this->isIndex[] = true;
- $this->mapsForm[] = true;
- }
- // Rewind the pointer as the last element above didn't match
- // (even if the pointer was moved forward)
- --$i;
- } elseif ('data' === $elements[$i] && $path->isProperty($i)) {
- // Skip element "data"
- ++$i;
- // End of path
- if ($i >= $l) {
- break;
- }
- $this->elements[] = $elements[$i];
- $this->isIndex[] = $path->isIndex($i);
- $this->mapsForm[] = false;
- $data = true;
- } else {
- // Neither "children" nor "data" property found
- // Consider this the end of the path
- break;
- }
- } else {
- // Already after the "data" element
- // Pick everything as is
- $this->elements[] = $elements[$i];
- $this->isIndex[] = $path->isIndex($i);
- $this->mapsForm[] = false;
- }
- }
- $this->length = \count($this->elements);
- $this->buildString();
- }
- public function __toString(): string
- {
- return $this->pathAsString;
- }
- public function getLength(): int
- {
- return $this->length;
- }
- public function getParent(): ?PropertyPathInterface
- {
- if ($this->length <= 1) {
- return null;
- }
- $parent = clone $this;
- --$parent->length;
- array_pop($parent->elements);
- array_pop($parent->isIndex);
- array_pop($parent->mapsForm);
- $parent->buildString();
- return $parent;
- }
- public function getElements(): array
- {
- return $this->elements;
- }
- public function getElement(int $index): string
- {
- if (!isset($this->elements[$index])) {
- throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index));
- }
- return $this->elements[$index];
- }
- public function isProperty(int $index): bool
- {
- if (!isset($this->isIndex[$index])) {
- throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index));
- }
- return !$this->isIndex[$index];
- }
- public function isIndex(int $index): bool
- {
- if (!isset($this->isIndex[$index])) {
- throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index));
- }
- return $this->isIndex[$index];
- }
- public function isNullSafe(int $index): bool
- {
- return false;
- }
- /**
- * Returns whether an element maps directly to a form.
- *
- * Consider the following violation path:
- *
- * children[address].children[office].data.street
- *
- * In this example, "address" and "office" map to forms, while
- * "street does not.
- *
- * @throws OutOfBoundsException if the offset is invalid
- */
- public function mapsForm(int $index): bool
- {
- if (!isset($this->mapsForm[$index])) {
- throw new OutOfBoundsException(\sprintf('The index "%s" is not within the violation path.', $index));
- }
- return $this->mapsForm[$index];
- }
- /**
- * Returns a new iterator for this path.
- */
- public function getIterator(): ViolationPathIterator
- {
- return new ViolationPathIterator($this);
- }
- /**
- * Builds the string representation from the elements.
- */
- private function buildString(): void
- {
- $this->pathAsString = '';
- $data = false;
- foreach ($this->elements as $index => $element) {
- if ($this->mapsForm[$index]) {
- $this->pathAsString .= ".children[$element]";
- } elseif (!$data) {
- $this->pathAsString .= '.data'.($this->isIndex[$index] ? "[$element]" : ".$element");
- $data = true;
- } else {
- $this->pathAsString .= $this->isIndex[$index] ? "[$element]" : ".$element";
- }
- }
- if ('' !== $this->pathAsString) {
- // remove leading dot
- $this->pathAsString = substr($this->pathAsString, 1);
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/ViolationMapper/ViolationPathIterator.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
- use Symfony\Component\PropertyAccess\PropertyPathIterator;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class ViolationPathIterator extends PropertyPathIterator
- {
- public function __construct(ViolationPath $violationPath)
- {
- parent::__construct($violationPath);
- }
- public function mapsForm(): bool
- {
- return $this->path->mapsForm($this->key());
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/ViolationMapper/MappingRule.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
- use Symfony\Component\Form\Exception\ErrorMappingException;
- use Symfony\Component\Form\FormInterface;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class MappingRule
- {
- public function __construct(
- private FormInterface $origin,
- private string $propertyPath,
- private string $targetPath,
- ) {
- }
- public function getOrigin(): FormInterface
- {
- return $this->origin;
- }
- /**
- * Matches a property path against the rule path.
- *
- * If the rule matches, the form mapped by the rule is returned.
- * Otherwise this method returns false.
- */
- public function match(string $propertyPath): ?FormInterface
- {
- return $propertyPath === $this->propertyPath ? $this->getTarget() : null;
- }
- /**
- * Matches a property path against a prefix of the rule path.
- */
- public function isPrefix(string $propertyPath): bool
- {
- $length = \strlen($propertyPath);
- $prefix = substr($this->propertyPath, 0, $length);
- $next = $this->propertyPath[$length] ?? null;
- return $prefix === $propertyPath && ('[' === $next || '.' === $next);
- }
- /**
- * @throws ErrorMappingException
- */
- public function getTarget(): FormInterface
- {
- $childNames = explode('.', $this->targetPath);
- $target = $this->origin;
- foreach ($childNames as $childName) {
- if (!$target->has($childName)) {
- 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()));
- }
- $target = $target->get($childName);
- }
- return $target;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/ViolationMapper/RelativePath.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\PropertyAccess\PropertyPath;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class RelativePath extends PropertyPath
- {
- public function __construct(
- private FormInterface $root,
- string $propertyPath,
- ) {
- parent::__construct($propertyPath);
- }
- public function getRoot(): FormInterface
- {
- return $this->root;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/ViolationMapper/ViolationMapperInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Validator\ConstraintViolation;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- interface ViolationMapperInterface
- {
- /**
- * Maps a constraint violation to a form in the form tree under
- * the given form.
- *
- * @param bool $allowNonSynchronized Whether to allow mapping to non-synchronized forms
- */
- public function mapViolation(ConstraintViolation $violation, FormInterface $form, bool $allowNonSynchronized = false): void;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/ViolationMapper/ViolationMapper.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
- use Symfony\Component\Form\FileUploadError;
- use Symfony\Component\Form\FormError;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Form\FormRendererInterface;
- use Symfony\Component\Form\Util\InheritDataAwareIterator;
- use Symfony\Component\PropertyAccess\PropertyPathBuilder;
- use Symfony\Component\PropertyAccess\PropertyPathIterator;
- use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
- use Symfony\Component\Validator\Constraints\File;
- use Symfony\Component\Validator\ConstraintViolation;
- use Symfony\Contracts\Translation\TranslatorInterface;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class ViolationMapper implements ViolationMapperInterface
- {
- private bool $allowNonSynchronized = false;
- public function __construct(
- private ?FormRendererInterface $formRenderer = null,
- private ?TranslatorInterface $translator = null,
- ) {
- }
- public function mapViolation(ConstraintViolation $violation, FormInterface $form, bool $allowNonSynchronized = false): void
- {
- $this->allowNonSynchronized = $allowNonSynchronized;
- // The scope is the currently found most specific form that
- // an error should be mapped to. After setting the scope, the
- // mapper will try to continue to find more specific matches in
- // the children of scope. If it cannot, the error will be
- // mapped to this scope.
- $scope = null;
- $violationPath = null;
- $relativePath = null;
- $match = false;
- // Don't create a ViolationPath instance for empty property paths
- if ('' !== $violation->getPropertyPath()) {
- $violationPath = new ViolationPath($violation->getPropertyPath());
- $relativePath = $this->reconstructPath($violationPath, $form);
- }
- // This case happens if the violation path is empty and thus
- // the violation should be mapped to the root form
- if (null === $violationPath) {
- $scope = $form;
- }
- // In general, mapping happens from the root form to the leaf forms
- // First, the rules of the root form are applied to determine
- // the subsequent descendant. The rules of this descendant are then
- // applied to find the next and so on, until we have found the
- // most specific form that matches the violation.
- // If any of the forms found in this process is not synchronized,
- // mapping is aborted. Non-synchronized forms could not reverse
- // transform the value entered by the user, thus any further violations
- // caused by the (invalid) reverse transformed value should be
- // ignored.
- if (null !== $relativePath) {
- // Set the scope to the root of the relative path
- // This root will usually be $form. If the path contains
- // an unmapped form though, the last unmapped form found
- // will be the root of the path.
- $scope = $relativePath->getRoot();
- $it = new PropertyPathIterator($relativePath);
- while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) {
- $scope = $child;
- $it->next();
- $match = true;
- }
- }
- // This case happens if an error happened in the data under a
- // form inheriting its parent data that does not match any of the
- // children of that form.
- if (null !== $violationPath && !$match) {
- // If we could not map the error to anything more specific
- // than the root element, map it to the innermost directly
- // mapped form of the violation path
- // e.g. "children[foo].children[bar].data.baz"
- // Here the innermost directly mapped child is "bar"
- $scope = $form;
- $it = new ViolationPathIterator($violationPath);
- // Note: acceptsErrors() will always return true for forms inheriting
- // their parent data, because these forms can never be non-synchronized
- // (they don't do any data transformation on their own)
- while ($this->acceptsErrors($scope) && $it->valid() && $it->mapsForm()) {
- if (!$scope->has($it->current())) {
- // Break if we find a reference to a non-existing child
- break;
- }
- $scope = $scope->get($it->current());
- $it->next();
- }
- }
- // Follow dot rules until we have the final target
- $mapping = $scope->getConfig()->getOption('error_mapping');
- while ($this->acceptsErrors($scope) && isset($mapping['.'])) {
- $dotRule = new MappingRule($scope, '.', $mapping['.']);
- $scope = $dotRule->getTarget();
- $mapping = $scope->getConfig()->getOption('error_mapping');
- }
- // Only add the error if the form is synchronized
- if ($this->acceptsErrors($scope)) {
- if ($violation->getConstraint() instanceof File && (string) \UPLOAD_ERR_INI_SIZE === $violation->getCode()) {
- $errorsTarget = $scope;
- while (null !== $errorsTarget->getParent() && $errorsTarget->getConfig()->getErrorBubbling()) {
- $errorsTarget = $errorsTarget->getParent();
- }
- $errors = $errorsTarget->getErrors();
- $errorsTarget->clearErrors();
- foreach ($errors as $error) {
- if (!$error instanceof FileUploadError) {
- $errorsTarget->addError($error);
- }
- }
- }
- $message = $violation->getMessage();
- $messageTemplate = $violation->getMessageTemplate();
- if (str_contains($message, '{{ label }}') || str_contains($messageTemplate, '{{ label }}')) {
- $form = $scope;
- do {
- $labelFormat = $form->getConfig()->getOption('label_format');
- } while (null === $labelFormat && null !== $form = $form->getParent());
- if (null !== $labelFormat) {
- $label = str_replace(
- [
- '%name%',
- '%id%',
- ],
- [
- $scope->getName(),
- (string) $scope->getPropertyPath(),
- ],
- $labelFormat
- );
- } else {
- $label = $scope->getConfig()->getOption('label');
- }
- if (false !== $label) {
- if (null === $label && null !== $this->formRenderer) {
- $label = $this->formRenderer->humanize($scope->getName());
- } else {
- $label ??= $scope->getName();
- }
- if (null !== $this->translator) {
- $form = $scope;
- $translationParameters[] = $form->getConfig()->getOption('label_translation_parameters', []);
- do {
- $translationDomain = $form->getConfig()->getOption('translation_domain');
- array_unshift(
- $translationParameters,
- $form->getConfig()->getOption('label_translation_parameters', [])
- );
- } while (null === $translationDomain && null !== $form = $form->getParent());
- $translationParameters = array_merge([], ...$translationParameters);
- $label = $this->translator->trans(
- $label,
- $translationParameters,
- $translationDomain
- );
- }
- $message = str_replace('{{ label }}', $label, $message);
- $messageTemplate = str_replace('{{ label }}', $label, $messageTemplate);
- }
- }
- $scope->addError(new FormError(
- $message,
- $messageTemplate,
- $violation->getParameters(),
- $violation->getPlural(),
- $violation
- ));
- }
- }
- /**
- * Tries to match the beginning of the property path at the
- * current position against the children of the scope.
- *
- * If a matching child is found, it is returned. Otherwise
- * null is returned.
- */
- private function matchChild(FormInterface $form, PropertyPathIteratorInterface $it): ?FormInterface
- {
- $target = null;
- $chunk = '';
- $foundAtIndex = null;
- // Construct mapping rules for the given form
- $rules = [];
- foreach ($form->getConfig()->getOption('error_mapping') as $propertyPath => $targetPath) {
- // Dot rules are considered at the very end
- if ('.' !== $propertyPath) {
- $rules[] = new MappingRule($form, $propertyPath, $targetPath);
- }
- }
- $children = iterator_to_array(new \RecursiveIteratorIterator(new InheritDataAwareIterator($form)), false);
- while ($it->valid()) {
- if ($it->isIndex()) {
- $chunk .= '['.$it->current().']';
- } else {
- $chunk .= ('' === $chunk ? '' : '.').$it->current();
- }
- // Test mapping rules as long as we have any
- foreach ($rules as $key => $rule) {
- /* @var MappingRule $rule */
- // Mapping rule matches completely, terminate.
- if (null !== ($form = $rule->match($chunk))) {
- return $form;
- }
- // Keep only rules that have $chunk as prefix
- if (!$rule->isPrefix($chunk)) {
- unset($rules[$key]);
- }
- }
- /** @var FormInterface $child */
- foreach ($children as $i => $child) {
- $childPath = (string) $child->getPropertyPath();
- if ($childPath === $chunk) {
- $target = $child;
- $foundAtIndex = $it->key();
- } elseif (str_starts_with($childPath, $chunk)) {
- continue;
- }
- unset($children[$i]);
- }
- $it->next();
- }
- if (null !== $foundAtIndex) {
- $it->seek($foundAtIndex);
- }
- return $target;
- }
- /**
- * Reconstructs a property path from a violation path and a form tree.
- */
- private function reconstructPath(ViolationPath $violationPath, FormInterface $origin): ?RelativePath
- {
- $propertyPathBuilder = new PropertyPathBuilder($violationPath);
- $it = $violationPath->getIterator();
- $scope = $origin;
- // Remember the current index in the builder
- $i = 0;
- // Expand elements that map to a form (like "children[address]")
- for ($it->rewind(); $it->valid() && $it->mapsForm(); $it->next()) {
- if (!$scope->has($it->current())) {
- // Scope relates to a form that does not exist
- // Bail out
- break;
- }
- // Process child form
- $scope = $scope->get($it->current());
- if ($scope->getConfig()->getInheritData()) {
- // Form inherits its parent data
- // Cut the piece out of the property path and proceed
- $propertyPathBuilder->remove($i);
- } else {
- /* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */
- $propertyPath = $scope->getPropertyPath();
- if (null === $propertyPath) {
- // Property path of a mapped form is null
- // Should not happen, bail out
- break;
- }
- $propertyPathBuilder->replace($i, 1, $propertyPath);
- $i += $propertyPath->getLength();
- }
- }
- $finalPath = $propertyPathBuilder->getPropertyPath();
- return null !== $finalPath ? new RelativePath($origin, $finalPath) : null;
- }
- private function acceptsErrors(FormInterface $form): bool
- {
- return $this->allowNonSynchronized || $form->isSynchronized();
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/Constraints/FormValidator.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\Constraints;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\Validator\Constraint;
- use Symfony\Component\Validator\Constraints\Composite;
- use Symfony\Component\Validator\Constraints\GroupSequence;
- use Symfony\Component\Validator\Constraints\Valid;
- use Symfony\Component\Validator\ConstraintValidator;
- use Symfony\Component\Validator\Exception\UnexpectedTypeException;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class FormValidator extends ConstraintValidator
- {
- /**
- * @var \SplObjectStorage<FormInterface, array<int, string|string[]|GroupSequence>>
- */
- private \SplObjectStorage $resolvedGroups;
- public function validate(mixed $form, Constraint $formConstraint): void
- {
- if (!$formConstraint instanceof Form) {
- throw new UnexpectedTypeException($formConstraint, Form::class);
- }
- if (!$form instanceof FormInterface) {
- return;
- }
- /* @var FormInterface $form */
- $config = $form->getConfig();
- $validator = $this->context->getValidator()->inContext($this->context);
- if ($form->isSubmitted() && $form->isSynchronized()) {
- // Validate the form data only if transformation succeeded
- $groups = $this->getValidationGroups($form);
- if (!$groups) {
- return;
- }
- $data = $form->getData();
- // Validate the data against its own constraints
- $validateDataGraph = $form->isRoot()
- && (\is_object($data) || \is_array($data))
- && (\is_array($groups) || ($groups instanceof GroupSequence && $groups->groups))
- ;
- // Validate the data against the constraints defined in the form
- /** @var Constraint[] $constraints */
- $constraints = $config->getOption('constraints', []);
- $hasChildren = $form->count() > 0;
- if ($hasChildren && $form->isRoot()) {
- $this->resolvedGroups = new \SplObjectStorage();
- }
- if ($groups instanceof GroupSequence) {
- // Validate the data, the form AND nested fields in sequence
- $violationsCount = $this->context->getViolations()->count();
- foreach ($groups->groups as $group) {
- if ($validateDataGraph) {
- $validator->atPath('data')->validate($data, null, $group);
- }
- if ($groupedConstraints = self::getConstraintsInGroups($constraints, $group)) {
- $validator->atPath('data')->validate($data, $groupedConstraints, $group);
- }
- foreach ($form->all() as $field) {
- if ($field->isSubmitted()) {
- // remember to validate this field in one group only
- // otherwise resolving the groups would reuse the same
- // sequence recursively, thus some fields could fail
- // in different steps without breaking early enough
- $this->resolvedGroups[$field] = (array) $group;
- $fieldFormConstraint = new Form();
- $fieldFormConstraint->groups = $group;
- $this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath());
- $validator->atPath(\sprintf('children[%s]', $field->getName()))->validate($field, $fieldFormConstraint, $group);
- }
- }
- if ($violationsCount < $this->context->getViolations()->count()) {
- break;
- }
- }
- } else {
- if ($validateDataGraph) {
- $validator->atPath('data')->validate($data, null, $groups);
- }
- $groupedConstraints = [];
- foreach ($constraints as $constraint) {
- // For the "Valid" constraint, validate the data in all groups
- if ($constraint instanceof Valid) {
- if (\is_object($data) || \is_array($data)) {
- $validator->atPath('data')->validate($data, $constraint, $groups);
- }
- continue;
- }
- // Otherwise validate a constraint only once for the first
- // matching group
- foreach ($groups as $group) {
- if (\in_array($group, $constraint->groups, true)) {
- $groupedConstraints[$group][] = $constraint;
- // Prevent duplicate validation
- if (!$constraint instanceof Composite) {
- continue 2;
- }
- }
- }
- }
- foreach ($groupedConstraints as $group => $constraint) {
- $validator->atPath('data')->validate($data, $constraint, $group);
- }
- foreach ($form->all() as $field) {
- if ($field->isSubmitted()) {
- $this->resolvedGroups[$field] = $groups;
- $this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath());
- $validator->atPath(\sprintf('children[%s]', $field->getName()))->validate($field, $formConstraint);
- }
- }
- }
- if ($hasChildren && $form->isRoot()) {
- // destroy storage to avoid memory leaks
- $this->resolvedGroups = new \SplObjectStorage();
- }
- } elseif (!$form->isSynchronized()) {
- $childrenSynchronized = true;
- /** @var FormInterface $child */
- foreach ($form as $child) {
- if (!$child->isSynchronized()) {
- $childrenSynchronized = false;
- $this->context->setNode($this->context->getValue(), $child, $this->context->getMetadata(), $this->context->getPropertyPath());
- $validator->atPath(\sprintf('children[%s]', $child->getName()))->validate($child, $formConstraint);
- }
- }
- // Mark the form with an error if it is not synchronized BUT all
- // of its children are synchronized. If any child is not
- // synchronized, an error is displayed there already and showing
- // a second error in its parent form is pointless, or worse, may
- // lead to duplicate errors if error bubbling is enabled on the
- // child.
- // See also https://github.com/symfony/symfony/issues/4359
- if ($childrenSynchronized) {
- $clientDataAsString = \is_scalar($form->getViewData())
- ? (string) $form->getViewData()
- : get_debug_type($form->getViewData());
- $failure = $form->getTransformationFailure();
- $this->context->setConstraint($formConstraint);
- $this->context->buildViolation($failure->getInvalidMessage() ?? $config->getOption('invalid_message'))
- ->setParameters(array_replace(
- ['{{ value }}' => $clientDataAsString],
- $config->getOption('invalid_message_parameters'),
- $failure->getInvalidMessageParameters()
- ))
- ->setInvalidValue($form->getViewData())
- ->setCode(Form::NOT_SYNCHRONIZED_ERROR)
- ->setCause($failure)
- ->addViolation();
- }
- }
- // Mark the form with an error if it contains extra fields
- if (!$config->getOption('allow_extra_fields') && \count($form->getExtraData()) > 0) {
- $this->context->setConstraint($formConstraint);
- $this->context->buildViolation($config->getOption('extra_fields_message', ''))
- ->setParameter('{{ extra_fields }}', '"'.implode('", "', array_keys($form->getExtraData())).'"')
- ->setPlural(\count($form->getExtraData()))
- ->setInvalidValue($form->getExtraData())
- ->setCode(Form::NO_SUCH_FIELD_ERROR)
- ->addViolation();
- }
- }
- /**
- * Returns the validation groups of the given form.
- *
- * @return string|GroupSequence|array<string|GroupSequence>
- */
- private function getValidationGroups(FormInterface $form): string|GroupSequence|array
- {
- // Determine the clicked button of the complete form tree
- $clickedButton = null;
- if (method_exists($form, 'getClickedButton')) {
- $clickedButton = $form->getClickedButton();
- }
- if (null !== $clickedButton) {
- $groups = $clickedButton->getConfig()->getOption('validation_groups');
- if (null !== $groups) {
- return self::resolveValidationGroups($groups, $form);
- }
- }
- do {
- $groups = $form->getConfig()->getOption('validation_groups');
- if (null !== $groups) {
- return self::resolveValidationGroups($groups, $form);
- }
- if (isset($this->resolvedGroups[$form])) {
- return $this->resolvedGroups[$form];
- }
- $form = $form->getParent();
- } while (null !== $form);
- return [Constraint::DEFAULT_GROUP];
- }
- /**
- * Post-processes the validation groups option for a given form.
- *
- * @param string|GroupSequence|array<string|GroupSequence>|callable $groups The validation groups
- *
- * @return GroupSequence|array<string|GroupSequence>
- */
- private static function resolveValidationGroups(string|GroupSequence|array|callable $groups, FormInterface $form): GroupSequence|array
- {
- if (!\is_string($groups) && \is_callable($groups)) {
- $groups = $groups($form);
- }
- if ($groups instanceof GroupSequence) {
- return $groups;
- }
- return (array) $groups;
- }
- private static function getConstraintsInGroups(array $constraints, string|array $group): array
- {
- $groups = (array) $group;
- return array_filter($constraints, static function (Constraint $constraint) use ($groups) {
- foreach ($groups as $group) {
- if (\in_array($group, $constraint->groups, true)) {
- return true;
- }
- }
- return false;
- });
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/Constraints/Form.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\Constraints;
- use Symfony\Component\Validator\Constraint;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class Form extends Constraint
- {
- public const NOT_SYNCHRONIZED_ERROR = '1dafa156-89e1-4736-b832-419c2e501fca';
- public const NO_SUCH_FIELD_ERROR = '6e5212ed-a197-4339-99aa-5654798a4854';
- protected const ERROR_NAMES = [
- self::NOT_SYNCHRONIZED_ERROR => 'NOT_SYNCHRONIZED_ERROR',
- self::NO_SUCH_FIELD_ERROR => 'NO_SUCH_FIELD_ERROR',
- ];
- public function getTargets(): string|array
- {
- return self::CLASS_CONSTRAINT;
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/EventListener/ValidationListener.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\EventListener;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- use Symfony\Component\Form\Extension\Validator\Constraints\Form;
- use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\Validator\Validator\ValidatorInterface;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class ValidationListener implements EventSubscriberInterface
- {
- public static function getSubscribedEvents(): array
- {
- return [FormEvents::POST_SUBMIT => 'validateForm'];
- }
- public function __construct(
- private ValidatorInterface $validator,
- private ViolationMapperInterface $violationMapper,
- ) {
- }
- public function validateForm(FormEvent $event): void
- {
- $form = $event->getForm();
- if ($form->isRoot()) {
- // Form groups are validated internally (FormValidator). Here we don't set groups as they are retrieved into the validator.
- foreach ($this->validator->validate($form) as $violation) {
- // Allow the "invalid" constraint to be put onto
- // non-synchronized forms
- $allowNonSynchronized = $violation->getConstraint() instanceof Form && Form::NOT_SYNCHRONIZED_ERROR === $violation->getCode();
- $this->violationMapper->mapViolation($violation, $form, $allowNonSynchronized);
- }
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/Type/SubmitTypeValidatorExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\Type;
- use Symfony\Component\Form\Extension\Core\Type\SubmitType;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class SubmitTypeValidatorExtension extends BaseValidatorExtension
- {
- public static function getExtendedTypes(): iterable
- {
- return [SubmitType::class];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/Type/FormTypeValidatorExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\Type;
- use Symfony\Component\Form\Extension\Core\Type\FormType;
- use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
- use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormRendererInterface;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- use Symfony\Component\Validator\Constraint;
- use Symfony\Component\Validator\Validator\ValidatorInterface;
- use Symfony\Contracts\Translation\TranslatorInterface;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class FormTypeValidatorExtension extends BaseValidatorExtension
- {
- private ViolationMapper $violationMapper;
- public function __construct(
- private ValidatorInterface $validator,
- private bool $legacyErrorMessages = true,
- ?FormRendererInterface $formRenderer = null,
- ?TranslatorInterface $translator = null,
- ) {
- $this->violationMapper = new ViolationMapper($formRenderer, $translator);
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $builder->addEventSubscriber(new ValidationListener($this->validator, $this->violationMapper));
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- parent::configureOptions($resolver);
- // Constraint should always be converted to an array
- $constraintsNormalizer = static fn (Options $options, $constraints) => \is_object($constraints) ? [$constraints] : (array) $constraints;
- $resolver->setDefaults([
- 'error_mapping' => [],
- 'constraints' => [],
- 'invalid_message' => 'This value is not valid.',
- 'invalid_message_parameters' => [],
- 'allow_extra_fields' => false,
- 'extra_fields_message' => 'This form should not contain extra fields.',
- ]);
- $resolver->setAllowedTypes('constraints', [Constraint::class, Constraint::class.'[]']);
- $resolver->setNormalizer('constraints', $constraintsNormalizer);
- }
- public static function getExtendedTypes(): iterable
- {
- return [FormType::class];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/Type/BaseValidatorExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\Type;
- use Symfony\Component\Form\AbstractTypeExtension;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- use Symfony\Component\Validator\Constraints\GroupSequence;
- /**
- * Encapsulates common logic of {@link FormTypeValidatorExtension} and
- * {@link SubmitTypeValidatorExtension}.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- abstract class BaseValidatorExtension extends AbstractTypeExtension
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- // Make sure that validation groups end up as null, closure or array
- $validationGroupsNormalizer = static function (Options $options, $groups) {
- if (false === $groups) {
- return [];
- }
- if (!$groups) {
- return null;
- }
- if (\is_callable($groups)) {
- return $groups;
- }
- if ($groups instanceof GroupSequence) {
- return $groups;
- }
- return (array) $groups;
- };
- $resolver->setDefaults([
- 'validation_groups' => null,
- ]);
- $resolver->setNormalizer('validation_groups', $validationGroupsNormalizer);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/Type/RepeatedTypeValidatorExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\Type;
- use Symfony\Component\Form\AbstractTypeExtension;
- use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class RepeatedTypeValidatorExtension extends AbstractTypeExtension
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- // Map errors to the first field
- $errorMapping = static fn (Options $options) => ['.' => $options['first_name']];
- $resolver->setDefaults([
- 'error_mapping' => $errorMapping,
- ]);
- }
- public static function getExtendedTypes(): iterable
- {
- return [RepeatedType::class];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/Validator/Type/UploadValidatorExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\Validator\Type;
- use Symfony\Component\Form\AbstractTypeExtension;
- use Symfony\Component\Form\Extension\Core\Type\FormType;
- use Symfony\Component\OptionsResolver\Options;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- use Symfony\Contracts\Translation\TranslatorInterface;
- /**
- * @author Abdellatif Ait boudad <[email protected]>
- * @author David Badura <[email protected]>
- */
- class UploadValidatorExtension extends AbstractTypeExtension
- {
- public function __construct(
- private TranslatorInterface $translator,
- private ?string $translationDomain = null,
- ) {
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $translator = $this->translator;
- $translationDomain = $this->translationDomain;
- $resolver->setNormalizer('upload_max_size_message', static fn (Options $options, $message) => static fn () => $translator->trans($message(), [], $translationDomain));
- }
- public static function getExtendedTypes(): iterable
- {
- return [FormType::class];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/PasswordHasher/Type/PasswordTypePasswordHasherExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\PasswordHasher\Type;
- use Symfony\Component\Form\AbstractTypeExtension;
- use Symfony\Component\Form\Extension\Core\Type\PasswordType;
- use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormEvents;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- use Symfony\Component\PropertyAccess\PropertyPath;
- /**
- * @author Sébastien Alfaiate <[email protected]>
- */
- class PasswordTypePasswordHasherExtension extends AbstractTypeExtension
- {
- public function __construct(
- private PasswordHasherListener $passwordHasherListener,
- ) {
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- if ($options['hash_property_path']) {
- $builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'registerPassword']);
- }
- }
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'hash_property_path' => null,
- ]);
- $resolver->setAllowedTypes('hash_property_path', ['null', 'string', PropertyPath::class]);
- $resolver->setInfo('hash_property_path', 'A valid PropertyAccess syntax where the hashed password will be set.');
- }
- public static function getExtendedTypes(): iterable
- {
- return [PasswordType::class];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/PasswordHasher/Type/FormTypePasswordHasherExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\PasswordHasher\Type;
- use Symfony\Component\Form\AbstractTypeExtension;
- use Symfony\Component\Form\Extension\Core\Type\FormType;
- use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
- use Symfony\Component\Form\FormBuilderInterface;
- use Symfony\Component\Form\FormEvents;
- /**
- * @author Sébastien Alfaiate <[email protected]>
- */
- class FormTypePasswordHasherExtension extends AbstractTypeExtension
- {
- public function __construct(
- private PasswordHasherListener $passwordHasherListener,
- ) {
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $builder->addEventListener(FormEvents::POST_SUBMIT, [$this->passwordHasherListener, 'hashPasswords']);
- }
- public static function getExtendedTypes(): iterable
- {
- return [FormType::class];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/PasswordHasher/EventListener/PasswordHasherListener.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\PasswordHasher\EventListener;
- use Symfony\Component\Form\Exception\InvalidConfigurationException;
- use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
- use Symfony\Component\Form\FormEvent;
- use Symfony\Component\Form\FormInterface;
- use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
- use Symfony\Component\PropertyAccess\PropertyAccess;
- use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
- use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
- /**
- * @author Sébastien Alfaiate <[email protected]>
- * @author Gábor Egyed <[email protected]>
- */
- class PasswordHasherListener
- {
- private array $passwords = [];
- public function __construct(
- private UserPasswordHasherInterface $passwordHasher,
- private ?PropertyAccessorInterface $propertyAccessor = null,
- ) {
- $this->propertyAccessor ??= PropertyAccess::createPropertyAccessor();
- }
- public function registerPassword(FormEvent $event): void
- {
- if (null === $event->getData() || '' === $event->getData()) {
- return;
- }
- $this->assertNotMapped($event->getForm());
- $this->passwords[] = [
- 'form' => $event->getForm(),
- 'property_path' => $event->getForm()->getConfig()->getOption('hash_property_path'),
- 'password' => $event->getData(),
- ];
- }
- public function hashPasswords(FormEvent $event): void
- {
- $form = $event->getForm();
- if (!$form->isRoot()) {
- return;
- }
- if ($form->isValid()) {
- foreach ($this->passwords as $password) {
- $user = $this->getUser($password['form']);
- $this->propertyAccessor->setValue(
- $user,
- $password['property_path'],
- $this->passwordHasher->hashPassword($user, $password['password'])
- );
- }
- }
- $this->passwords = [];
- }
- private function getTargetForm(FormInterface $form): FormInterface
- {
- if (!$parentForm = $form->getParent()) {
- return $form;
- }
- $parentType = $parentForm->getConfig()->getType();
- do {
- if ($parentType->getInnerType() instanceof RepeatedType) {
- return $parentForm;
- }
- } while ($parentType = $parentType->getParent());
- return $form;
- }
- private function getUser(FormInterface $form): PasswordAuthenticatedUserInterface
- {
- $parent = $this->getTargetForm($form)->getParent();
- if (!($user = $parent?->getData()) || !$user instanceof PasswordAuthenticatedUserInterface) {
- throw new InvalidConfigurationException(\sprintf('The "hash_property_path" option only supports "%s" objects, "%s" given.', PasswordAuthenticatedUserInterface::class, get_debug_type($user)));
- }
- return $user;
- }
- private function assertNotMapped(FormInterface $form): void
- {
- if ($this->getTargetForm($form)->getConfig()->getMapped()) {
- throw new InvalidConfigurationException('The "hash_property_path" option cannot be used on mapped field.');
- }
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./Extension/PasswordHasher/PasswordHasherExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form\Extension\PasswordHasher;
- use Symfony\Component\Form\AbstractExtension;
- use Symfony\Component\Form\Extension\PasswordHasher\EventListener\PasswordHasherListener;
- /**
- * Integrates the PasswordHasher component with the Form library.
- *
- * @author Sébastien Alfaiate <[email protected]>
- */
- class PasswordHasherExtension extends AbstractExtension
- {
- public function __construct(
- private PasswordHasherListener $passwordHasherListener,
- ) {
- }
- protected function loadTypeExtensions(): array
- {
- return [
- new Type\FormTypePasswordHasherExtension($this->passwordHasherListener),
- new Type\PasswordTypePasswordHasherExtension($this->passwordHasherListener),
- ];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./AbstractTypeExtension.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\OptionsResolver\OptionsResolver;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- abstract class AbstractTypeExtension implements FormTypeExtensionInterface
- {
- public function configureOptions(OptionsResolver $resolver): void
- {
- }
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- }
- public function buildView(FormView $view, FormInterface $form, array $options): void
- {
- }
- public function finishView(FormView $view, FormInterface $form, array $options): void
- {
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormFactoryInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Extension\Core\Type\FormType;
- use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
- /**
- * Allows creating a form based on a name, a class or a property.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormFactoryInterface
- {
- /**
- * Returns a form.
- *
- * @see createBuilder()
- *
- * @param mixed $data The initial data
- *
- * @throws InvalidOptionsException if any given option is not applicable to the given type
- */
- public function create(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface;
- /**
- * Returns a form.
- *
- * @see createNamedBuilder()
- *
- * @param mixed $data The initial data
- *
- * @throws InvalidOptionsException if any given option is not applicable to the given type
- */
- public function createNamed(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormInterface;
- /**
- * Returns a form for a property of a class.
- *
- * @see createBuilderForProperty()
- *
- * @param string $class The fully qualified class name
- * @param string $property The name of the property to guess for
- * @param mixed $data The initial data
- *
- * @throws InvalidOptionsException if any given option is not applicable to the form type
- */
- public function createForProperty(string $class, string $property, mixed $data = null, array $options = []): FormInterface;
- /**
- * Returns a form builder.
- *
- * @param mixed $data The initial data
- *
- * @throws InvalidOptionsException if any given option is not applicable to the given type
- */
- public function createBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface;
- /**
- * Returns a form builder.
- *
- * @param mixed $data The initial data
- *
- * @throws InvalidOptionsException if any given option is not applicable to the given type
- */
- public function createNamedBuilder(string $name, string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface;
- /**
- * Returns a form builder for a property of a class.
- *
- * If any of the 'required' and type options can be guessed,
- * and are not provided in the options argument, the guessed value is used.
- *
- * @param string $class The fully qualified class name
- * @param string $property The name of the property to guess for
- * @param mixed $data The initial data
- *
- * @throws InvalidOptionsException if any given option is not applicable to the form type
- */
- public function createBuilderForProperty(string $class, string $property, mixed $data = null, array $options = []): FormBuilderInterface;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormBuilder.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\EventDispatcher\EventDispatcherInterface;
- use Symfony\Component\Form\Exception\BadMethodCallException;
- use Symfony\Component\Form\Exception\InvalidArgumentException;
- use Symfony\Component\Form\Extension\Core\Type\TextType;
- /**
- * A builder for creating {@link Form} instances.
- *
- * @author Bernhard Schussek <[email protected]>
- *
- * @implements \IteratorAggregate<string, FormBuilderInterface>
- */
- class FormBuilder extends FormConfigBuilder implements \IteratorAggregate, FormBuilderInterface
- {
- /**
- * The children of the form builder.
- *
- * @var FormBuilderInterface[]
- */
- private array $children = [];
- /**
- * The data of children who haven't been converted to form builders yet.
- */
- private array $unresolvedChildren = [];
- public function __construct(?string $name, ?string $dataClass, EventDispatcherInterface $dispatcher, FormFactoryInterface $factory, array $options = [])
- {
- parent::__construct($name, $dataClass, $dispatcher, $options);
- $this->setFormFactory($factory);
- }
- public function add(FormBuilderInterface|string $child, ?string $type = null, array $options = []): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- if ($child instanceof FormBuilderInterface) {
- $this->children[$child->getName()] = $child;
- // In case an unresolved child with the same name exists
- unset($this->unresolvedChildren[$child->getName()]);
- return $this;
- }
- // Add to "children" to maintain order
- $this->children[$child] = null;
- $this->unresolvedChildren[$child] = [$type, $options];
- return $this;
- }
- public function create(string $name, ?string $type = null, array $options = []): FormBuilderInterface
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- if (null === $type && null === $this->getDataClass()) {
- $type = TextType::class;
- }
- if (null !== $type) {
- return $this->getFormFactory()->createNamedBuilder($name, $type, null, $options);
- }
- return $this->getFormFactory()->createBuilderForProperty($this->getDataClass(), $name, null, $options);
- }
- public function get(string $name): FormBuilderInterface
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- if (isset($this->unresolvedChildren[$name])) {
- return $this->resolveChild($name);
- }
- if (isset($this->children[$name])) {
- return $this->children[$name];
- }
- throw new InvalidArgumentException(\sprintf('The child with the name "%s" does not exist.', $name));
- }
- public function remove(string $name): static
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- unset($this->unresolvedChildren[$name], $this->children[$name]);
- return $this;
- }
- public function has(string $name): bool
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- return isset($this->unresolvedChildren[$name]) || isset($this->children[$name]);
- }
- public function all(): array
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->resolveChildren();
- return $this->children;
- }
- public function count(): int
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- return \count($this->children);
- }
- public function getFormConfig(): FormConfigInterface
- {
- /** @var self $config */
- $config = parent::getFormConfig();
- $config->children = [];
- $config->unresolvedChildren = [];
- return $config;
- }
- public function getForm(): FormInterface
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- $this->resolveChildren();
- $form = new Form($this->getFormConfig());
- foreach ($this->children as $child) {
- // Automatic initialization is only supported on root forms
- $form->add($child->setAutoInitialize(false)->getForm());
- }
- if ($this->getAutoInitialize()) {
- // Automatically initialize the form if it is configured so
- $form->initialize();
- }
- return $form;
- }
- /**
- * @return \Traversable<string, FormBuilderInterface>
- */
- public function getIterator(): \Traversable
- {
- if ($this->locked) {
- throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
- }
- return new \ArrayIterator($this->all());
- }
- /**
- * Converts an unresolved child into a {@link FormBuilderInterface} instance.
- */
- private function resolveChild(string $name): FormBuilderInterface
- {
- [$type, $options] = $this->unresolvedChildren[$name];
- unset($this->unresolvedChildren[$name]);
- return $this->children[$name] = $this->create($name, $type, $options);
- }
- /**
- * Converts all unresolved children into {@link FormBuilder} instances.
- */
- private function resolveChildren(): void
- {
- foreach ($this->unresolvedChildren as $name => $info) {
- $this->children[$name] = $this->create($name, $info[0], $info[1]);
- }
- $this->unresolvedChildren = [];
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormRenderer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- use Symfony\Component\Form\Exception\BadMethodCallException;
- use Symfony\Component\Form\Exception\LogicException;
- use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
- use Twig\Environment;
- /**
- * Renders a form into HTML using a rendering engine.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class FormRenderer implements FormRendererInterface
- {
- public const CACHE_KEY_VAR = 'unique_block_prefix';
- private array $blockNameHierarchyMap = [];
- private array $hierarchyLevelMap = [];
- private array $variableStack = [];
- public function __construct(
- private FormRendererEngineInterface $engine,
- private ?CsrfTokenManagerInterface $csrfTokenManager = null,
- ) {
- }
- public function getEngine(): FormRendererEngineInterface
- {
- return $this->engine;
- }
- public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void
- {
- $this->engine->setTheme($view, $themes, $useDefaultThemes);
- }
- public function renderCsrfToken(string $tokenId): string
- {
- if (null === $this->csrfTokenManager) {
- throw new BadMethodCallException('CSRF tokens can only be generated if a CsrfTokenManagerInterface is injected in FormRenderer::__construct(). Try running "composer require symfony/security-csrf".');
- }
- return $this->csrfTokenManager->getToken($tokenId)->getValue();
- }
- public function renderBlock(FormView $view, string $blockName, array $variables = []): string
- {
- $resource = $this->engine->getResourceForBlockName($view, $blockName);
- if (!$resource) {
- throw new LogicException(\sprintf('No block "%s" found while rendering the form.', $blockName));
- }
- $viewCacheKey = $view->vars[self::CACHE_KEY_VAR];
- // The variables are cached globally for a view (instead of for the
- // current suffix)
- if (!isset($this->variableStack[$viewCacheKey])) {
- $this->variableStack[$viewCacheKey] = [];
- // The default variable scope contains all view variables, merged with
- // the variables passed explicitly to the helper
- $scopeVariables = $view->vars;
- $varInit = true;
- } else {
- // Reuse the current scope and merge it with the explicitly passed variables
- $scopeVariables = end($this->variableStack[$viewCacheKey]);
- $varInit = false;
- }
- // Merge the passed with the existing attributes
- if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
- $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
- }
- // Merge the passed with the exist *label* attributes
- if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
- $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
- }
- // Do not use array_replace_recursive(), otherwise array variables
- // cannot be overwritten
- $variables = array_replace($scopeVariables, $variables);
- $this->variableStack[$viewCacheKey][] = $variables;
- // Do the rendering
- $html = $this->engine->renderBlock($view, $resource, $blockName, $variables);
- // Clear the stack
- array_pop($this->variableStack[$viewCacheKey]);
- if ($varInit) {
- unset($this->variableStack[$viewCacheKey]);
- }
- return $html;
- }
- public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, array $variables = []): string
- {
- $renderOnlyOnce = 'row' === $blockNameSuffix || 'widget' === $blockNameSuffix;
- if ($renderOnlyOnce && $view->isRendered()) {
- // This is not allowed, because it would result in rendering same IDs multiple times, which is not valid.
- 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']));
- }
- // The cache key for storing the variables and types
- $viewCacheKey = $view->vars[self::CACHE_KEY_VAR];
- $viewAndSuffixCacheKey = $viewCacheKey.$blockNameSuffix;
- // In templates, we have to deal with two kinds of block hierarchies:
- //
- // +---------+ +---------+
- // | Theme B | -------> | Theme A |
- // +---------+ +---------+
- //
- // form_widget -------> form_widget
- // ^
- // |
- // choice_widget -----> choice_widget
- //
- // The first kind of hierarchy is the theme hierarchy. This allows to
- // override the block "choice_widget" from Theme A in the extending
- // Theme B. This kind of inheritance needs to be supported by the
- // template engine and, for example, offers "parent()" or similar
- // functions to fall back from the custom to the parent implementation.
- //
- // The second kind of hierarchy is the form type hierarchy. This allows
- // to implement a custom "choice_widget" block (no matter in which theme),
- // or to fallback to the block of the parent type, which would be
- // "form_widget" in this example (again, no matter in which theme).
- // If the designer wants to explicitly fallback to "form_widget" in their
- // custom "choice_widget", for example because they only want to wrap
- // a <div> around the original implementation, they can call the
- // widget() function again to render the block for the parent type.
- //
- // The second kind is implemented in the following blocks.
- if (!isset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey])) {
- // INITIAL CALL
- // Calculate the hierarchy of template blocks and start on
- // the bottom level of the hierarchy (= "_<id>_<section>" block)
- $blockNameHierarchy = [];
- foreach ($view->vars['block_prefixes'] as $blockNamePrefix) {
- $blockNameHierarchy[] = $blockNamePrefix.'_'.$blockNameSuffix;
- }
- $hierarchyLevel = \count($blockNameHierarchy) - 1;
- $hierarchyInit = true;
- } else {
- // RECURSIVE CALL
- // If a block recursively calls searchAndRenderBlock() again, resume rendering
- // using the parent type in the hierarchy.
- $blockNameHierarchy = $this->blockNameHierarchyMap[$viewAndSuffixCacheKey];
- $hierarchyLevel = $this->hierarchyLevelMap[$viewAndSuffixCacheKey] - 1;
- $hierarchyInit = false;
- }
- // The variables are cached globally for a view (instead of for the
- // current suffix)
- if (!isset($this->variableStack[$viewCacheKey])) {
- $this->variableStack[$viewCacheKey] = [];
- // The default variable scope contains all view variables, merged with
- // the variables passed explicitly to the helper
- $scopeVariables = $view->vars;
- $varInit = true;
- } else {
- // Reuse the current scope and merge it with the explicitly passed variables
- $scopeVariables = end($this->variableStack[$viewCacheKey]);
- $varInit = false;
- }
- // Load the resource where this block can be found
- $resource = $this->engine->getResourceForBlockNameHierarchy($view, $blockNameHierarchy, $hierarchyLevel);
- // Update the current hierarchy level to the one at which the resource was
- // found. For example, if looking for "choice_widget", but only a resource
- // is found for its parent "form_widget", then the level is updated here
- // to the parent level.
- $hierarchyLevel = $this->engine->getResourceHierarchyLevel($view, $blockNameHierarchy, $hierarchyLevel);
- // The actually existing block name in $resource
- $blockName = $blockNameHierarchy[$hierarchyLevel];
- // Escape if no resource exists for this block
- if (!$resource) {
- if (\count($blockNameHierarchy) !== \count(array_unique($blockNameHierarchy))) {
- throw new LogicException(\sprintf('Unable to render the form because the block names array contains duplicates: "%s".', implode('", "', array_reverse($blockNameHierarchy))));
- }
- throw new LogicException(\sprintf('Unable to render the form as none of the following blocks exist: "%s".', implode('", "', array_reverse($blockNameHierarchy))));
- }
- // Merge the passed with the existing attributes
- if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
- $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
- }
- // Merge the passed with the exist *label* attributes
- if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
- $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
- }
- // Do not use array_replace_recursive(), otherwise array variables
- // cannot be overwritten
- $variables = array_replace($scopeVariables, $variables);
- // In order to make recursive calls possible, we need to store the block hierarchy,
- // the current level of the hierarchy and the variables so that this method can
- // resume rendering one level higher of the hierarchy when it is called recursively.
- //
- // We need to store these values in maps (associative arrays) because within a
- // call to widget() another call to widget() can be made, but for a different view
- // object. These nested calls should not override each other.
- $this->blockNameHierarchyMap[$viewAndSuffixCacheKey] = $blockNameHierarchy;
- $this->hierarchyLevelMap[$viewAndSuffixCacheKey] = $hierarchyLevel;
- // We also need to store the variables for the view so that we can render other
- // blocks for the same view using the same variables as in the outer block.
- $this->variableStack[$viewCacheKey][] = $variables;
- // Do the rendering
- $html = $this->engine->renderBlock($view, $resource, $blockName, $variables);
- // Clear the stack
- array_pop($this->variableStack[$viewCacheKey]);
- // Clear the caches if they were filled for the first time within
- // this function call
- if ($hierarchyInit) {
- unset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey], $this->hierarchyLevelMap[$viewAndSuffixCacheKey]);
- }
- if ($varInit) {
- unset($this->variableStack[$viewCacheKey]);
- }
- if ($renderOnlyOnce) {
- $view->setRendered();
- }
- return $html;
- }
- public function humanize(string $text): string
- {
- return ucfirst(strtolower(trim(preg_replace(['/([A-Z])/', '/[_\s]+/'], ['_$1', ' '], $text))));
- }
- /**
- * @internal
- */
- public function encodeCurrency(Environment $environment, string $text, string $widget = ''): string
- {
- if ('UTF-8' === $charset = $environment->getCharset()) {
- $text = htmlspecialchars($text, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
- } else {
- $text = htmlentities($text, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
- $text = iconv('UTF-8', $charset, $text);
- $widget = iconv('UTF-8', $charset, $widget);
- }
- return str_replace('{{ widget }}', $widget, $text);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./ResolvedFormTypeFactory.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * @author Bernhard Schussek <[email protected]>
- */
- class ResolvedFormTypeFactory implements ResolvedFormTypeFactoryInterface
- {
- public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface
- {
- return new ResolvedFormType($type, $typeExtensions, $parent);
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./SubmitButtonBuilder.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * A builder for {@link SubmitButton} instances.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- class SubmitButtonBuilder extends ButtonBuilder
- {
- /**
- * Creates the button.
- */
- public function getForm(): SubmitButton
- {
- return new SubmitButton($this->getFormConfig());
- }
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormExtensionInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * Interface for extensions which provide types, type extensions and a guesser.
- */
- interface FormExtensionInterface
- {
- /**
- * Returns a type by name.
- *
- * @param string $name The name of the type
- *
- * @throws Exception\InvalidArgumentException if the given type is not supported by this extension
- */
- public function getType(string $name): FormTypeInterface;
- /**
- * Returns whether the given type is supported.
- *
- * @param string $name The name of the type
- */
- public function hasType(string $name): bool;
- /**
- * Returns the extensions for the given type.
- *
- * @param string $name The name of the type
- *
- * @return FormTypeExtensionInterface[]
- */
- public function getTypeExtensions(string $name): array;
- /**
- * Returns whether this extension provides type extensions for the given type.
- *
- * @param string $name The name of the type
- */
- public function hasTypeExtensions(string $name): bool;
- /**
- * Returns the type guesser provided by this extension.
- */
- public function getTypeGuesser(): ?FormTypeGuesserInterface;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FormRendererInterface.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * Renders a form into HTML.
- *
- * @author Bernhard Schussek <[email protected]>
- */
- interface FormRendererInterface
- {
- /**
- * Returns the engine used by this renderer.
- */
- public function getEngine(): FormRendererEngineInterface;
- /**
- * Sets the theme(s) to be used for rendering a view and its children.
- *
- * @param FormView $view The view to assign the theme(s) to
- * @param mixed $themes The theme(s). The type of these themes
- * is open to the implementation.
- * @param bool $useDefaultThemes If true, will use default themes specified
- * in the renderer
- */
- public function setTheme(FormView $view, mixed $themes, bool $useDefaultThemes = true): void;
- /**
- * Renders a named block of the form theme.
- *
- * @param FormView $view The view for which to render the block
- * @param array $variables The variables to pass to the template
- */
- public function renderBlock(FormView $view, string $blockName, array $variables = []): string;
- /**
- * Searches and renders a block for a given name suffix.
- *
- * The block is searched by combining the block names stored in the
- * form view with the given suffix. If a block name is found, that
- * block is rendered.
- *
- * If this method is called recursively, the block search is continued
- * where a block was found before.
- *
- * @param FormView $view The view for which to render the block
- * @param array $variables The variables to pass to the template
- */
- public function searchAndRenderBlock(FormView $view, string $blockNameSuffix, array $variables = []): string;
- /**
- * Renders a CSRF token.
- *
- * Use this helper for CSRF protection without the overhead of creating a
- * form.
- *
- * <input type="hidden" name="token" value="<?php $renderer->renderCsrfToken('rm_user_'.$user->getId()) ?>">
- *
- * Check the token in your action using the same token ID.
- *
- * // $csrfProvider being an instance of Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface
- * if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
- * throw new \RuntimeException('CSRF attack detected.');
- * }
- */
- public function renderCsrfToken(string $tokenId): string;
- /**
- * Makes a technical name human readable.
- *
- * Sequences of underscores are replaced by single spaces. The first letter
- * of the resulting string is capitalized, while all other letters are
- * turned to lowercase.
- */
- public function humanize(string $text): string;
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./FileUploadError.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- /**
- * @internal
- */
- class FileUploadError extends FormError
- {
- }
- ------------------------------------------------------------------------------------------------------------------------
- ./CallbackTransformer.php
- ------------------------------------------------------------------------------------------------------------------------
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <[email protected]>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Form;
- class CallbackTransformer implements DataTransformerInterface
- {
- private \Closure $transform;
- private \Closure $reverseTransform;
- public function __construct(callable $transform, callable $reverseTransform)
- {
- $this->transform = $transform(...);
- $this->reverseTransform = $reverseTransform(...);
- }
- public function transform(mixed $data): mixed
- {
- return ($this->transform)($data);
- }
- public function reverseTransform(mixed $data): mixed
- {
- return ($this->reverseTransform)($data);
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement