form
actual-batman |
PRO |
03/09/25 07:36:14 PM UTC |
0 ⭐ |
10528 👁️ |
Never ⏰ |
[Symfony 7.2 Form Component]
------------------------------------------------------------------------------------------------------------------------
./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);
}
}
Comments
0 B
|👍
/👎
0 B
|👍
/👎