login consent app sql

This commit is contained in:
2022-05-03 08:54:45 +02:00
parent e7253acfd8
commit f9a6535906
1652 changed files with 187600 additions and 45 deletions

View File

@ -0,0 +1,198 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
abstract class AbstractExtension implements FormExtensionInterface
{
/**
* The types provided by this extension.
*
* @var FormTypeInterface[]
*/
private $types;
/**
* The type extensions provided by this extension.
*
* @var FormTypeExtensionInterface[][]
*/
private $typeExtensions;
/**
* The type guesser provided by this extension.
*
* @var FormTypeGuesserInterface|null
*/
private $typeGuesser;
/**
* Whether the type guesser has been loaded.
*
* @var bool
*/
private $typeGuesserLoaded = false;
/**
* {@inheritdoc}
*/
public function getType(string $name)
{
if (null === $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];
}
/**
* {@inheritdoc}
*/
public function hasType(string $name)
{
if (null === $this->types) {
$this->initTypes();
}
return isset($this->types[$name]);
}
/**
* {@inheritdoc}
*/
public function getTypeExtensions(string $name)
{
if (null === $this->typeExtensions) {
$this->initTypeExtensions();
}
return $this->typeExtensions[$name]
?? [];
}
/**
* {@inheritdoc}
*/
public function hasTypeExtensions(string $name)
{
if (null === $this->typeExtensions) {
$this->initTypeExtensions();
}
return isset($this->typeExtensions[$name]) && \count($this->typeExtensions[$name]) > 0;
}
/**
* {@inheritdoc}
*/
public function getTypeGuesser()
{
if (!$this->typeGuesserLoaded) {
$this->initTypeGuesser();
}
return $this->typeGuesser;
}
/**
* Registers the types.
*
* @return FormTypeInterface[]
*/
protected function loadTypes()
{
return [];
}
/**
* Registers the type extensions.
*
* @return FormTypeExtensionInterface[]
*/
protected function loadTypeExtensions()
{
return [];
}
/**
* Registers the type guesser.
*
* @return FormTypeGuesserInterface|null
*/
protected function loadTypeGuesser()
{
return null;
}
/**
* Initializes the types.
*
* @throws UnexpectedTypeException if any registered type is not an instance of FormTypeInterface
*/
private function initTypes()
{
$this->types = [];
foreach ($this->loadTypes() as $type) {
if (!$type instanceof FormTypeInterface) {
throw new UnexpectedTypeException($type, FormTypeInterface::class);
}
$this->types[\get_class($type)] = $type;
}
}
/**
* Initializes the type extensions.
*
* @throws UnexpectedTypeException if any registered type extension is not
* an instance of FormTypeExtensionInterface
*/
private function initTypeExtensions()
{
$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()
{
$this->typeGuesserLoaded = true;
$this->typeGuesser = $this->loadTypeGuesser();
if (null !== $this->typeGuesser && !$this->typeGuesser instanceof FormTypeGuesserInterface) {
throw new UnexpectedTypeException($this->typeGuesser, FormTypeGuesserInterface::class);
}
}
}

View File

@ -0,0 +1,206 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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 $defaultThemes;
/**
* @var array[]
*/
protected $themes = [];
/**
* @var bool[]
*/
protected $useDefaultThemes = [];
/**
* @var array[]
*/
protected $resources = [];
/**
* @var array<array<int|false>>
*/
private $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(array $defaultThemes = [])
{
$this->defaultThemes = $defaultThemes;
}
/**
* {@inheritdoc}
*/
public function setTheme(FormView $view, $themes, bool $useDefaultThemes = true)
{
$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]);
}
/**
* {@inheritdoc}
*/
public function getResourceForBlockName(FormView $view, string $blockName)
{
$cacheKey = $view->vars[self::CACHE_KEY_VAR];
if (!isset($this->resources[$cacheKey][$blockName])) {
$this->loadResourceForBlockName($cacheKey, $view, $blockName);
}
return $this->resources[$cacheKey][$blockName];
}
/**
* {@inheritdoc}
*/
public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, int $hierarchyLevel)
{
$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];
}
/**
* {@inheritdoc}
*/
public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, int $hierarchyLevel)
{
$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()
*
* @return bool
*/
abstract protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName);
/**
* 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 = [];
}
}

66
vendor/symfony/form/AbstractType.php vendored Normal file
View File

@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
abstract class AbstractType implements FormTypeInterface
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return StringUtil::fqcnToBlockPrefix(static::class) ?: '';
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return FormType::class;
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
abstract class AbstractTypeExtension implements FormTypeExtensionInterface
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
}
}

455
vendor/symfony/form/Button.php vendored Normal file
View File

@ -0,0 +1,455 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
/**
* A form button.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @implements \IteratorAggregate<string, FormInterface>
*/
class Button implements \IteratorAggregate, FormInterface
{
/**
* @var FormInterface|null
*/
private $parent;
/**
* @var FormConfigInterface
*/
private $config;
/**
* @var bool
*/
private $submitted = false;
/**
* Creates a new button from a form configuration.
*/
public function __construct(FormConfigInterface $config)
{
$this->config = $config;
}
/**
* Unsupported method.
*
* @param string $offset
*
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return false;
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param string $offset
*
* @return FormInterface
*
* @throws BadMethodCallException
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param string $offset
* @param FormInterface $value
*
* @return void
*
* @throws BadMethodCallException
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param string $offset
*
* @return void
*
* @throws BadMethodCallException
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* {@inheritdoc}
*/
public function setParent(FormInterface $parent = null)
{
if ($this->submitted) {
throw new AlreadySubmittedException('You cannot set the parent of a submitted button.');
}
$this->parent = $parent;
return $this;
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return $this->parent;
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @throws BadMethodCallException
*/
public function add($child, string $type = null, array $options = [])
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @throws BadMethodCallException
*/
public function get(string $name)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* @return bool
*/
public function has(string $name)
{
return false;
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @throws BadMethodCallException
*/
public function remove(string $name)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* {@inheritdoc}
*/
public function all()
{
return [];
}
/**
* {@inheritdoc}
*/
public function getErrors(bool $deep = false, bool $flatten = true)
{
return new FormErrorIterator($this, []);
}
/**
* Unsupported method.
*
* This method should not be invoked.
*
* @param mixed $modelData
*
* @return $this
*/
public function setData($modelData)
{
// no-op, called during initialization of the form tree
return $this;
}
/**
* Unsupported method.
*/
public function getData()
{
return null;
}
/**
* Unsupported method.
*/
public function getNormData()
{
return null;
}
/**
* Unsupported method.
*/
public function getViewData()
{
return null;
}
/**
* Unsupported method.
*
* @return array
*/
public function getExtraData()
{
return [];
}
/**
* Returns the button's configuration.
*
* @return FormConfigInterface
*/
public function getConfig()
{
return $this->config;
}
/**
* Returns whether the button is submitted.
*
* @return bool
*/
public function isSubmitted()
{
return $this->submitted;
}
/**
* Returns the name by which the button is identified in forms.
*
* @return string
*/
public function getName()
{
return $this->config->getName();
}
/**
* Unsupported method.
*/
public function getPropertyPath()
{
return null;
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function addError(FormError $error)
{
throw new BadMethodCallException('Buttons cannot have errors.');
}
/**
* Unsupported method.
*
* @return bool
*/
public function isValid()
{
return true;
}
/**
* Unsupported method.
*
* @return bool
*/
public function isRequired()
{
return false;
}
/**
* {@inheritdoc}
*/
public function isDisabled()
{
if ($this->parent && $this->parent->isDisabled()) {
return true;
}
return $this->config->getDisabled();
}
/**
* Unsupported method.
*
* @return bool
*/
public function isEmpty()
{
return true;
}
/**
* Unsupported method.
*
* @return bool
*/
public function isSynchronized()
{
return true;
}
/**
* Unsupported method.
*/
public function getTransformationFailure()
{
return null;
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function initialize()
{
throw new BadMethodCallException('Buttons cannot be initialized. Call initialize() on the root form instead.');
}
/**
* Unsupported method.
*
* @param mixed $request
*
* @throws BadMethodCallException
*/
public function handleRequest($request = null)
{
throw new BadMethodCallException('Buttons cannot handle requests. Call handleRequest() on the root form instead.');
}
/**
* Submits data to the button.
*
* @param array|string|null $submittedData Not used
* @param bool $clearMissing Not used
*
* @return $this
*
* @throws Exception\AlreadySubmittedException if the button has already been submitted
*/
public function submit($submittedData, bool $clearMissing = true)
{
if ($this->submitted) {
throw new AlreadySubmittedException('A form can only be submitted once.');
}
$this->submitted = true;
return $this;
}
/**
* {@inheritdoc}
*/
public function getRoot()
{
return $this->parent ? $this->parent->getRoot() : $this;
}
/**
* {@inheritdoc}
*/
public function isRoot()
{
return null === $this->parent;
}
/**
* {@inheritdoc}
*/
public function createView(FormView $parent = null)
{
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.
*
* @return int
*/
#[\ReturnTypeWillChange]
public function count()
{
return 0;
}
/**
* Unsupported method.
*
* @return \EmptyIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \EmptyIterator();
}
}

744
vendor/symfony/form/ButtonBuilder.php vendored Normal file
View File

@ -0,0 +1,744 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
/**
* A builder for {@link Button} instances.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @implements \IteratorAggregate<string, FormBuilderInterface>
*/
class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
{
protected $locked = false;
/**
* @var bool
*/
private $disabled = false;
/**
* @var ResolvedFormTypeInterface
*/
private $type;
/**
* @var string
*/
private $name;
/**
* @var array
*/
private $attributes = [];
/**
* @var array
*/
private $options;
/**
* @throws InvalidArgumentException if the name is empty
*/
public function __construct(?string $name, array $options = [])
{
if ('' === $name || null === $name) {
throw new InvalidArgumentException('Buttons cannot have empty names.');
}
$this->name = $name;
$this->options = $options;
FormConfigBuilder::validateName($name);
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function add($child, string $type = null, array $options = [])
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function create(string $name, string $type = null, array $options = [])
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function get(string $name)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function remove(string $name)
{
throw new BadMethodCallException('Buttons cannot have children.');
}
/**
* Unsupported method.
*
* @return bool
*/
public function has(string $name)
{
return false;
}
/**
* Returns the children.
*
* @return array
*/
public function all()
{
return [];
}
/**
* Creates the button.
*
* @return Button
*/
public function getForm()
{
return new Button($this->getFormConfig());
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function addEventListener(string $eventName, callable $listener, int $priority = 0)
{
throw new BadMethodCallException('Buttons do not support event listeners.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function addEventSubscriber(EventSubscriberInterface $subscriber)
{
throw new BadMethodCallException('Buttons do not support event subscribers.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false)
{
throw new BadMethodCallException('Buttons do not support data transformers.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function resetViewTransformers()
{
throw new BadMethodCallException('Buttons do not support data transformers.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false)
{
throw new BadMethodCallException('Buttons do not support data transformers.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function resetModelTransformers()
{
throw new BadMethodCallException('Buttons do not support data transformers.');
}
/**
* {@inheritdoc}
*/
public function setAttribute(string $name, $value)
{
$this->attributes[$name] = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function setAttributes(array $attributes)
{
$this->attributes = $attributes;
return $this;
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setDataMapper(DataMapperInterface $dataMapper = null)
{
throw new BadMethodCallException('Buttons do not support data mappers.');
}
/**
* Set whether the button is disabled.
*
* @return $this
*/
public function setDisabled(bool $disabled)
{
$this->disabled = $disabled;
return $this;
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setEmptyData($emptyData)
{
throw new BadMethodCallException('Buttons do not support empty data.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setErrorBubbling(bool $errorBubbling)
{
throw new BadMethodCallException('Buttons do not support error bubbling.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setRequired(bool $required)
{
throw new BadMethodCallException('Buttons cannot be required.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setPropertyPath($propertyPath)
{
throw new BadMethodCallException('Buttons do not support property paths.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setMapped(bool $mapped)
{
throw new BadMethodCallException('Buttons do not support data mapping.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setByReference(bool $byReference)
{
throw new BadMethodCallException('Buttons do not support data mapping.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setCompound(bool $compound)
{
throw new BadMethodCallException('Buttons cannot be compound.');
}
/**
* Sets the type of the button.
*
* @return $this
*/
public function setType(ResolvedFormTypeInterface $type)
{
$this->type = $type;
return $this;
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setData($data)
{
throw new BadMethodCallException('Buttons do not support data.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setDataLocked(bool $locked)
{
throw new BadMethodCallException('Buttons do not support data locking.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setFormFactory(FormFactoryInterface $formFactory)
{
throw new BadMethodCallException('Buttons do not support form factories.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setAction(string $action)
{
throw new BadMethodCallException('Buttons do not support actions.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setMethod(string $method)
{
throw new BadMethodCallException('Buttons do not support methods.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setRequestHandler(RequestHandlerInterface $requestHandler)
{
throw new BadMethodCallException('Buttons do not support request handlers.');
}
/**
* Unsupported method.
*
* @return $this
*
* @throws BadMethodCallException
*/
public function setAutoInitialize(bool $initialize)
{
if (true === $initialize) {
throw new BadMethodCallException('Buttons do not support automatic initialization.');
}
return $this;
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function setInheritData(bool $inheritData)
{
throw new BadMethodCallException('Buttons do not support data inheritance.');
}
/**
* Builds and returns the button configuration.
*
* @return FormConfigInterface
*/
public function getFormConfig()
{
// 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)
{
throw new BadMethodCallException('Buttons do not support "is empty" callback.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function getEventDispatcher()
{
throw new BadMethodCallException('Buttons do not support event dispatching.');
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* Unsupported method.
*/
public function getPropertyPath()
{
return null;
}
/**
* Unsupported method.
*
* @return bool
*/
public function getMapped()
{
return false;
}
/**
* Unsupported method.
*
* @return bool
*/
public function getByReference()
{
return false;
}
/**
* Unsupported method.
*
* @return bool
*/
public function getCompound()
{
return false;
}
/**
* Returns the form type used to construct the button.
*
* @return ResolvedFormTypeInterface
*/
public function getType()
{
return $this->type;
}
/**
* Unsupported method.
*
* @return array
*/
public function getViewTransformers()
{
return [];
}
/**
* Unsupported method.
*
* @return array
*/
public function getModelTransformers()
{
return [];
}
/**
* Unsupported method.
*/
public function getDataMapper()
{
return null;
}
/**
* Unsupported method.
*
* @return bool
*/
public function getRequired()
{
return false;
}
/**
* Returns whether the button is disabled.
*
* @return bool
*/
public function getDisabled()
{
return $this->disabled;
}
/**
* Unsupported method.
*
* @return bool
*/
public function getErrorBubbling()
{
return false;
}
/**
* Unsupported method.
*/
public function getEmptyData()
{
return null;
}
/**
* Returns additional attributes of the button.
*
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Returns whether the attribute with the given name exists.
*
* @return bool
*/
public function hasAttribute(string $name)
{
return \array_key_exists($name, $this->attributes);
}
/**
* Returns the value of the given attribute.
*
* @param mixed $default The value returned if the attribute does not exist
*
* @return mixed
*/
public function getAttribute(string $name, $default = null)
{
return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
}
/**
* Unsupported method.
*/
public function getData()
{
return null;
}
/**
* Unsupported method.
*/
public function getDataClass()
{
return null;
}
/**
* Unsupported method.
*
* @return bool
*/
public function getDataLocked()
{
return false;
}
/**
* Unsupported method.
*/
public function getFormFactory()
{
throw new BadMethodCallException('Buttons do not support adding children.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function getAction()
{
throw new BadMethodCallException('Buttons do not support actions.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function getMethod()
{
throw new BadMethodCallException('Buttons do not support methods.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function getRequestHandler()
{
throw new BadMethodCallException('Buttons do not support request handlers.');
}
/**
* Unsupported method.
*
* @return bool
*/
public function getAutoInitialize()
{
return false;
}
/**
* Unsupported method.
*
* @return bool
*/
public function getInheritData()
{
return false;
}
/**
* Returns all options passed during the construction of the button.
*
* @return array
*/
public function getOptions()
{
return $this->options;
}
/**
* Returns whether a specific option exists.
*
* @return bool
*/
public function hasOption(string $name)
{
return \array_key_exists($name, $this->options);
}
/**
* Returns the value of a specific option.
*
* @param mixed $default The value returned if the option does not exist
*
* @return mixed
*/
public function getOption(string $name, $default = null)
{
return \array_key_exists($name, $this->options) ? $this->options[$name] : $default;
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function getIsEmptyCallback(): ?callable
{
throw new BadMethodCallException('Buttons do not support "is empty" callback.');
}
/**
* Unsupported method.
*
* @return int
*/
#[\ReturnTypeWillChange]
public function count()
{
return 0;
}
/**
* Unsupported method.
*
* @return \EmptyIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \EmptyIterator();
}
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
interface ButtonTypeInterface extends FormTypeInterface
{
}

567
vendor/symfony/form/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,567 @@
CHANGELOG
=========
5.4
---
* Deprecate calling `FormErrorIterator::children()` if the current element is not iterable.
* Allow to pass `TranslatableMessage` objects to the `help` option
* Add the `EnumType`
5.3
---
* Changed `$forms` parameter type of the `DataMapperInterface::mapDataToForms()` method from `iterable` to `\Traversable`.
* Changed `$forms` parameter type of the `DataMapperInterface::mapFormsToData()` method from `iterable` to `\Traversable`.
* Deprecated passing an array as the second argument of the `DataMapper::mapDataToForms()` method, pass `\Traversable` instead.
* Deprecated passing an array as the first argument of the `DataMapper::mapFormsToData()` method, pass `\Traversable` instead.
* Deprecated passing an array as the second argument of the `CheckboxListMapper::mapDataToForms()` method, pass `\Traversable` instead.
* Deprecated passing an array as the first argument of the `CheckboxListMapper::mapFormsToData()` method, pass `\Traversable` instead.
* Deprecated passing an array as the second argument of the `RadioListMapper::mapDataToForms()` method, pass `\Traversable` instead.
* Deprecated passing an array as the first argument of the `RadioListMapper::mapFormsToData()` method, pass `\Traversable` instead.
* Added a `choice_translation_parameters` option to `ChoiceType`
* Add `UuidType` and `UlidType`
* Dependency on `symfony/intl` was removed. Install `symfony/intl` if you are using `LocaleType`, `CountryType`, `CurrencyType`, `LanguageType` or `TimezoneType`.
* Add `priority` option to `BaseType` and sorting view fields
5.2.0
-----
* Added support for using the `{{ label }}` placeholder in constraint messages, which is replaced in the `ViolationMapper` by the corresponding field form label.
* Added `DataMapper`, `ChainAccessor`, `PropertyPathAccessor` and `CallbackAccessor` with new callable `getter` and `setter` options for each form type
* Deprecated `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`
* Added an `html5` option to `MoneyType` and `PercentType`, to use `<input type="number" />`
5.1.0
-----
* Deprecated not configuring the `rounding_mode` option of the `PercentType`. It will default to `\NumberFormatter::ROUND_HALFUP` in Symfony 6.
* Deprecated not passing a rounding mode to the constructor of `PercentToLocalizedStringTransformer`. It will default to `\NumberFormatter::ROUND_HALFUP` in Symfony 6.
* Added `collection_entry` block prefix to `CollectionType` entries
* Added a `choice_filter` option to `ChoiceType`
* Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()` - not defining them is deprecated.
* Added a `ChoiceList` facade to leverage explicit choice list caching based on options
* Added an `AbstractChoiceLoader` to simplify implementations and handle global optimizations
* The `view_timezone` option defaults to the `model_timezone` if no `reference_date` is configured.
* Implementing the `FormConfigInterface` without implementing the `getIsEmptyCallback()` method
is deprecated. The method will be added to the interface in 6.0.
* Implementing the `FormConfigBuilderInterface` without implementing the `setIsEmptyCallback()` method
is deprecated. The method will be added to the interface in 6.0.
* Added a `rounding_mode` option for the PercentType and correctly round the value when submitted
* Deprecated `Symfony\Component\Form\Extension\Validator\Util\ServerParams` in favor of its parent class `Symfony\Component\Form\Util\ServerParams`
* Added the `html5` option to the `ColorType` to validate the input
* Deprecated `NumberToLocalizedStringTransformer::ROUND_*` constants, use `\NumberFormatter::ROUND_*` instead
5.0.0
-----
* Removed support for using different values for the "model_timezone" and "view_timezone" options of the `TimeType`
without configuring a reference date.
* Removed the `scale` option of the `IntegerType`.
* Using the `date_format`, `date_widget`, and `time_widget` options of the `DateTimeType` when the `widget` option is
set to `single_text` is not supported anymore.
* The `format` option of `DateType` and `DateTimeType` cannot be used when the `html5` option is enabled.
* Using names for buttons that do not start with a letter, a digit, or an underscore throw an exception
* Using names for buttons that do not contain only letters, digits, underscores, hyphens, and colons throw an exception.
* removed the `ChoiceLoaderInterface` implementation in `CountryType`, `LanguageType`, `LocaleType` and `CurrencyType`
* removed `getExtendedType()` method of the `FormTypeExtensionInterface`
* added static `getExtendedTypes()` method to the `FormTypeExtensionInterface`
* calling to `FormRenderer::searchAndRenderBlock()` method for fields which were already rendered throw a `BadMethodCallException`
* removed the `regions` option of the `TimezoneType`
* removed the `$scale` argument of the `IntegerToLocalizedStringTransformer`
* removed `TemplatingExtension` and `TemplatingRendererEngine` classes, use Twig instead
* passing a null message when instantiating a `Symfony\Component\Form\FormError` is not allowed
* removed support for using `int` or `float` as data for the `NumberType` when the `input` option is set to `string`
4.4.0
-----
* add new `WeekType`
* using different values for the "model_timezone" and "view_timezone" options of the `TimeType` without configuring a
reference date is deprecated
* preferred choices are repeated in the list of all choices
* deprecated using `int` or `float` as data for the `NumberType` when the `input` option is set to `string`
* The type guesser guesses the HTML accept attribute when a mime type is configured in the File or Image constraint.
* Overriding the methods `FormIntegrationTestCase::setUp()`, `TypeTestCase::setUp()` and `TypeTestCase::tearDown()` without the `void` return-type is deprecated.
* marked all dispatched event classes as `@final`
* Added the `validate` option to `SubmitType` to toggle the browser built-in form validation.
* Added the `alpha3` option to `LanguageType` and `CountryType` to use alpha3 instead of alpha2 codes
4.3.0
-----
* added a `symbol` option to the `PercentType` that allows to disable or customize the output of the percent character
* Using the `format` option of `DateType` and `DateTimeType` when the `html5` option is enabled is deprecated.
* Using names for buttons that do not start with a letter, a digit, or an underscore is deprecated and will lead to an
exception in 5.0.
* Using names for buttons that do not contain only letters, digits, underscores, hyphens, and colons is deprecated and
will lead to an exception in 5.0.
* added `html5` option to `NumberType` that allows to render `type="number"` input fields
* deprecated using the `date_format`, `date_widget`, and `time_widget` options of the `DateTimeType` when the `widget`
option is set to `single_text`
* added `block_prefix` option to `BaseType`.
* added `help_html` option to display the `help` text as HTML.
* `FormError` doesn't implement `Serializable` anymore
* `FormDataCollector` has been marked as `final`
* added `label_translation_parameters`, `attr_translation_parameters`, `help_translation_parameters` options
to `FormType` to pass translation parameters to form labels, attributes (`placeholder` and `title`) and help text respectively.
The passed parameters will replace placeholders in translation messages.
```php
class OrderType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('comment', TextType::class, [
'label' => 'Comment to the order to %company%',
'label_translation_parameters' => [
'%company%' => 'Acme',
],
'help' => 'The address of the %company% is %address%',
'help_translation_parameters' => [
'%company%' => 'Acme Ltd.',
'%address%' => '4 Form street, Symfonyville',
],
])
}
}
```
* added the `input_format` option to `DateType`, `DateTimeType`, and `TimeType` to specify the input format when setting
the `input` option to `string`
* dispatch `PreSubmitEvent` on `form.pre_submit`
* dispatch `SubmitEvent` on `form.submit`
* dispatch `PostSubmitEvent` on `form.post_submit`
* dispatch `PreSetDataEvent` on `form.pre_set_data`
* dispatch `PostSetDataEvent` on `form.post_set_data`
* added an `input` option to `NumberType`
* removed default option grouping in `TimezoneType`, use `group_by` instead
4.2.0
-----
* The `getExtendedType()` method of the `FormTypeExtensionInterface` is deprecated and will be removed in 5.0. Type
extensions must implement the static `getExtendedTypes()` method instead and return an iterable of extended types.
Before:
```php
class FooTypeExtension extends AbstractTypeExtension
{
public function getExtendedType()
{
return FormType::class;
}
// ...
}
```
After:
```php
class FooTypeExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [FormType::class];
}
// ...
}
```
* deprecated the `$scale` argument of the `IntegerToLocalizedStringTransformer`
* added `Symfony\Component\Form\ClearableErrorsInterface`
* deprecated calling `FormRenderer::searchAndRenderBlock` for fields which were already rendered
* added a cause when a CSRF error has occurred
* deprecated the `scale` option of the `IntegerType`
* removed restriction on allowed HTTP methods
* deprecated the `regions` option of the `TimezoneType`
4.1.0
-----
* added `input=datetime_immutable` to `DateType`, `TimeType`, `DateTimeType`
* added `rounding_mode` option to `MoneyType`
* added `choice_translation_locale` option to `CountryType`, `LanguageType`, `LocaleType` and `CurrencyType`
* deprecated the `ChoiceLoaderInterface` implementation in `CountryType`, `LanguageType`, `LocaleType` and `CurrencyType`
* added `input=datetime_immutable` to DateType, TimeType, DateTimeType
* added `rounding_mode` option to MoneyType
4.0.0
-----
* using the `choices` option in `CountryType`, `CurrencyType`, `LanguageType`,
`LocaleType`, and `TimezoneType` when the `choice_loader` option is not `null`
is not supported anymore and the configured choices will be ignored
* callable strings that are passed to the options of the `ChoiceType` are
treated as property paths
* the `choices_as_values` option of the `ChoiceType` has been removed
* removed the support for caching loaded choice lists in `LazyChoiceList`,
cache the choice list in the used `ChoiceLoaderInterface` implementation
instead
* removed the support for objects implementing both `\Traversable` and `\ArrayAccess` in `ResizeFormListener::preSubmit()`
* removed the ability to use `FormDataCollector` without the `symfony/var-dumper` component
* removed passing a `ValueExporter` instance to the `FormDataExtractor::__construct()` method
* removed passing guesser services ids as the fourth argument of `DependencyInjectionExtension::__construct()`
* removed the ability to validate an unsubmitted form.
* removed `ChoiceLoaderInterface` implementation in `TimezoneType`
* added the `false_values` option to the `CheckboxType` which allows to configure custom values which will be treated as `false` during submission
3.4.0
-----
* added `DebugCommand`
* deprecated `ChoiceLoaderInterface` implementation in `TimezoneType`
* added options "input" and "regions" to `TimezoneType`
* added an option to ``Symfony\Component\Form\FormRendererEngineInterface::setTheme()`` and
``Symfony\Component\Form\FormRendererInterface::setTheme()`` to disable usage of default themes when rendering a form
3.3.0
-----
* deprecated using "choices" option in ``CountryType``, ``CurrencyType``, ``LanguageType``, ``LocaleType``, and
``TimezoneType`` when "choice_loader" is not ``null``
* added `Symfony\Component\Form\FormErrorIterator::findByCodes()`
* added `getTypedExtensions`, `getTypes`, and `getTypeGuessers` to `Symfony\Component\Form\Test\FormIntegrationTestCase`
* added `FormPass`
3.2.0
-----
* added `CallbackChoiceLoader`
* implemented `ChoiceLoaderInterface` in children of `ChoiceType`
3.1.0
-----
* deprecated the "choices_as_values" option of ChoiceType
* deprecated support for data objects that implements both `Traversable` and
`ArrayAccess` in `ResizeFormListener::preSubmit` method
* Using callable strings as choice options in `ChoiceType` has been deprecated
and will be used as `PropertyPath` instead of callable in Symfony 4.0.
* implemented `DataTransformerInterface` in `TextType`
* deprecated caching loaded choice list in `LazyChoiceList::$loadedList`
3.0.0
-----
* removed `FormTypeInterface::setDefaultOptions()` method
* removed `AbstractType::setDefaultOptions()` method
* removed `FormTypeExtensionInterface::setDefaultOptions()` method
* removed `AbstractTypeExtension::setDefaultOptions()` method
* added `FormTypeInterface::configureOptions()` method
* added `FormTypeExtensionInterface::configureOptions()` method
2.8.0
-----
* added option "choice_translation_domain" to DateType, TimeType and DateTimeType.
* deprecated option "read_only" in favor of "attr['readonly']"
* added the html5 "range" FormType
* deprecated the "cascade_validation" option in favor of setting "constraints"
with the Valid constraint
* moved data trimming logic of TrimListener into StringUtil
* [BC BREAK] When registering a type extension through the DI extension, the tag alias has to match the actual extended type.
2.7.38
------
* [BC BREAK] the `isFileUpload()` method was added to the `RequestHandlerInterface`
2.7.0
-----
* added option "choice_translation_domain" to ChoiceType.
* deprecated option "precision" in favor of "scale"
* deprecated the overwriting of AbstractType::setDefaultOptions() in favor of overwriting AbstractType::configureOptions().
* deprecated the overwriting of AbstractTypeExtension::setDefaultOptions() in favor of overwriting AbstractTypeExtension::configureOptions().
* added new ChoiceList interface and implementations in the Symfony\Component\Form\ChoiceList namespace
* added new ChoiceView in the Symfony\Component\Form\ChoiceList\View namespace
* choice groups are now represented by ChoiceGroupView objects in the view
* deprecated the old ChoiceList interface and implementations
* deprecated the old ChoiceView class
* added CheckboxListMapper and RadioListMapper
* deprecated ChoiceToBooleanArrayTransformer and ChoicesToBooleanArrayTransformer
* deprecated FixCheckboxInputListener and FixRadioInputListener
* deprecated the "choice_list" option of ChoiceType
* added new options to ChoiceType:
* "choices_as_values"
* "choice_loader"
* "choice_label"
* "choice_name"
* "choice_value"
* "choice_attr"
* "group_by"
2.6.2
-----
* Added back the `model_timezone` and `view_timezone` options for `TimeType`, `DateType`
and `BirthdayType`
2.6.0
-----
* added "html5" option to Date, Time and DateTimeFormType to be able to
enable/disable HTML5 input date when widget option is "single_text"
* added "label_format" option with possible placeholders "%name%" and "%id%"
* [BC BREAK] drop support for model_timezone and view_timezone options in TimeType, DateType and BirthdayType,
update to 2.6.2 to get back support for these options
2.5.0
------
* deprecated options "max_length" and "pattern" in favor of putting these values in "attr" option
* added an option for multiple files upload
* form errors now reference their cause (constraint violation, exception, ...)
* form errors now remember which form they were originally added to
* [BC BREAK] added two optional parameters to FormInterface::getErrors() and
changed the method to return a Symfony\Component\Form\FormErrorIterator
instance instead of an array
* errors mapped to unsubmitted forms are discarded now
* ObjectChoiceList now compares choices by their value, if a value path is
given
* you can now pass interface names in the "data_class" option
* [BC BREAK] added `FormInterface::getTransformationFailure()`
2.4.0
-----
* moved CSRF implementation to the new Security CSRF sub-component
* deprecated CsrfProviderInterface and its implementations
* deprecated options "csrf_provider" and "intention" in favor of the new options "csrf_token_manager" and "csrf_token_id"
2.3.0
-----
* deprecated FormPerformanceTestCase and FormIntegrationTestCase in the Symfony\Component\Form\Tests namespace and moved them to the Symfony\Component\Form\Test namespace
* deprecated TypeTestCase in the Symfony\Component\Form\Tests\Extension\Core\Type namespace and moved it to the Symfony\Component\Form\Test namespace
* changed FormRenderer::humanize() to humanize also camel cased field name
* added RequestHandlerInterface and FormInterface::handleRequest()
* deprecated passing a Request instance to FormInterface::bind()
* added options "method" and "action" to FormType
* deprecated option "virtual" in favor "inherit_data"
* deprecated VirtualFormAwareIterator in favor of InheritDataAwareIterator
* [BC BREAK] removed the "array" type hint from DataMapperInterface
* improved forms inheriting their parent data to actually return that data from getData(), getNormData() and getViewData()
* added component-level exceptions for various SPL exceptions
changed all uses of the deprecated Exception class to use more specialized exceptions instead
removed NotInitializedException, NotValidException, TypeDefinitionException, TypeLoaderException, CreationException
* added events PRE_SUBMIT, SUBMIT and POST_SUBMIT
* deprecated events PRE_BIND, BIND and POST_BIND
* [BC BREAK] renamed bind() and isBound() in FormInterface to submit() and isSubmitted()
* added methods submit() and isSubmitted() to Form
* deprecated bind() and isBound() in Form
* deprecated AlreadyBoundException in favor of AlreadySubmittedException
* added support for PATCH requests
* [BC BREAK] added initialize() to FormInterface
* [BC BREAK] added getAutoInitialize() to FormConfigInterface
* [BC BREAK] added setAutoInitialize() to FormConfigBuilderInterface
* [BC BREAK] initialization for Form instances added to a form tree must be manually disabled
* PRE_SET_DATA is now guaranteed to be called after children were added by the form builder,
unless FormInterface::setData() is called manually
* fixed CSRF error message to be translated
* custom CSRF error messages can now be set through the "csrf_message" option
* fixed: expanded single-choice fields now show a radio button for the empty value
2.2.0
-----
* TrimListener now removes unicode whitespaces
* deprecated getParent(), setParent() and hasParent() in FormBuilderInterface
* FormInterface::add() now accepts a FormInterface instance OR a field's name, type and options
* removed special characters between the choice or text fields of DateType unless
the option "format" is set to a custom value
* deprecated FormException and introduced ExceptionInterface instead
* [BC BREAK] FormException is now an interface
* protected FormBuilder methods from being called when it is turned into a FormConfigInterface with getFormConfig()
* [BC BREAK] inserted argument `$message` in the constructor of `FormError`
* the PropertyPath class and related classes were moved to a dedicated
PropertyAccess component. During the move, InvalidPropertyException was
renamed to NoSuchPropertyException. FormUtil was split: FormUtil::singularify()
can now be found in Symfony\Component\PropertyAccess\StringUtil. The methods
getValue() and setValue() from PropertyPath were extracted into a new class
PropertyAccessor.
* added an optional PropertyAccessorInterface parameter to FormType,
ObjectChoiceList and PropertyPathMapper
* [BC BREAK] PropertyPathMapper and FormType now have a constructor
* [BC BREAK] setting the option "validation_groups" to ``false`` now disables validation
instead of assuming group "Default"
2.1.0
-----
* [BC BREAK] ``read_only`` field attribute now renders as ``readonly="readonly"``, use ``disabled`` instead
* [BC BREAK] child forms now aren't validated anymore by default
* made validation of form children configurable (new option: cascade_validation)
* added support for validation groups as callbacks
* made the translation catalogue configurable via the "translation_domain" option
* added Form::getErrorsAsString() to help debugging forms
* allowed setting different options for RepeatedType fields (like the label)
* added support for empty form name at root level, this enables rendering forms
without form name prefix in field names
* [BC BREAK] form and field names must start with a letter, digit or underscore
and only contain letters, digits, underscores, hyphens and colons
* [BC BREAK] changed default name of the prototype in the "collection" type
from "$$name$$" to "\__name\__". No dollars are appended/prepended to custom
names anymore.
* [BC BREAK] improved ChoiceListInterface
* [BC BREAK] added SimpleChoiceList and LazyChoiceList as replacement of
ArrayChoiceList
* added ChoiceList and ObjectChoiceList to use objects as choices
* [BC BREAK] removed EntitiesToArrayTransformer and EntityToIdTransformer.
The former has been replaced by CollectionToArrayTransformer in combination
with EntityChoiceList, the latter is not required in the core anymore.
* [BC BREAK] renamed
* ArrayToBooleanChoicesTransformer to ChoicesToBooleanArrayTransformer
* ScalarToBooleanChoicesTransformer to ChoiceToBooleanArrayTransformer
* ArrayToChoicesTransformer to ChoicesToValuesTransformer
* ScalarToChoiceTransformer to ChoiceToValueTransformer
to be consistent with the naming in ChoiceListInterface.
They were merged into ChoiceList and have no public equivalent anymore.
* choice fields now throw a FormException if neither the "choices" nor the
"choice_list" option is set
* the radio type is now a child of the checkbox type
* the collection, choice (with multiple selection) and entity (with multiple
selection) types now make use of addXxx() and removeXxx() methods in your
model if you set "by_reference" to false. For a custom, non-recognized
singular form, set the "property_path" option like this: "plural|singular"
* forms now don't create an empty object anymore if they are completely
empty and not required. The empty value for such forms is null.
* added constant Guess::VERY_HIGH_CONFIDENCE
* [BC BREAK] The methods `add`, `remove`, `setParent`, `bind` and `setData`
in class Form now throw an exception if the form is already bound
* fields of constrained classes without a NotBlank or NotNull constraint are
set to not required now, as stated in the docs
* fixed TimeType and DateTimeType to not display seconds when "widget" is
"single_text" unless "with_seconds" is set to true
* checkboxes of in an expanded multiple-choice field don't include the choice
in their name anymore. Their names terminate with "[]" now.
* deprecated FormValidatorInterface and substituted its implementations
by event subscribers
* simplified CSRF protection and removed the csrf type
* deprecated FieldType and merged it into FormType
* added new option "compound" that lets you switch between field and form behavior
* [BC BREAK] renamed theme blocks
* "field_*" to "form_*"
* "field_widget" to "form_widget_simple"
* "widget_choice_options" to "choice_widget_options"
* "generic_label" to "form_label"
* added theme blocks "form_widget_compound", "choice_widget_expanded" and
"choice_widget_collapsed" to make theming more modular
* ValidatorTypeGuesser now guesses "collection" for array type constraint
* added method `guessPattern` to FormTypeGuesserInterface to guess which pattern to use in the HTML5 attribute "pattern"
* deprecated method `guessMinLength` in favor of `guessPattern`
* labels don't display field attributes anymore. Label attributes can be
passed in the "label_attr" option/variable
* added option "mapped" which should be used instead of setting "property_path" to false
* [BC BREAK] "data_class" now *must* be set if a form maps to an object and should be left empty otherwise
* improved error mapping on forms
* dot (".") rules are now allowed to map errors assigned to a form to
one of its children
* errors are not mapped to unsynchronized forms anymore
* [BC BREAK] changed Form constructor to accept a single `FormConfigInterface` object
* [BC BREAK] changed argument order in the FormBuilder constructor
* added Form method `getViewData`
* deprecated Form methods
* `getTypes`
* `getErrorBubbling`
* `getNormTransformers`
* `getClientTransformers`
* `getAttribute`
* `hasAttribute`
* `getClientData`
* added FormBuilder methods
* `getTypes`
* `addViewTransformer`
* `getViewTransformers`
* `resetViewTransformers`
* `addModelTransformer`
* `getModelTransformers`
* `resetModelTransformers`
* deprecated FormBuilder methods
* `prependClientTransformer`
* `appendClientTransformer`
* `getClientTransformers`
* `resetClientTransformers`
* `prependNormTransformer`
* `appendNormTransformer`
* `getNormTransformers`
* `resetNormTransformers`
* deprecated the option "validation_constraint" in favor of the new
option "constraints"
* removed superfluous methods from DataMapperInterface
* `mapFormToData`
* `mapDataToForm`
* added `setDefaultOptions` to FormTypeInterface and FormTypeExtensionInterface
which accepts an OptionsResolverInterface instance
* deprecated the methods `getDefaultOptions` and `getAllowedOptionValues`
in FormTypeInterface and FormTypeExtensionInterface
* options passed during construction can now be accessed from FormConfigInterface
* added FormBuilderInterface and FormConfigEditorInterface
* [BC BREAK] the method `buildForm` in FormTypeInterface and FormTypeExtensionInterface
now receives a FormBuilderInterface instead of a FormBuilder instance
* [BC BREAK] the method `buildViewBottomUp` was renamed to `finishView` in
FormTypeInterface and FormTypeExtensionInterface
* [BC BREAK] the options array is now passed as last argument of the
methods
* `buildView`
* `finishView`
in FormTypeInterface and FormTypeExtensionInterface
* [BC BREAK] no options are passed to `getParent` of FormTypeInterface anymore
* deprecated DataEvent and FilterDataEvent in favor of the new FormEvent which is
now passed to all events thrown by the component
* FormEvents::BIND now replaces FormEvents::BIND_NORM_DATA
* FormEvents::PRE_SET_DATA now replaces FormEvents::SET_DATA
* FormEvents::PRE_BIND now replaces FormEvents::BIND_CLIENT_DATA
* deprecated FormEvents::SET_DATA, FormEvents::BIND_CLIENT_DATA and
FormEvents::BIND_NORM_DATA
* [BC BREAK] reversed the order of the first two arguments to `createNamed`
and `createNamedBuilder` in `FormFactoryInterface`
* deprecated `getChildren` in Form and FormBuilder in favor of `all`
* deprecated `hasChildren` in Form and FormBuilder in favor of `count`
* FormBuilder now implements \IteratorAggregate
* [BC BREAK] compound forms now always need a data mapper
* FormBuilder now maintains the order when explicitly adding form builders as children
* ChoiceType now doesn't add the empty value anymore if the choices already contain an empty element
* DateType, TimeType and DateTimeType now show empty values again if not required
* [BC BREAK] fixed rendering of errors for DateType, BirthdayType and similar ones
* [BC BREAK] fixed: form constraints are only validated if they belong to the validated group
* deprecated `bindRequest` in `Form` and replaced it by a listener to FormEvents::PRE_BIND
* fixed: the "data" option supersedes default values from the model
* changed DateType to refer to the "format" option for calculating the year and day choices instead
of padding them automatically
* [BC BREAK] DateType defaults to the format "yyyy-MM-dd" now if the widget is
"single_text", in order to support the HTML 5 date field out of the box
* added the option "format" to DateTimeType
* [BC BREAK] DateTimeType now outputs RFC 3339 dates by default, as generated and
consumed by HTML5 browsers, if the widget is "single_text"
* deprecated the options "data_timezone" and "user_timezone" in DateType, DateTimeType and TimeType
and renamed them to "model_timezone" and "view_timezone"
* fixed: TransformationFailedExceptions thrown in the model transformer are now caught by the form
* added FormRegistryInterface, ResolvedFormTypeInterface and ResolvedFormTypeFactoryInterface
* deprecated FormFactory methods
* `addType`
* `hasType`
* `getType`
* [BC BREAK] FormFactory now expects a FormRegistryInterface and a ResolvedFormTypeFactoryInterface as constructor argument
* [BC BREAK] The method `createBuilder` in FormTypeInterface is not supported anymore for performance reasons
* [BC BREAK] Removed `setTypes` from FormBuilder
* deprecated AbstractType methods
* `getExtensions`
* `setExtensions`
* ChoiceType now caches its created choice lists to improve performance
* [BC BREAK] Rows of a collection field cannot be themed individually anymore. All rows in the collection
field now have the same block names, which contains "entry" where it previously contained the row index.
* [BC BREAK] When registering a type through the DI extension, the tag alias has to match the actual type name.
* added FormRendererInterface, FormRendererEngineInterface and implementations of these interfaces
* [BC BREAK] removed the following methods from FormUtil:
* `toArrayKey`
* `toArrayKeys`
* `isChoiceGroup`
* `isChoiceSelected`
* [BC BREAK] renamed method `renderBlock` in FormHelper to `block` and changed its signature
* made FormView properties public and deprecated their accessor methods
* made the normalized data of a form accessible in the template through the variable "form.vars.data"
* made the original data of a choice accessible in the template through the property "choice.data"
* added convenience class Forms and FormFactoryBuilderInterface

View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 $transform;
private $reverseTransform;
/**
* @param callable $transform The forward transform callback
* @param callable $reverseTransform The reverse transform callback
*/
public function __construct(callable $transform, callable $reverseTransform)
{
$this->transform = $transform;
$this->reverseTransform = $reverseTransform;
}
/**
* {@inheritdoc}
*/
public function transform($data)
{
return ($this->transform)($data);
}
/**
* {@inheritdoc}
*/
public function reverseTransform($data)
{
return ($this->reverseTransform)($data);
}
}

View File

@ -0,0 +1,238 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 annd 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 <bschussek@gmail.com>
*/
class ArrayChoiceList implements ChoiceListInterface
{
/**
* The choices in the list.
*
* @var array
*/
protected $choices;
/**
* The values indexed by the original keys.
*
* @var array
*/
protected $structuredValues;
/**
* The original keys of the choices array.
*
* @var int[]|string[]
*/
protected $originalKeys;
protected $valueCallback;
/**
* 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 = function ($choice) {
return 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
$i = 0;
$value = function () use (&$i) {
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;
}
/**
* {@inheritdoc}
*/
public function getChoices()
{
return $this->choices;
}
/**
* {@inheritdoc}
*/
public function getValues()
{
return array_map('strval', array_keys($this->choices));
}
/**
* {@inheritdoc}
*/
public function getStructuredValues()
{
return $this->structuredValues;
}
/**
* {@inheritdoc}
*/
public function getOriginalKeys()
{
return $this->originalKeys;
}
/**
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
$choices = [];
foreach ($values as $i => $givenValue) {
if (\array_key_exists($givenValue, $this->choices)) {
$choices[$i] = $this->choices[$givenValue];
}
}
return $choices;
}
/**
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
$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)
{
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;
}
}

View File

@ -0,0 +1,159 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
final class ChoiceList
{
/**
* Creates a cacheable loader from any callable providing iterable choices.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable $choices A callable that must return iterable choices or grouped choices
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the loader
*/
public static function lazy($formType, callable $choices, $vary = null): ChoiceLoader
{
return self::loader($formType, new CallbackChoiceLoader($choices), $vary);
}
/**
* Decorates a loader to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param ChoiceLoaderInterface $loader A loader responsible for creating loading choices or grouped choices
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the loader
*/
public static function loader($formType, ChoiceLoaderInterface $loader, $vary = null): ChoiceLoader
{
return new ChoiceLoader($formType, $loader, $vary);
}
/**
* Decorates a "choice_value" callback to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable $value Any pseudo callable to create a unique string value from a choice
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
*/
public static function value($formType, $value, $vary = null): ChoiceValue
{
return new ChoiceValue($formType, $value, $vary);
}
/**
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable $filter Any pseudo callable to filter a choice list
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
*/
public static function filter($formType, $filter, $vary = null): ChoiceFilter
{
return new ChoiceFilter($formType, $filter, $vary);
}
/**
* Decorates a "choice_label" option to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable|false $label Any pseudo callable to create a label from a choice or false to discard it
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
*/
public static function label($formType, $label, $vary = null): ChoiceLabel
{
return new ChoiceLabel($formType, $label, $vary);
}
/**
* Decorates a "choice_name" callback to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable $fieldName Any pseudo callable to create a field name from a choice
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
*/
public static function fieldName($formType, $fieldName, $vary = null): ChoiceFieldName
{
return new ChoiceFieldName($formType, $fieldName, $vary);
}
/**
* Decorates a "choice_attr" option to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable|array $attr Any pseudo callable or array to create html attributes from a choice
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
*/
public static function attr($formType, $attr, $vary = null): ChoiceAttr
{
return new ChoiceAttr($formType, $attr, $vary);
}
/**
* Decorates a "choice_translation_parameters" option to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable|array $translationParameters Any pseudo callable or array to create translation parameters from a choice
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
*/
public static function translationParameters($formType, $translationParameters, $vary = null): ChoiceTranslationParameters
{
return new ChoiceTranslationParameters($formType, $translationParameters, $vary);
}
/**
* Decorates a "group_by" callback to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable $groupBy Any pseudo callable to return a group name from a choice
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
*/
public static function groupBy($formType, $groupBy, $vary = null): GroupBy
{
return new GroupBy($formType, $groupBy, $vary);
}
/**
* Decorates a "preferred_choices" option to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable|array $preferred Any pseudo callable or array to return a group name from a choice
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
*/
public static function preferred($formType, $preferred, $vary = null): PreferredChoice
{
return new PreferredChoice($formType, $preferred, $vary);
}
/**
* Should not be instantiated.
*/
private function __construct()
{
}
}

View File

@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
interface ChoiceListInterface
{
/**
* Returns all selectable choices.
*
* @return array The selectable choices indexed by the corresponding values
*/
public function getChoices();
/**
* 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();
/**
* 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();
/**
* 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();
/**
* 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
*
* @return array
*/
public function getChoicesForValues(array $values);
/**
* 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);
}

View File

@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
abstract class AbstractStaticOption
{
private static $options = [];
/** @var bool|callable|string|array|\Closure|ChoiceLoaderInterface */
private $option;
/**
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param mixed $option Any pseudo callable, array, string or bool to define a choice list option
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
*/
final public function __construct($formType, $option, $vary = null)
{
if (!$formType instanceof FormTypeInterface && !$formType instanceof FormTypeExtensionInterface) {
throw new \TypeError(sprintf('Expected an instance of "%s" or "%s", but got "%s".', FormTypeInterface::class, FormTypeExtensionInterface::class, get_debug_type($formType)));
}
$hash = CachingFactoryDecorator::generateHash([static::class, $formType, $vary]);
$this->option = self::$options[$hash] ?? self::$options[$hash] = $option;
}
/**
* @return mixed
*/
final public function getOption()
{
return $this->option;
}
final public static function reset(): void
{
self::$options = [];
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
final class ChoiceAttr extends AbstractStaticOption
{
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
final class ChoiceFieldName extends AbstractStaticOption
{
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
final class ChoiceFilter extends AbstractStaticOption
{
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
final class ChoiceLabel extends AbstractStaticOption
{
}

View File

@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
final class ChoiceLoader extends AbstractStaticOption implements ChoiceLoaderInterface
{
/**
* {@inheritdoc}
*/
public function loadChoiceList(callable $value = null): ChoiceListInterface
{
return $this->getOption()->loadChoiceList($value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, callable $value = null): array
{
return $this->getOption()->loadChoicesForValues($values, $value);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, callable $value = null): array
{
return $this->getOption()->loadValuesForChoices($choices, $value);
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <vincentlanglet@users.noreply.github.com>
*/
final class ChoiceTranslationParameters extends AbstractStaticOption
{
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
final class ChoiceValue extends AbstractStaticOption
{
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
final class GroupBy extends AbstractStaticOption
{
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
final class PreferredChoice extends AbstractStaticOption
{
}

View File

@ -0,0 +1,254 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
* @author Jules Pietri <jules@heahprod.com>
*/
class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterface
{
private $decoratedFactory;
/**
* @var ChoiceListInterface[]
*/
private $lists = [];
/**
* @var ChoiceListView[]
*/
private $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.
*
* @param mixed $value The value to hash
*
* @return string The SHA-256 hash
*
* @internal
*/
public static function generateHash($value, string $namespace = ''): string
{
if (\is_object($value)) {
$value = spl_object_hash($value);
} elseif (\is_array($value)) {
array_walk_recursive($value, function (&$v) {
if (\is_object($v)) {
$v = spl_object_hash($v);
}
});
}
return hash('sha256', $namespace.':'.serialize($value));
}
public function __construct(ChoiceListFactoryInterface $decoratedFactory)
{
$this->decoratedFactory = $decoratedFactory;
}
/**
* Returns the decorated factory.
*
* @return ChoiceListFactoryInterface
*/
public function getDecoratedFactory()
{
return $this->decoratedFactory;
}
/**
* {@inheritdoc}
*
* @param mixed $value
* @param mixed $filter
*/
public function createListFromChoices(iterable $choices, $value = null/*, $filter = null*/)
{
$filter = \func_num_args() > 2 ? func_get_arg(2) : null;
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];
}
/**
* {@inheritdoc}
*
* @param mixed $value
* @param mixed $filter
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null/*, $filter = null*/)
{
$filter = \func_num_args() > 2 ? func_get_arg(2) : null;
$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];
}
/**
* {@inheritdoc}
*
* @param mixed $preferredChoices
* @param mixed $label
* @param mixed $index
* @param mixed $groupBy
* @param mixed $attr
* @param mixed $labelTranslationParameters
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null/*, $labelTranslationParameters = []*/)
{
$labelTranslationParameters = \func_num_args() > 6 ? func_get_arg(6) : [];
$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
);
}
$hash = self::generateHash([$list, $preferredChoices, $label, $index, $groupBy, $attr, $labelTranslationParameters]);
if (!isset($this->views[$hash])) {
$this->views[$hash] = $this->decoratedFactory->createView(
$list,
$preferredChoices,
$label,
$index,
$groupBy,
$attr,
$labelTranslationParameters
);
}
return $this->views[$hash];
}
public function reset()
{
$this->lists = [];
$this->views = [];
Cache\AbstractStaticOption::reset();
}
}

View File

@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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
*
* @return ChoiceListInterface
*/
public function createListFromChoices(iterable $choices, callable $value = null/*, callable $filter = null*/);
/**
* 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
*
* @return ChoiceListInterface
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, callable $value = null/*, callable $filter = null*/);
/**
* 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
*
* @return ChoiceListView
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, callable $index = null, callable $groupBy = null, $attr = null/*, $labelTranslationParameters = []*/);
}

View File

@ -0,0 +1,322 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Component\Translation\TranslatableMessage;
/**
* Default implementation of {@link ChoiceListFactoryInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Jules Pietri <jules@heahprod.com>
*/
class DefaultChoiceListFactory implements ChoiceListFactoryInterface
{
/**
* {@inheritdoc}
*
* @param callable|null $filter
*/
public function createListFromChoices(iterable $choices, callable $value = null/*, callable $filter = null*/)
{
$filter = \func_num_args() > 2 ? func_get_arg(2) : null;
if ($filter) {
// filter the choice list lazily
return $this->createListFromLoader(new FilterChoiceLoaderDecorator(
new CallbackChoiceLoader(static function () use ($choices) {
return $choices;
}
), $filter), $value);
}
return new ArrayChoiceList($choices, $value);
}
/**
* {@inheritdoc}
*
* @param callable|null $filter
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, callable $value = null/*, callable $filter = null*/)
{
$filter = \func_num_args() > 2 ? func_get_arg(2) : null;
if ($filter) {
$loader = new FilterChoiceLoaderDecorator($loader, $filter);
}
return new LazyChoiceList($loader, $value);
}
/**
* {@inheritdoc}
*
* @param array|callable $labelTranslationParameters The parameters used to translate the choice labels
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, callable $index = null, callable $groupBy = null, $attr = null/*, $labelTranslationParameters = []*/)
{
$labelTranslationParameters = \func_num_args() > 6 ? func_get_arg(6) : [];
$preferredViews = [];
$preferredViewsOrder = [];
$otherViews = [];
$choices = $list->getChoices();
$keys = $list->getOriginalKeys();
if (!\is_callable($preferredChoices)) {
if (empty($preferredChoices)) {
$preferredChoices = null;
} else {
// make sure we have keys that reflect order
$preferredChoices = array_values($preferredChoices);
$preferredChoices = static function ($choice) use ($preferredChoices) {
return array_search($choice, $preferredChoices, true);
};
}
}
// The names are generated from an incrementing integer by default
if (null === $index) {
$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
);
}
// 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
);
}
uksort($preferredViews, static function ($a, $b) use ($preferredViewsOrder): int {
return 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)
{
// $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 TranslatableMessage) {
$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;
}
$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)
{
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
);
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
);
}
}
private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choice, string $value, $label, array $keys, &$index, $attr, $labelTranslationParameters, ?callable $isPreferred, array &$preferredViews, array &$preferredViewsOrder, array &$otherViews)
{
$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
);
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
);
}
}
}

View File

@ -0,0 +1,239 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class PropertyAccessDecorator implements ChoiceListFactoryInterface
{
private $decoratedFactory;
private $propertyAccessor;
public function __construct(ChoiceListFactoryInterface $decoratedFactory, PropertyAccessorInterface $propertyAccessor = null)
{
$this->decoratedFactory = $decoratedFactory;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
/**
* Returns the decorated factory.
*
* @return ChoiceListFactoryInterface
*/
public function getDecoratedFactory()
{
return $this->decoratedFactory;
}
/**
* {@inheritdoc}
*
* @param mixed $value
* @param mixed $filter
*
* @return ChoiceListInterface
*/
public function createListFromChoices(iterable $choices, $value = null/*, $filter = null*/)
{
$filter = \func_num_args() > 2 ? func_get_arg(2) : null;
if (\is_string($value)) {
$value = new PropertyPath($value);
}
if ($value instanceof PropertyPathInterface) {
$accessor = $this->propertyAccessor;
$value = function ($choice) use ($accessor, $value) {
// 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.
return \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 function ($choice) use ($accessor, $filter) {
return (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter);
};
}
return $this->decoratedFactory->createListFromChoices($choices, $value, $filter);
}
/**
* {@inheritdoc}
*
* @param mixed $value
* @param mixed $filter
*
* @return ChoiceListInterface
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null/*, $filter = null*/)
{
$filter = \func_num_args() > 2 ? func_get_arg(2) : null;
if (\is_string($value)) {
$value = new PropertyPath($value);
}
if ($value instanceof PropertyPathInterface) {
$accessor = $this->propertyAccessor;
$value = function ($choice) use ($accessor, $value) {
// 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.
return \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 function ($choice) use ($accessor, $filter) {
return (\is_object($choice) || \is_array($choice)) && $accessor->getValue($choice, $filter);
};
}
return $this->decoratedFactory->createListFromLoader($loader, $value, $filter);
}
/**
* {@inheritdoc}
*
* @param mixed $preferredChoices
* @param mixed $label
* @param mixed $index
* @param mixed $groupBy
* @param mixed $attr
* @param mixed $labelTranslationParameters
*
* @return ChoiceListView
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null/*, $labelTranslationParameters = []*/)
{
$labelTranslationParameters = \func_num_args() > 6 ? func_get_arg(6) : [];
$accessor = $this->propertyAccessor;
if (\is_string($label)) {
$label = new PropertyPath($label);
}
if ($label instanceof PropertyPathInterface) {
$label = function ($choice) use ($accessor, $label) {
return $accessor->getValue($choice, $label);
};
}
if (\is_string($preferredChoices)) {
$preferredChoices = new PropertyPath($preferredChoices);
}
if ($preferredChoices instanceof PropertyPathInterface) {
$preferredChoices = function ($choice) use ($accessor, $preferredChoices) {
try {
return $accessor->getValue($choice, $preferredChoices);
} catch (UnexpectedTypeException $e) {
// Assume not preferred if not readable
return false;
}
};
}
if (\is_string($index)) {
$index = new PropertyPath($index);
}
if ($index instanceof PropertyPathInterface) {
$index = function ($choice) use ($accessor, $index) {
return $accessor->getValue($choice, $index);
};
}
if (\is_string($groupBy)) {
$groupBy = new PropertyPath($groupBy);
}
if ($groupBy instanceof PropertyPathInterface) {
$groupBy = function ($choice) use ($accessor, $groupBy) {
try {
return $accessor->getValue($choice, $groupBy);
} catch (UnexpectedTypeException $e) {
// Don't group if path is not readable
return null;
}
};
}
if (\is_string($attr)) {
$attr = new PropertyPath($attr);
}
if ($attr instanceof PropertyPathInterface) {
$attr = function ($choice) use ($accessor, $attr) {
return $accessor->getValue($choice, $attr);
};
}
if (\is_string($labelTranslationParameters)) {
$labelTranslationParameters = new PropertyPath($labelTranslationParameters);
}
if ($labelTranslationParameters instanceof PropertyPath) {
$labelTranslationParameters = static function ($choice) use ($accessor, $labelTranslationParameters) {
return $accessor->getValue($choice, $labelTranslationParameters);
};
}
return $this->decoratedFactory->createView(
$list,
$preferredChoices,
$label,
$index,
$groupBy,
$attr,
$labelTranslationParameters
);
}
}

View File

@ -0,0 +1,103 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class LazyChoiceList implements ChoiceListInterface
{
private $loader;
/**
* The callable creating string values for each choice.
*
* If null, choices are cast to strings.
*
* @var callable|null
*/
private $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 generating the choice values
*/
public function __construct(ChoiceLoaderInterface $loader, callable $value = null)
{
$this->loader = $loader;
$this->value = $value;
}
/**
* {@inheritdoc}
*/
public function getChoices()
{
return $this->loader->loadChoiceList($this->value)->getChoices();
}
/**
* {@inheritdoc}
*/
public function getValues()
{
return $this->loader->loadChoiceList($this->value)->getValues();
}
/**
* {@inheritdoc}
*/
public function getStructuredValues()
{
return $this->loader->loadChoiceList($this->value)->getStructuredValues();
}
/**
* {@inheritdoc}
*/
public function getOriginalKeys()
{
return $this->loader->loadChoiceList($this->value)->getOriginalKeys();
}
/**
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
return $this->loader->loadChoicesForValues($values, $this->value);
}
/**
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
return $this->loader->loadValuesForChoices($choices, $this->value);
}
}

View File

@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
abstract class AbstractChoiceLoader implements ChoiceLoaderInterface
{
/**
* The loaded choice list.
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* @final
*
* {@inheritdoc}
*/
public function loadChoiceList(callable $value = null): ChoiceListInterface
{
return $this->choiceList ?? ($this->choiceList = new ArrayChoiceList($this->loadChoices(), $value));
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, callable $value = null)
{
if (!$values) {
return [];
}
if ($this->choiceList) {
return $this->choiceList->getChoicesForValues($values);
}
return $this->doLoadChoicesForValues($values, $value);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, callable $value = null)
{
if (!$choices) {
return [];
}
if ($value) {
// if a value callback exists, use it
return array_map($value, $choices);
}
if ($this->choiceList) {
return $this->choiceList->getValuesForChoices($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);
}
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
class CallbackChoiceLoader extends AbstractChoiceLoader
{
private $callback;
/**
* @param callable $callback The callable returning iterable choices
*/
public function __construct(callable $callback)
{
$this->callback = $callback;
}
protected function loadChoices(): iterable
{
return ($this->callback)();
}
}

View File

@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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
*
* @return ChoiceListInterface
*/
public function loadChoiceList(callable $value = null);
/**
* 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
*
* @return array
*/
public function loadChoicesForValues(array $values, callable $value = null);
/**
* 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);
}

View File

@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
*/
class FilterChoiceLoaderDecorator extends AbstractChoiceLoader
{
private $decoratedLoader;
private $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 ($values && $filtered = array_filter($list->getChoicesForValues($values), $this->filter)) {
$choices[$group] = $filtered;
}
// filter empty groups
}
return $choices ?? [];
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, callable $value = null): array
{
return array_filter($this->decoratedLoader->loadChoicesForValues($values, $value), $this->filter);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, callable $value = null): array
{
return $this->decoratedLoader->loadValuesForChoices(array_filter($choices, $this->filter), $value);
}
}

View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <jules@heahprod.com>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class IntlCallbackChoiceLoader extends CallbackChoiceLoader
{
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, callable $value = null)
{
return parent::loadChoicesForValues(array_filter($values), $value);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, callable $value = null)
{
$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);
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*
* @implements \IteratorAggregate<array-key, ChoiceGroupView|ChoiceView>
*/
class ChoiceGroupView implements \IteratorAggregate
{
public $label;
public $choices;
/**
* Creates a new choice group view.
*
* @param array<array-key, ChoiceGroupView|ChoiceView> $choices the choice views in the group
*/
public function __construct(string $label, array $choices = [])
{
$this->label = $label;
$this->choices = $choices;
}
/**
* {@inheritdoc}
*
* @return \Traversable<array-key, ChoiceGroupView|ChoiceView>
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator($this->choices);
}
}

View File

@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class ChoiceListView
{
public $choices;
public $preferredChoices;
/**
* Creates a new choice list view.
*
* @param ChoiceGroupView[]|ChoiceView[] $choices The choice views
* @param ChoiceGroupView[]|ChoiceView[] $preferredChoices the preferred choice views
*/
public function __construct(array $choices = [], array $preferredChoices = [])
{
$this->choices = $choices;
$this->preferredChoices = $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.
*
* @return bool
*/
public function hasPlaceholder()
{
if ($this->preferredChoices) {
$firstChoice = reset($this->preferredChoices);
return $firstChoice instanceof ChoiceView && '' === $firstChoice->value;
}
$firstChoice = reset($this->choices);
return $firstChoice instanceof ChoiceView && '' === $firstChoice->value;
}
}

View File

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Component\Translation\TranslatableMessage;
/**
* Represents a choice in templates.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceView
{
public $label;
public $value;
public $data;
/**
* Additional attributes for the HTML tag.
*/
public $attr;
/**
* Additional parameters used to translate the label.
*/
public $labelTranslationParameters;
/**
* Creates a new choice view.
*
* @param mixed $data The original choice
* @param string $value The view representation of the choice
* @param string|TranslatableMessage|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($data, string $value, $label, array $attr = [], array $labelTranslationParameters = [])
{
$this->data = $data;
$this->value = $value;
$this->label = $label;
$this->attr = $attr;
$this->labelTranslationParameters = $labelTranslationParameters;
}
}

View File

@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <colinodell@gmail.com>
*/
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);
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
interface ClickableInterface
{
/**
* Returns whether this element was clicked.
*
* @return bool
*/
public function isClicked();
}

View File

@ -0,0 +1,292 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\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\Form\Console\Helper\DescriptorHelper;
use Symfony\Component\Form\Extension\Core\CoreExtension;
use Symfony\Component\Form\FormRegistryInterface;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
/**
* A console command for retrieving information about form types.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class DebugCommand extends Command
{
protected static $defaultName = 'debug:form';
protected static $defaultDescription = 'Display form type information';
private $formRegistry;
private $namespaces;
private $types;
private $extensions;
private $guessers;
private $fileLinkFormatter;
public function __construct(FormRegistryInterface $formRegistry, array $namespaces = ['Symfony\Component\Form\Extension\Core\Type'], array $types = [], array $extensions = [], array $guessers = [], FileLinkFormatter $fileLinkFormatter = null)
{
parent::__construct();
$this->formRegistry = $formRegistry;
$this->namespaces = $namespaces;
$this->types = $types;
$this->extensions = $extensions;
$this->guessers = $guessers;
$this->fileLinkFormatter = $fileLinkFormatter;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$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, 'The output format (txt or json)', 'txt'),
])
->setDescription(self::$defaultDescription)
->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
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$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, \get_class($resolvedType->getInnerType()));
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');
$loadTypesRefMethod->setAccessible(true);
$coreTypes = $loadTypesRefMethod->invoke($coreExtension);
$coreTypes = array_map(function (FormTypeInterface $type) { return \get_class($type); }, $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, function ($lev) use ($threshold) { return $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')) {
$helper = new DescriptorHelper();
$suggestions->suggestValues($helper->getFormats());
}
}
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());
}
}

View File

@ -0,0 +1,207 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <yonelceruto@gmail.com>
*
* @internal
*/
abstract class Descriptor implements DescriptorInterface
{
/** @var OutputStyle */
protected $output;
protected $type;
protected $ownOptions = [];
protected $overriddenOptions = [];
protected $parentOptions = [];
protected $extensionOptions = [];
protected $requiredOptions = [];
protected $parents = [];
protected $extensions = [];
/**
* {@inheritdoc}
*/
public function describe(OutputInterface $output, $object, array $options = [])
{
$this->output = $output instanceof OutputStyle ? $output : new SymfonyStyle(new ArrayInput([]), $output);
switch (true) {
case null === $object:
$this->describeDefaults($options);
break;
case $object instanceof ResolvedFormTypeInterface:
$this->describeResolvedFormType($object, $options);
break;
case $object instanceof OptionsResolver:
$this->describeOption($object, $options);
break;
default:
throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object)));
}
}
abstract protected function describeDefaults(array $options);
abstract protected function describeResolvedFormType(ResolvedFormTypeInterface $resolvedFormType, array $options = []);
abstract protected function describeOption(OptionsResolver $optionsResolver, array $options);
protected function collectOptions(ResolvedFormTypeInterface $type)
{
$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)
{
$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 $e) {
// noop
}
}
if (isset($definition['deprecation']) && 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)
{
$deprecatedOptions = [];
$resolver = $type->getOptionsResolver();
foreach ($resolver->getDefinedOptions() as $option) {
if ($resolver->isDeprecated($option)) {
$deprecatedOptions[] = $option;
}
}
$filterByDeprecated = 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 = \get_class($type->getInnerType())] = [];
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)
{
foreach ($type->getTypeExtensions() as $extension) {
$inheritedOptions = $optionsResolver->getDefinedOptions();
$extension->configureOptions($optionsResolver);
$this->extensions[\get_class($extension)] = array_diff($optionsResolver->getDefinedOptions(), $inheritedOptions);
}
}
}

View File

@ -0,0 +1,118 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <yonelceruto@gmail.com>
*
* @internal
*/
class JsonDescriptor extends Descriptor
{
protected function describeDefaults(array $options)
{
$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 = [])
{
$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' => \get_class($resolvedFormType->getInnerType()),
'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)
{
$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)
{
$flags = $options['json_encoding'] ?? 0;
$this->output->write(json_encode($data, $flags | \JSON_PRETTY_PRINT)."\n");
}
private function sortOptions(array &$options)
{
foreach ($options as &$opts) {
$sorted = false;
foreach ($opts as &$opt) {
if (\is_array($opt)) {
sort($opt);
$sorted = true;
}
}
if (!$sorted) {
sort($opts);
}
}
}
}

View File

@ -0,0 +1,222 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Form\ResolvedFormTypeInterface;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*
* @internal
*/
class TextDescriptor extends Descriptor
{
private $fileLinkFormatter;
public function __construct(FileLinkFormatter $fileLinkFormatter = null)
{
$this->fileLinkFormatter = $fileLinkFormatter;
}
protected function describeDefaults(array $options)
{
if ($options['core_types']) {
$this->output->section('Built-in form types (Symfony\Component\Form\Extension\Core\Type)');
$shortClassNames = array_map(function ($fqcn) {
return $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 = [])
{
$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")', \get_class($resolvedFormType->getInnerType()), $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)
{
$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)', \get_class($options['type']), $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
{
if (null === $text) {
$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 $e) {
return '';
}
return (string) $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine());
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Form\Console\Descriptor\JsonDescriptor;
use Symfony\Component\Form\Console\Descriptor\TextDescriptor;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*
* @internal
*/
class DescriptorHelper extends BaseDescriptorHelper
{
public function __construct(FileLinkFormatter $fileLinkFormatter = null)
{
$this
->register('txt', new TextDescriptor($fileLinkFormatter))
->register('json', new JsonDescriptor())
;
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <yonelceruto@gmail.com>
*/
interface DataAccessorInterface
{
/**
* Returns the value at the end of the property of the object graph.
*
* @param object|array $viewData The view data of the compound form
* @param FormInterface $form The {@link FormInterface()} instance to check
*
* @return mixed
*
* @throws Exception\AccessException If unable to read from the given form data
*/
public function getValue($viewData, FormInterface $form);
/**
* Sets the value at the end of the property of the object graph.
*
* @param object|array $viewData The view data of the compound form
* @param mixed $value The value to set at the end of the object graph
* @param FormInterface $form The {@link FormInterface()} instance to check
*
* @throws Exception\AccessException If unable to write the given value
*/
public function setValue(&$viewData, $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.
*
* @param object|array $viewData The view data of the compound form
* @param FormInterface $form The {@link FormInterface()} instance to check
*/
public function isReadable($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.
*
* @param object|array $viewData The view data of the compound form
* @param FormInterface $form The {@link FormInterface()} instance to check
*/
public function isWritable($viewData, FormInterface $form): bool;
}

View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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 FormInterface[]|\Traversable $forms A list of {@link FormInterface} instances
*
* @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported
*/
public function mapDataToForms($viewData, \Traversable $forms);
/**
* 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 FormInterface[]|\Traversable $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, &$viewData);
}

View File

@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
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 mixed $value The value in the original representation
*
* @return mixed
*
* @throws TransformationFailedException when the transformation fails
*/
public function transform($value);
/**
* 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 mixed $value The value in the transformed representation
*
* @return mixed
*
* @throws TransformationFailedException when the transformation fails
*/
public function reverseTransform($value);
}

View File

@ -0,0 +1,146 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class FormPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;
private $formExtensionService;
private $formTypeTag;
private $formTypeExtensionTag;
private $formTypeGuesserTag;
private $formDebugCommandService;
public function __construct(string $formExtensionService = 'form.extension', string $formTypeTag = 'form.type', string $formTypeExtensionTag = 'form.type_extension', string $formTypeGuesserTag = 'form.type_guesser', string $formDebugCommandService = 'console.command.form_debug')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/http-kernel', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->formExtensionService = $formExtensionService;
$this->formTypeTag = $formTypeTag;
$this->formTypeExtensionTag = $formTypeExtensionTag;
$this->formTypeGuesserTag = $formTypeGuesserTag;
$this->formDebugCommandService = $formDebugCommandService;
}
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->formExtensionService)) {
return;
}
$definition = $container->getDefinition($this->formExtensionService);
$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];
// Builds an array with fully-qualified type class names as keys and service IDs as values
foreach ($container->findTaggedServiceIds($this->formTypeTag, 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 ($container->hasDefinition($this->formDebugCommandService)) {
$commandDefinition = $container->getDefinition($this->formDebugCommandService);
$commandDefinition->setArgument(1, array_keys($namespaces));
$commandDefinition->setArgument(2, array_keys($servicesMap));
}
return ServiceLocatorTagPass::register($container, $servicesMap);
}
private function processFormTypeExtensions(ContainerBuilder $container): array
{
$typeExtensions = [];
$typeExtensionsClasses = [];
foreach ($this->findAndSortTaggedServices($this->formTypeExtensionTag, $container) as $reference) {
$serviceId = (string) $reference;
$serviceDefinition = $container->getDefinition($serviceId);
$tag = $serviceDefinition->getTag($this->formTypeExtensionTag);
$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($this->formDebugCommandService)) {
$commandDefinition = $container->getDefinition($this->formDebugCommandService);
$commandDefinition->setArgument(3, $typeExtensionsClasses);
}
return $typeExtensions;
}
private function processFormTypeGuessers(ContainerBuilder $container): ArgumentInterface
{
$guessers = [];
$guessersClasses = [];
foreach ($container->findTaggedServiceIds($this->formTypeGuesserTag, true) as $serviceId => $tags) {
$guessers[] = new Reference($serviceId);
$serviceDefinition = $container->getDefinition($serviceId);
$guessersClasses[] = $serviceDefinition->getClass();
}
if ($container->hasDefinition($this->formDebugCommandService)) {
$commandDefinition = $container->getDefinition($this->formDebugCommandService);
$commandDefinition->setArgument(4, $guessersClasses);
}
return new IteratorArgument($guessers);
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 end of the Form::setData() method.
*
* This event is mostly here for reading data after having pre-populated the form.
*/
final class PostSetDataEvent extends FormEvent
{
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 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
{
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
* - Modify a form depending on the pre-populated data (adding or removing fields dynamically).
*/
final class PreSetDataEvent extends FormEvent
{
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
{
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
{
}

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class AlreadySubmittedException extends LogicException
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <aleksander.kot@gmail.com>
*/
class LogicException extends \LogicException implements ExceptionInterface
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <aleksander.kot@gmail.com>
*/
class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
{
}

View File

@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class TransformationFailedException extends RuntimeException
{
private $invalidMessage;
private $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 = null, array $invalidMessageParameters = []): void
{
$this->invalidMessage = $invalidMessage;
$this->invalidMessageParameters = $invalidMessageParameters;
}
public function getInvalidMessage(): ?string
{
return $this->invalidMessage;
}
public function getInvalidMessageParameters(): array
{
return $this->invalidMessageParameters;
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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($value, string $expectedType)
{
parent::__construct(sprintf('Expected argument of type "%s", "%s" given', $expectedType, get_debug_type($value)));
}
}

View File

@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class CoreExtension extends AbstractExtension
{
private $propertyAccessor;
private $choiceListFactory;
private $translator;
public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null, TranslatorInterface $translator = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->choiceListFactory = $choiceListFactory ?? new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
$this->translator = $translator;
}
protected function loadTypes()
{
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()
{
return [
new TransformationFailureExtension($this->translator),
];
}
}

View File

@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <yonelceruto@gmail.com>
*/
class CallbackAccessor implements DataAccessorInterface
{
/**
* {@inheritdoc}
*/
public function getValue($data, FormInterface $form)
{
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);
}
/**
* {@inheritdoc}
*/
public function setValue(&$data, $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);
}
/**
* {@inheritdoc}
*/
public function isReadable($data, FormInterface $form): bool
{
return null !== $form->getConfig()->getOption('getter');
}
/**
* {@inheritdoc}
*/
public function isWritable($data, FormInterface $form): bool
{
return null !== $form->getConfig()->getOption('setter');
}
}

View File

@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <yonelceruto@gmail.com>
*/
class ChainAccessor implements DataAccessorInterface
{
private $accessors;
/**
* @param DataAccessorInterface[]|iterable $accessors
*/
public function __construct(iterable $accessors)
{
$this->accessors = $accessors;
}
/**
* {@inheritdoc}
*/
public function getValue($data, FormInterface $form)
{
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.');
}
/**
* {@inheritdoc}
*/
public function setValue(&$data, $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.');
}
/**
* {@inheritdoc}
*/
public function isReadable($data, FormInterface $form): bool
{
foreach ($this->accessors as $accessor) {
if ($accessor->isReadable($data, $form)) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function isWritable($data, FormInterface $form): bool
{
foreach ($this->accessors as $accessor) {
if ($accessor->isWritable($data, $form)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,103 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
use Symfony\Component\PropertyAccess\Exception\AccessException as PropertyAccessException;
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 <yonelceruto@gmail.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathAccessor implements DataAccessorInterface
{
private $propertyAccessor;
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
}
/**
* {@inheritdoc}
*/
public function getValue($data, FormInterface $form)
{
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);
}
/**
* {@inheritdoc}
*/
public function setValue(&$data, $propertyValue, FormInterface $form): void
{
if (null === $propertyPath = $form->getPropertyPath()) {
throw new AccessException('Unable to write the given value as no property path is defined.');
}
// If the field is of type DateTimeInterface and the data is the same skip the update to
// keep the original object hash
if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $this->getPropertyValue($data, $propertyPath)) {
return;
}
// If the data is identical to the value in $data, we are
// dealing with a reference
if (!\is_object($data) || !$form->getConfig()->getByReference() || $propertyValue !== $this->getPropertyValue($data, $propertyPath)) {
$this->propertyAccessor->setValue($data, $propertyPath, $propertyValue);
}
}
/**
* {@inheritdoc}
*/
public function isReadable($data, FormInterface $form): bool
{
return null !== $form->getPropertyPath();
}
/**
* {@inheritdoc}
*/
public function isWritable($data, FormInterface $form): bool
{
return null !== $form->getPropertyPath();
}
private function getPropertyValue($data, PropertyPathInterface $propertyPath)
{
try {
return $this->propertyAccessor->getValue($data, $propertyPath);
} catch (PropertyAccessException $e) {
if (!$e instanceof UninitializedPropertyException
// For versions without UninitializedPropertyException check the exception message
&& (class_exists(UninitializedPropertyException::class) || false === strpos($e->getMessage(), 'You should initialize it'))
) {
throw $e;
}
return null;
}
}
}

View File

@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class CheckboxListMapper implements DataMapperInterface
{
/**
* {@inheritdoc}
*/
public function mapDataToForms($choices, iterable $checkboxes)
{
if (\is_array($checkboxes)) {
trigger_deprecation('symfony/form', '5.3', 'Passing an array as the second argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__);
}
if (null === $choices) {
$choices = [];
}
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));
}
}
/**
* {@inheritdoc}
*/
public function mapFormsToData(iterable $checkboxes, &$choices)
{
if (\is_array($checkboxes)) {
trigger_deprecation('symfony/form', '5.3', 'Passing an array as the first argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__);
}
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;
}
}

View File

@ -0,0 +1,91 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class DataMapper implements DataMapperInterface
{
private $dataAccessor;
public function __construct(DataAccessorInterface $dataAccessor = null)
{
$this->dataAccessor = $dataAccessor ?? new ChainAccessor([
new CallbackAccessor(),
new PropertyPathAccessor(),
]);
}
/**
* {@inheritdoc}
*/
public function mapDataToForms($data, iterable $forms): void
{
if (\is_array($forms)) {
trigger_deprecation('symfony/form', '5.3', 'Passing an array as the second argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__);
}
$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());
}
}
}
/**
* {@inheritdoc}
*/
public function mapFormsToData(iterable $forms, &$data): void
{
if (\is_array($forms)) {
trigger_deprecation('symfony/form', '5.3', 'Passing an array as the first argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__);
}
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);
}
}
}
}

View File

@ -0,0 +1,113 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
trigger_deprecation('symfony/form', '5.2', 'The "%s" class is deprecated. Use "%s" instead.', PropertyPathMapper::class, DataMapper::class);
/**
* Maps arrays/objects to/from forms using property paths.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since symfony/form 5.2. Use {@see DataMapper} instead.
*/
class PropertyPathMapper implements DataMapperInterface
{
private $propertyAccessor;
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
/**
* {@inheritdoc}
*/
public function mapDataToForms($data, iterable $forms)
{
$empty = null === $data || [] === $data;
if (!$empty && !\is_array($data) && !\is_object($data)) {
throw new UnexpectedTypeException($data, 'object, array or empty');
}
foreach ($forms as $form) {
$propertyPath = $form->getPropertyPath();
$config = $form->getConfig();
if (!$empty && null !== $propertyPath && $config->getMapped()) {
$form->setData($this->getPropertyValue($data, $propertyPath));
} else {
$form->setData($config->getData());
}
}
}
/**
* {@inheritdoc}
*/
public function mapFormsToData(iterable $forms, &$data)
{
if (null === $data) {
return;
}
if (!\is_array($data) && !\is_object($data)) {
throw new UnexpectedTypeException($data, 'object, array or empty');
}
foreach ($forms as $form) {
$propertyPath = $form->getPropertyPath();
$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 (null !== $propertyPath && $config->getMapped() && $form->isSubmitted() && $form->isSynchronized() && !$form->isDisabled()) {
$propertyValue = $form->getData();
// If the field is of type DateTimeInterface and the data is the same skip the update to
// keep the original object hash
if ($propertyValue instanceof \DateTimeInterface && $propertyValue == $this->getPropertyValue($data, $propertyPath)) {
continue;
}
// If the data is identical to the value in $data, we are
// dealing with a reference
if (!\is_object($data) || !$config->getByReference() || $propertyValue !== $this->getPropertyValue($data, $propertyPath)) {
$this->propertyAccessor->setValue($data, $propertyPath, $propertyValue);
}
}
}
}
private function getPropertyValue($data, $propertyPath)
{
try {
return $this->propertyAccessor->getValue($data, $propertyPath);
} catch (AccessException $e) {
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;
}
}
}

View File

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class RadioListMapper implements DataMapperInterface
{
/**
* {@inheritdoc}
*/
public function mapDataToForms($choice, iterable $radios)
{
if (\is_array($radios)) {
trigger_deprecation('symfony/form', '5.3', 'Passing an array as the second argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__);
}
if (!\is_string($choice)) {
throw new UnexpectedTypeException($choice, 'string');
}
foreach ($radios as $radio) {
$value = $radio->getConfig()->getOption('value');
$radio->setData($choice === $value);
}
}
/**
* {@inheritdoc}
*/
public function mapFormsToData(iterable $radios, &$choice)
{
if (\is_array($radios)) {
trigger_deprecation('symfony/form', '5.3', 'Passing an array as the first argument of the "%s()" method is deprecated, pass "\Traversable" instead.', __METHOD__);
}
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;
}
}
}
}

View File

@ -0,0 +1,84 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class ArrayToPartsTransformer implements DataTransformerInterface
{
private $partMapping;
public function __construct(array $partMapping)
{
$this->partMapping = $partMapping;
}
public function transform($array)
{
if (null === $array) {
$array = [];
}
if (!\is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
$result = [];
foreach ($this->partMapping as $partKey => $originalKeys) {
if (empty($array)) {
$result[$partKey] = null;
} else {
$result[$partKey] = array_intersect_key($array, array_flip($originalKeys));
}
}
return $result;
}
public function reverseTransform($array)
{
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;
}
}

View File

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
abstract class BaseDateTimeTransformer implements DataTransformerInterface
{
protected static $formats = [
\IntlDateFormatter::NONE,
\IntlDateFormatter::FULL,
\IntlDateFormatter::LONG,
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::SHORT,
];
protected $inputTimezone;
protected $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);
}
}
}

View File

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class BooleanToStringTransformer implements DataTransformerInterface
{
private $trueValue;
private $falseValues;
/**
* @param string $trueValue The value emitted upon transform if the input is true
*/
public function __construct(string $trueValue, array $falseValues = [null])
{
$this->trueValue = $trueValue;
$this->falseValues = $falseValues;
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
*
* @return string|null
*
* @throws TransformationFailedException if the given value is not a Boolean
*/
public function transform($value)
{
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
*
* @return bool
*
* @throws TransformationFailedException if the given value is not a string
*/
public function reverseTransform($value)
{
if (\in_array($value, $this->falseValues, true)) {
return false;
}
if (!\is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
return true;
}
}

View File

@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class ChoiceToValueTransformer implements DataTransformerInterface
{
private $choiceList;
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
public function transform($choice)
{
return (string) current($this->choiceList->getValuesForChoices([$choice]));
}
public function reverseTransform($value)
{
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);
}
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class ChoicesToValuesTransformer implements DataTransformerInterface
{
private $choiceList;
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
/**
* @return array
*
* @throws TransformationFailedException if the given value is not an array
*/
public function transform($array)
{
if (null === $array) {
return [];
}
if (!\is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
return $this->choiceList->getValuesForChoices($array);
}
/**
* @return 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($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;
}
}

View File

@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class DataTransformerChain implements DataTransformerInterface
{
protected $transformers;
/**
* Uses the given value transformers to transform values.
*
* @param DataTransformerInterface[] $transformers
*/
public function __construct(array $transformers)
{
$this->transformers = $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
*
* @return mixed
*
* @throws TransformationFailedException
*/
public function transform($value)
{
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
*
* @return mixed
*
* @throws TransformationFailedException
*/
public function reverseTransform($value)
{
for ($i = \count($this->transformers) - 1; $i >= 0; --$i) {
$value = $this->transformers[$i]->reverseTransform($value);
}
return $value;
}
/**
* @return DataTransformerInterface[]
*/
public function getTransformers()
{
return $this->transformers;
}
}

View File

@ -0,0 +1,171 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <steffen.rosskamp@gimmickmedia.de>
*/
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 $fields;
private $pad;
/**
* @param string[]|null $fields The date fields
* @param bool $pad Whether to use padding
*/
public function __construct(array $fields = null, bool $pad = false)
{
$this->fields = $fields ?? ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'invert'];
$this->pad = $pad;
}
/**
* Transforms a normalized date interval into an interval array.
*
* @param \DateInterval $dateInterval Normalized date interval
*
* @return array
*
* @throws UnexpectedTypeException if the given value is not a \DateInterval instance
*/
public function transform($dateInterval)
{
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'];
$result = array_intersect_key($result, array_flip($this->fields));
return $result;
}
/**
* Transforms an interval array into a normalized date interval.
*
* @param array $value Interval array
*
* @return \DateInterval|null
*
* @throws UnexpectedTypeException if the given value is not an array
* @throws TransformationFailedException if the value could not be transformed
*/
public function reverseTransform($value)
{
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'],
empty($value['weeks']) ? '0' : $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;
}
}

View File

@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <steffen.rosskamp@gimmickmedia.de>
*/
class DateIntervalToStringTransformer implements DataTransformerInterface
{
private $format;
/**
* Transforms a \DateInterval instance to a string.
*
* @see \DateInterval::format() for supported formats
*
* @param string $format The date format
*/
public function __construct(string $format = 'P%yY%mM%dDT%hH%iM%sS')
{
$this->format = $format;
}
/**
* Transforms a DateInterval object into a date string with the configured format.
*
* @param \DateInterval|null $value A DateInterval object
*
* @return string
*
* @throws UnexpectedTypeException if the given value is not a \DateInterval instance
*/
public function transform($value)
{
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
*
* @return \DateInterval|null
*
* @throws UnexpectedTypeException if the given value is not a string
* @throws TransformationFailedException if the date interval could not be parsed
*/
public function reverseTransform($value)
{
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);
}
}

View File

@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <udaltsov.valentin@gmail.com>
*/
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($value): ?\DateTime
{
if (null === $value) {
return null;
}
if (!$value instanceof \DateTimeImmutable) {
throw new TransformationFailedException('Expected a \DateTimeImmutable.');
}
if (\PHP_VERSION_ID >= 70300) {
return \DateTime::createFromImmutable($value);
}
return \DateTime::createFromFormat('U.u', $value->format('U.u'))->setTimezone($value->getTimezone());
}
/**
* 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($value): ?\DateTimeImmutable
{
if (null === $value) {
return null;
}
if (!$value instanceof \DateTime) {
throw new TransformationFailedException('Expected a \DateTime.');
}
return \DateTimeImmutable::createFromMutable($value);
}
}

View File

@ -0,0 +1,184 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class DateTimeToArrayTransformer extends BaseDateTimeTransformer
{
private $pad;
private $fields;
private $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, bool $pad = false, \DateTimeInterface $referenceDate = null)
{
parent::__construct($inputTimezone, $outputTimezone);
$this->fields = $fields ?? ['year', 'month', 'day', 'hour', 'minute', 'second'];
$this->pad = $pad;
$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
*
* @return array
*
* @throws TransformationFailedException If the given value is not a \DateTimeInterface
*/
public function transform($dateTime)
{
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) {
if (!$dateTime instanceof \DateTimeImmutable) {
$dateTime = clone $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
*
* @return \DateTime|null
*
* @throws TransformationFailedException If the given value is not an array,
* if the value could not be transformed
*/
public function reverseTransform($value)
{
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;
}
}

View File

@ -0,0 +1,106 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <franz.wilding@me.com>
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Fred Cox <mcfedr@gmail.com>
*/
class DateTimeToHtml5LocalDateTimeTransformer extends BaseDateTimeTransformer
{
public const HTML5_FORMAT = 'Y-m-d\\TH:i:s';
/**
* 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 \DateTime|\DateTimeInterface $dateTime A DateTime object
*
* @return string
*
* @throws TransformationFailedException If the given value is not an
* instance of \DateTime or \DateTimeInterface
*/
public function transform($dateTime)
{
if (null === $dateTime) {
return '';
}
if (!$dateTime instanceof \DateTime && !$dateTime instanceof \DateTimeInterface) {
throw new TransformationFailedException('Expected a \DateTime or \DateTimeInterface.');
}
if ($this->inputTimezone !== $this->outputTimezone) {
if (!$dateTime instanceof \DateTimeImmutable) {
$dateTime = clone $dateTime;
}
$dateTime = $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
}
return $dateTime->format(self::HTML5_FORMAT);
}
/**
* 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
*
* @return \DateTime|null
*
* @throws TransformationFailedException If the given value is not a string,
* if the value could not be transformed
*/
public function reverseTransform($dateTimeLocal)
{
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;
}
}

View File

@ -0,0 +1,208 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* Transforms between a normalized time and a localized time string.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
{
private $dateFormat;
private $timeFormat;
private $pattern;
private $calendar;
/**
* @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 $calendar One of the \IntlDateFormatter calendar constants
* @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, int $calendar = \IntlDateFormatter::GREGORIAN, string $pattern = null)
{
parent::__construct($inputTimezone, $outputTimezone);
if (null === $dateFormat) {
$dateFormat = \IntlDateFormatter::MEDIUM;
}
if (null === $timeFormat) {
$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));
}
$this->dateFormat = $dateFormat;
$this->timeFormat = $timeFormat;
$this->calendar = $calendar;
$this->pattern = $pattern;
}
/**
* Transforms a normalized date into a localized date string/array.
*
* @param \DateTimeInterface $dateTime A DateTimeInterface object
*
* @return string
*
* @throws TransformationFailedException if the given value is not a \DateTimeInterface
* or if the date could not be transformed
*/
public function transform($dateTime)
{
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|array $value Localized date string/array
*
* @return \DateTime|null
*
* @throws TransformationFailedException if the given value is not a string,
* if the date could not be parsed
*/
public function reverseTransform($value)
{
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
*
* @return \IntlDateFormatter
*
* @throws TransformationFailedException in case the date formatter cannot be constructed
*/
protected function getIntlDateFormatter(bool $ignoreTimezone = false)
{
$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 ?? '');
// new \intlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/66323
if (!$intlDateFormatter) {
throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code());
}
$intlDateFormatter->setLenient(false);
return $intlDateFormatter;
}
/**
* Checks if the pattern contains only a date.
*
* @return bool
*/
protected function isPatternDateOnly()
{
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);
}
}

View File

@ -0,0 +1,91 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class DateTimeToRfc3339Transformer extends BaseDateTimeTransformer
{
/**
* Transforms a normalized date into a localized date.
*
* @param \DateTimeInterface $dateTime A DateTimeInterface object
*
* @return string
*
* @throws TransformationFailedException If the given value is not a \DateTimeInterface
*/
public function transform($dateTime)
{
if (null === $dateTime) {
return '';
}
if (!$dateTime instanceof \DateTimeInterface) {
throw new TransformationFailedException('Expected a \DateTimeInterface.');
}
if ($this->inputTimezone !== $this->outputTimezone) {
if (!$dateTime instanceof \DateTimeImmutable) {
$dateTime = clone $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
*
* @return \DateTime|null
*
* @throws TransformationFailedException If the given value is not a string,
* if the value could not be transformed
*/
public function reverseTransform($rfc3339)
{
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;
}
}

View File

@ -0,0 +1,138 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class DateTimeToStringTransformer extends BaseDateTimeTransformer
{
/**
* Format used for generating strings.
*
* @var string
*/
private $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.
*
* @var string
*/
private $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
*/
public function __construct(string $inputTimezone = null, string $outputTimezone = null, string $format = 'Y-m-d H:i:s')
{
parent::__construct($inputTimezone, $outputTimezone);
$this->generateFormat = $this->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
*
* @return string
*
* @throws TransformationFailedException If the given value is not a \DateTimeInterface
*/
public function transform($dateTime)
{
if (null === $dateTime) {
return '';
}
if (!$dateTime instanceof \DateTimeInterface) {
throw new TransformationFailedException('Expected a \DateTimeInterface.');
}
if (!$dateTime instanceof \DateTimeImmutable) {
$dateTime = clone $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
*
* @return \DateTime|null
*
* @throws TransformationFailedException If the given value is not a string,
* or could not be transformed
*/
public function reverseTransform($value)
{
if (empty($value)) {
return null;
}
if (!\is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
$outputTz = new \DateTimeZone($this->outputTimezone);
$dateTime = \DateTime::createFromFormat($this->parseFormat, $value, $outputTz);
$lastErrors = \DateTime::getLastErrors();
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;
}
}

View File

@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class DateTimeToTimestampTransformer extends BaseDateTimeTransformer
{
/**
* Transforms a DateTime object into a timestamp in the configured timezone.
*
* @param \DateTimeInterface $dateTime A DateTimeInterface object
*
* @return int|null
*
* @throws TransformationFailedException If the given value is not a \DateTimeInterface
*/
public function transform($dateTime)
{
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
*
* @return \DateTime|null
*
* @throws TransformationFailedException If the given value is not a timestamp
* or if the given timestamp is invalid
*/
public function reverseTransform($value)
{
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;
}
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <franssen.roland@gmail.com>
*/
class DateTimeZoneToStringTransformer implements DataTransformerInterface
{
private $multiple;
public function __construct(bool $multiple = false)
{
$this->multiple = $multiple;
}
/**
* {@inheritdoc}
*/
public function transform($dateTimeZone)
{
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();
}
/**
* {@inheritdoc}
*/
public function reverseTransform($value)
{
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);
}
}
}

View File

@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class IntegerToLocalizedStringTransformer extends NumberToLocalizedStringTransformer
{
/**
* Constructs a transformer.
*
* @param bool $grouping Whether thousands should be grouped
* @param int $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);
}
/**
* {@inheritdoc}
*/
public function reverseTransform($value)
{
$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($value)
{
return $value;
}
}

View File

@ -0,0 +1,84 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <franssen.roland@gmail.com>
*/
class IntlTimeZoneToStringTransformer implements DataTransformerInterface
{
private $multiple;
public function __construct(bool $multiple = false)
{
$this->multiple = $multiple;
}
/**
* {@inheritdoc}
*/
public function transform($intlTimeZone)
{
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();
}
/**
* {@inheritdoc}
*/
public function reverseTransform($value)
{
if (null === $value) {
return;
}
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;
}
}

View File

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransformer
{
private $divisor;
public function __construct(?int $scale = 2, ?bool $grouping = true, ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, ?int $divisor = 1, string $locale = null)
{
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
*
* @return string
*
* @throws TransformationFailedException if the given value is not numeric or
* if the value cannot be transformed
*/
public function transform($value)
{
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
*
* @return int|float|null
*
* @throws TransformationFailedException if the given value is not a string
* or if the value cannot be transformed
*/
public function reverseTransform($value)
{
$value = parent::reverseTransform($value);
if (null !== $value && 1 !== $this->divisor) {
$value = (float) (string) ($value * $this->divisor);
}
return $value;
}
}

View File

@ -0,0 +1,265 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class NumberToLocalizedStringTransformer implements DataTransformerInterface
{
/**
* @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_CEILING instead.
*/
public const ROUND_CEILING = \NumberFormatter::ROUND_CEILING;
/**
* @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_FLOOR instead.
*/
public const ROUND_FLOOR = \NumberFormatter::ROUND_FLOOR;
/**
* @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_UP instead.
*/
public const ROUND_UP = \NumberFormatter::ROUND_UP;
/**
* @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_DOWN instead.
*/
public const ROUND_DOWN = \NumberFormatter::ROUND_DOWN;
/**
* @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_HALFEVEN instead.
*/
public const ROUND_HALF_EVEN = \NumberFormatter::ROUND_HALFEVEN;
/**
* @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_HALFUP instead.
*/
public const ROUND_HALF_UP = \NumberFormatter::ROUND_HALFUP;
/**
* @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_HALFDOWN instead.
*/
public const ROUND_HALF_DOWN = \NumberFormatter::ROUND_HALFDOWN;
protected $grouping;
protected $roundingMode;
private $scale;
private $locale;
public function __construct(int $scale = null, ?bool $grouping = false, ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, string $locale = null)
{
$this->scale = $scale;
$this->grouping = $grouping ?? false;
$this->roundingMode = $roundingMode ?? \NumberFormatter::ROUND_HALFUP;
$this->locale = $locale;
}
/**
* Transforms a number type into localized number.
*
* @param int|float|null $value Number value
*
* @return string
*
* @throws TransformationFailedException if the given value is not numeric
* or if the value cannot be transformed
*/
public function transform($value)
{
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
$value = str_replace(["\xc2\xa0", "\xe2\x80\xaf"], ' ', $value);
return $value;
}
/**
* Transforms a localized number into an integer or float.
*
* @param string $value The localized value
*
* @return int|float|null
*
* @throws TransformationFailedException if the given value is not a string
* or if the value cannot be transformed
*/
public function reverseTransform($value)
{
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 (str_contains($value, $decSep)) {
$type = \NumberFormatter::TYPE_DOUBLE;
} else {
$type = \PHP_INT_SIZE === 8
? \NumberFormatter::TYPE_INT64
: \NumberFormatter::TYPE_INT32;
}
$result = $formatter->parse($value, $type, $position);
if (intl_is_failure($formatter->getErrorCode())) {
throw new TransformationFailedException($formatter->getErrorMessage());
}
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.
*
* @return \NumberFormatter
*/
protected function getNumberFormatter()
{
$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($value)
{
if (\is_int($value) && $value === (int) $float = (float) $value) {
return $float;
}
return $value;
}
/**
* Rounds a number according to the configured scale and rounding mode.
*
* @param int|float $number A number
*
* @return int|float
*/
private function round($number)
{
if (null !== $this->scale && null !== $this->roundingMode) {
// 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);
switch ($this->roundingMode) {
case \NumberFormatter::ROUND_CEILING:
$number = ceil($number);
break;
case \NumberFormatter::ROUND_FLOOR:
$number = floor($number);
break;
case \NumberFormatter::ROUND_UP:
$number = $number > 0 ? ceil($number) : floor($number);
break;
case \NumberFormatter::ROUND_DOWN:
$number = $number > 0 ? floor($number) : ceil($number);
break;
case \NumberFormatter::ROUND_HALFEVEN:
$number = round($number, 0, \PHP_ROUND_HALF_EVEN);
break;
case \NumberFormatter::ROUND_HALFUP:
$number = round($number, 0, \PHP_ROUND_HALF_UP);
break;
case \NumberFormatter::ROUND_HALFDOWN:
$number = round($number, 0, \PHP_ROUND_HALF_DOWN);
break;
}
$number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef;
}
return $number;
}
}

View File

@ -0,0 +1,249 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class PercentToLocalizedStringTransformer implements DataTransformerInterface
{
public const FRACTIONAL = 'fractional';
public const INTEGER = 'integer';
protected static $types = [
self::FRACTIONAL,
self::INTEGER,
];
private $roundingMode;
private $type;
private $scale;
private $html5Format;
/**
* @see self::$types for a list of supported types
*
* @param int|null $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, int $roundingMode = null, bool $html5Format = false)
{
if (null === $type) {
$type = self::FRACTIONAL;
}
if (null === $roundingMode && (\func_num_args() < 4 || func_get_arg(3))) {
trigger_deprecation('symfony/form', '5.1', 'Not passing a rounding mode to "%s()" is deprecated. Starting with Symfony 6.0 it will default to "\NumberFormatter::ROUND_HALFUP".', __METHOD__);
}
if (!\in_array($type, self::$types, true)) {
throw new UnexpectedTypeException($type, implode('", "', self::$types));
}
$this->type = $type;
$this->scale = $scale ?? 0;
$this->roundingMode = $roundingMode;
$this->html5Format = $html5Format;
}
/**
* Transforms between a normalized format (integer or float) into a percentage value.
*
* @param int|float $value Normalized value
*
* @return string
*
* @throws TransformationFailedException if the given value is not numeric or
* if the value could not be transformed
*/
public function transform($value)
{
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
*
* @return int|float|null
*
* @throws TransformationFailedException if the given value is not a string or
* if the value could not be transformed
*/
public function reverseTransform($value)
{
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;
}
// replace normal spaces so that the formatter can read them
$result = $formatter->parse(str_replace(' ', "\xc2\xa0", $value), $type, $position);
if (intl_is_failure($formatter->getErrorCode())) {
throw new TransformationFailedException($formatter->getErrorMessage());
}
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.
*
* @return \NumberFormatter
*/
protected function getNumberFormatter()
{
// 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);
if (null !== $this->roundingMode) {
$formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
}
return $formatter;
}
/**
* Rounds a number according to the configured scale and rounding mode.
*
* @param int|float $number A number
*
* @return int|float
*/
private function round($number)
{
if (null !== $this->scale && null !== $this->roundingMode) {
// 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);
switch ($this->roundingMode) {
case \NumberFormatter::ROUND_CEILING:
$number = ceil($number);
break;
case \NumberFormatter::ROUND_FLOOR:
$number = floor($number);
break;
case \NumberFormatter::ROUND_UP:
$number = $number > 0 ? ceil($number) : floor($number);
break;
case \NumberFormatter::ROUND_DOWN:
$number = $number > 0 ? floor($number) : ceil($number);
break;
case \NumberFormatter::ROUND_HALFEVEN:
$number = round($number, 0, \PHP_ROUND_HALF_EVEN);
break;
case \NumberFormatter::ROUND_HALFUP:
$number = round($number, 0, \PHP_ROUND_HALF_UP);
break;
case \NumberFormatter::ROUND_HALFDOWN:
$number = round($number, 0, \PHP_ROUND_HALF_DOWN);
break;
}
$number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef;
}
return $number;
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
class StringToFloatTransformer implements DataTransformerInterface
{
private $scale;
public function __construct(int $scale = null)
{
$this->scale = $scale;
}
/**
* @param mixed $value
*
* @return float|null
*/
public function transform($value)
{
if (null === $value) {
return null;
}
if (!\is_string($value) || !is_numeric($value)) {
throw new TransformationFailedException('Expected a numeric string.');
}
return (float) $value;
}
/**
* @param mixed $value
*
* @return string|null
*/
public function reverseTransform($value)
{
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;
}
}

View File

@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <wapinet@mail.ru>
*/
class UlidToStringTransformer implements DataTransformerInterface
{
/**
* Transforms a Ulid object into a string.
*
* @param Ulid $value A Ulid object
*
* @return string|null
*
* @throws TransformationFailedException If the given value is not a Ulid object
*/
public function transform($value)
{
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
*
* @return Ulid|null
*
* @throws TransformationFailedException If the given value is not a string,
* or could not be transformed
*/
public function reverseTransform($value)
{
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;
}
}

View File

@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <wapinet@mail.ru>
*/
class UuidToStringTransformer implements DataTransformerInterface
{
/**
* Transforms a Uuid object into a string.
*
* @param Uuid $value A Uuid object
*
* @return string|null
*
* @throws TransformationFailedException If the given value is not a Uuid object
*/
public function transform($value)
{
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
*
* @return Uuid|null
*
* @throws TransformationFailedException If the given value is not a string,
* or could not be transformed
*/
public function reverseTransform($value)
{
if (null === $value || '' === $value) {
return null;
}
if (!\is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
try {
$uuid = new Uuid($value);
} catch (\InvalidArgumentException $e) {
throw new TransformationFailedException(sprintf('The value "%s" is not a valid UUID.', $value), $e->getCode(), $e);
}
return $uuid;
}
}

View File

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class ValueToDuplicatesTransformer implements DataTransformerInterface
{
private $keys;
public function __construct(array $keys)
{
$this->keys = $keys;
}
/**
* Duplicates the given value through the array.
*
* @param mixed $value The value
*
* @return array
*/
public function transform($value)
{
$result = [];
foreach ($this->keys as $key) {
$result[$key] = $value;
}
return $result;
}
/**
* Extracts the duplicated value from an array.
*
* @return mixed
*
* @throws TransformationFailedException if the given value is not an array or
* if the given array cannot be transformed
*/
public function reverseTransform($array)
{
if (!\is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
$result = current($array);
$emptyKeys = [];
foreach ($this->keys as $key) {
if (isset($array[$key]) && '' !== $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;
}
}

View File

@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <damienf1521@gmail.com>
*/
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($value)
{
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($value)
{
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']);
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class FixUrlProtocolListener implements EventSubscriberInterface
{
private $defaultProtocol;
/**
* @param string|null $defaultProtocol The URL scheme to add when there is none or null to not modify the data
*/
public function __construct(?string $defaultProtocol = 'http')
{
$this->defaultProtocol = $defaultProtocol;
}
public function onSubmit(FormEvent $event)
{
$data = $event->getData();
if ($this->defaultProtocol && $data && \is_string($data) && !preg_match('~^(?:[/.]|[\w+.-]+://|[^:/?@#]++@)~', $data)) {
$event->setData($this->defaultProtocol.'://'.$data);
}
}
public static function getSubscribedEvents()
{
return [FormEvents::SUBMIT => 'onSubmit'];
}
}

View File

@ -0,0 +1,113 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class MergeCollectionListener implements EventSubscriberInterface
{
private $allowAdd;
private $allowDelete;
/**
* @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(bool $allowAdd = false, bool $allowDelete = false)
{
$this->allowAdd = $allowAdd;
$this->allowDelete = $allowDelete;
}
public static function getSubscribedEvents()
{
return [
FormEvents::SUBMIT => 'onSubmit',
];
}
public function onSubmit(FormEvent $event)
{
$dataToMergeInto = $event->getForm()->getNormData();
$data = $event->getData();
if (null === $data) {
$data = [];
}
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);
}
}

Some files were not shown because too many files have changed in this diff Show More