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,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);
}
}

View File

@ -0,0 +1,169 @@
<?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;
use Symfony\Component\Form\FormInterface;
/**
* Resize a collection form element based on the data sent from the client.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ResizeFormListener implements EventSubscriberInterface
{
protected $type;
protected $options;
protected $allowAdd;
protected $allowDelete;
private $deleteEmpty;
/**
* @param bool $allowAdd Whether children could be added to the group
* @param bool $allowDelete Whether children could be removed from the group
* @param bool|callable $deleteEmpty
*/
public function __construct(string $type, array $options = [], bool $allowAdd = false, bool $allowDelete = false, $deleteEmpty = false)
{
$this->type = $type;
$this->allowAdd = $allowAdd;
$this->allowDelete = $allowDelete;
$this->options = $options;
$this->deleteEmpty = $deleteEmpty;
}
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit',
// (MergeCollectionListener, MergeDoctrineCollectionListener)
FormEvents::SUBMIT => ['onSubmit', 50],
];
}
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
$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)');
}
// First remove all rows
foreach ($form as $name => $child) {
$form->remove($name);
}
// Then add all rows again in the correct order
foreach ($data as $name => $value) {
$form->add($name, $this->type, array_replace([
'property_path' => '['.$name.']',
], $this->options));
}
}
public function preSubmit(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
if (!\is_array($data)) {
$data = [];
}
// Remove all empty rows
if ($this->allowDelete) {
foreach ($form as $name => $child) {
if (!isset($data[$name])) {
$form->remove($name);
}
}
}
// Add all additional rows
if ($this->allowAdd) {
foreach ($data as $name => $value) {
if (!$form->has($name)) {
$form->add($name, $this->type, array_replace([
'property_path' => '['.$name.']',
], $this->options));
}
}
}
}
public function onSubmit(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
// At this point, $data is an array or an array-like object that already contains the
// new entries, which were added by the data mapper. The data mapper ignores existing
// entries, so we need to manually unset removed entries in the collection.
if (null === $data) {
$data = [];
}
if (!\is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
}
if ($this->deleteEmpty) {
$previousData = $form->getData();
/** @var FormInterface $child */
foreach ($form as $name => $child) {
if (!$child->isValid() || !$child->isSynchronized()) {
continue;
}
$isNew = !isset($previousData[$name]);
$isEmpty = \is_callable($this->deleteEmpty) ? ($this->deleteEmpty)($child->getData()) : $child->isEmpty();
// $isNew can only be true if allowAdd is true, so we don't
// need to check allowAdd again
if ($isEmpty && ($isNew || $this->allowDelete)) {
unset($data[$name]);
$form->remove($name);
}
}
}
// The data mapper only adds, but does not remove items, so do this
// here
if ($this->allowDelete) {
$toDelete = [];
foreach ($data as $name => $child) {
if (!$form->has($name)) {
$toDelete[] = $name;
}
}
foreach ($toDelete as $name) {
unset($data[$name]);
}
}
$event->setData($data);
}
}

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\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*/
class TransformationFailureListener implements EventSubscriberInterface
{
private $translator;
public function __construct(TranslatorInterface $translator = null)
{
$this->translator = $translator;
}
public static function getSubscribedEvents()
{
return [
FormEvents::POST_SUBMIT => ['convertTransformationFailureToFormError', -1024],
];
}
public function convertTransformationFailureToFormError(FormEvent $event)
{
$form = $event->getForm();
if (null === $form->getTransformationFailure() || !$form->isValid()) {
return;
}
foreach ($form as $child) {
if (!$child->isSynchronized()) {
return;
}
}
$clientDataAsString = is_scalar($form->getViewData()) ? (string) $form->getViewData() : get_debug_type($form->getViewData());
$messageTemplate = $form->getConfig()->getOption('invalid_message', 'The value {{ value }} is not valid.');
$messageParameters = array_replace(['{{ value }}' => $clientDataAsString], $form->getConfig()->getOption('invalid_message_parameters', []));
if (null !== $this->translator) {
$message = $this->translator->trans($messageTemplate, $messageParameters);
} else {
$message = strtr($messageTemplate, $messageParameters);
}
$form->addError(new FormError($message, $messageTemplate, $messageParameters, null, $form->getTransformationFailure()));
}
}

View File

@ -0,0 +1,41 @@
<?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;
use Symfony\Component\Form\Util\StringUtil;
/**
* Trims string data.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TrimListener implements EventSubscriberInterface
{
public function preSubmit(FormEvent $event)
{
$data = $event->getData();
if (!\is_string($data)) {
return;
}
$event->setData(StringUtil::trim($data));
}
public static function getSubscribedEvents()
{
return [FormEvents::PRE_SUBMIT => 'preSubmit'];
}
}

View File

@ -0,0 +1,150 @@
<?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\Type;
use Symfony\Component\Form\AbstractRendererEngine;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Encapsulates common logic of {@link FormType} and {@link ButtonType}.
*
* This type does not appear in the form's type inheritance chain and as such
* cannot be extended (via {@link \Symfony\Component\Form\FormExtensionInterface}) nor themed.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class BaseType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->setDisabled($options['disabled']);
$builder->setAutoInitialize($options['auto_initialize']);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$name = $form->getName();
$blockName = $options['block_name'] ?: $form->getName();
$translationDomain = $options['translation_domain'];
$labelTranslationParameters = $options['label_translation_parameters'];
$attrTranslationParameters = $options['attr_translation_parameters'];
$labelFormat = $options['label_format'];
if ($view->parent) {
if ('' !== ($parentFullName = $view->parent->vars['full_name'])) {
$id = sprintf('%s_%s', $view->parent->vars['id'], $name);
$fullName = sprintf('%s[%s]', $parentFullName, $name);
$uniqueBlockPrefix = sprintf('%s_%s', $view->parent->vars['unique_block_prefix'], $blockName);
} else {
$id = $name;
$fullName = $name;
$uniqueBlockPrefix = '_'.$blockName;
}
if (null === $translationDomain) {
$translationDomain = $view->parent->vars['translation_domain'];
}
$labelTranslationParameters = array_merge($view->parent->vars['label_translation_parameters'], $labelTranslationParameters);
$attrTranslationParameters = array_merge($view->parent->vars['attr_translation_parameters'], $attrTranslationParameters);
if (!$labelFormat) {
$labelFormat = $view->parent->vars['label_format'];
}
} else {
$id = $name;
$fullName = $name;
$uniqueBlockPrefix = '_'.$blockName;
// Strip leading underscores and digits. These are allowed in
// form names, but not in HTML4 ID attributes.
// https://www.w3.org/TR/html401/struct/global#adef-id
$id = ltrim($id, '_0123456789');
}
$blockPrefixes = [];
for ($type = $form->getConfig()->getType(); null !== $type; $type = $type->getParent()) {
array_unshift($blockPrefixes, $type->getBlockPrefix());
}
if (null !== $options['block_prefix']) {
$blockPrefixes[] = $options['block_prefix'];
}
$blockPrefixes[] = $uniqueBlockPrefix;
$view->vars = array_replace($view->vars, [
'form' => $view,
'id' => $id,
'name' => $name,
'full_name' => $fullName,
'disabled' => $form->isDisabled(),
'label' => $options['label'],
'label_format' => $labelFormat,
'label_html' => $options['label_html'],
'multipart' => false,
'attr' => $options['attr'],
'block_prefixes' => $blockPrefixes,
'unique_block_prefix' => $uniqueBlockPrefix,
'row_attr' => $options['row_attr'],
'translation_domain' => $translationDomain,
'label_translation_parameters' => $labelTranslationParameters,
'attr_translation_parameters' => $attrTranslationParameters,
'priority' => $options['priority'],
// Using the block name here speeds up performance in collection
// forms, where each entry has the same full block name.
// Including the type is important too, because if rows of a
// collection form have different types (dynamically), they should
// be rendered differently.
// https://github.com/symfony/symfony/issues/5038
AbstractRendererEngine::CACHE_KEY_VAR => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(),
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'block_name' => null,
'block_prefix' => null,
'disabled' => false,
'label' => null,
'label_format' => null,
'row_attr' => [],
'label_html' => false,
'label_translation_parameters' => [],
'attr_translation_parameters' => [],
'attr' => [],
'translation_domain' => null,
'auto_initialize' => true,
'priority' => 0,
]);
$resolver->setAllowedTypes('block_prefix', ['null', 'string']);
$resolver->setAllowedTypes('attr', 'array');
$resolver->setAllowedTypes('row_attr', 'array');
$resolver->setAllowedTypes('label_html', 'bool');
$resolver->setAllowedTypes('priority', 'int');
$resolver->setInfo('priority', 'The form rendering priority (higher priorities will be rendered first)');
}
}

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\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class BirthdayType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'years' => range((int) date('Y') - 120, date('Y')),
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a valid birthdate.';
},
]);
$resolver->setAllowedTypes('years', 'array');
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return DateType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'birthday';
}
}

View File

@ -0,0 +1,49 @@
<?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\Type;
use Symfony\Component\Form\ButtonTypeInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* A form button.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ButtonType extends BaseType implements ButtonTypeInterface
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return null;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'button';
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefault('auto_initialize', false);
}
}

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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\DataTransformer\BooleanToStringTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CheckboxType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Unlike in other types, where the data is NULL by default, it
// needs to be a Boolean here. setData(null) is not acceptable
// for checkboxes and radio buttons (unless a custom model
// transformer handles this case).
// We cannot solve this case via overriding the "data" option, because
// doing so also calls setDataLocked(true).
$builder->setData($options['data'] ?? false);
$builder->addViewTransformer(new BooleanToStringTransformer($options['value'], $options['false_values']));
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, [
'value' => $options['value'],
'checked' => null !== $form->getViewData(),
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$emptyData = function (FormInterface $form, $viewData) {
return $viewData;
};
$resolver->setDefaults([
'value' => '1',
'empty_data' => $emptyData,
'compound' => false,
'false_values' => [null],
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'The checkbox has an invalid value.';
},
'is_empty_callback' => static function ($modelData): bool {
return false === $modelData;
},
]);
$resolver->setAllowedTypes('false_values', 'array');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'checkbox';
}
}

View File

@ -0,0 +1,501 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFilter;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceTranslationParameters;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy;
use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper;
use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Contracts\Translation\TranslatorInterface;
class ChoiceType extends AbstractType
{
private $choiceListFactory;
private $translator;
/**
* @param TranslatorInterface $translator
*/
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null, $translator = null)
{
$this->choiceListFactory = $choiceListFactory ?? new CachingFactoryDecorator(
new PropertyAccessDecorator(
new DefaultChoiceListFactory()
)
);
if (null !== $translator && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be han instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->translator = $translator;
// BC, to be removed in 6.0
if ($this->choiceListFactory instanceof CachingFactoryDecorator) {
return;
}
$ref = new \ReflectionMethod($this->choiceListFactory, 'createListFromChoices');
if ($ref->getNumberOfParameters() < 3) {
trigger_deprecation('symfony/form', '5.1', 'Not defining a third parameter "callable|null $filter" in "%s::%s()" is deprecated.', $ref->class, $ref->name);
}
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$unknownValues = [];
$choiceList = $this->createChoiceList($options);
$builder->setAttribute('choice_list', $choiceList);
if ($options['expanded']) {
$builder->setDataMapper($options['multiple'] ? new CheckboxListMapper() : new RadioListMapper());
// Initialize all choices before doing the index check below.
// This helps in cases where index checks are optimized for non
// initialized choice lists. For example, when using an SQL driver,
// the index check would read in one SQL query and the initialization
// requires another SQL query. When the initialization is done first,
// one SQL query is sufficient.
$choiceListView = $this->createChoiceListView($choiceList, $options);
$builder->setAttribute('choice_list_view', $choiceListView);
// Check if the choices already contain the empty value
// Only add the placeholder option if this is not the case
if (null !== $options['placeholder'] && 0 === \count($choiceList->getChoicesForValues(['']))) {
$placeholderView = new ChoiceView(null, '', $options['placeholder']);
// "placeholder" is a reserved name
$this->addSubForm($builder, 'placeholder', $placeholderView, $options);
}
$this->addSubForms($builder, $choiceListView->preferredChoices, $options);
$this->addSubForms($builder, $choiceListView->choices, $options);
}
if ($options['expanded'] || $options['multiple']) {
// Make sure that scalar, submitted values are converted to arrays
// which can be submitted to the checkboxes/radio buttons
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($choiceList, $options, &$unknownValues) {
$form = $event->getForm();
$data = $event->getData();
// Since the type always use mapper an empty array will not be
// considered as empty in Form::submit(), we need to evaluate
// empty data here so its value is submitted to sub forms
if (null === $data) {
$emptyData = $form->getConfig()->getEmptyData();
$data = $emptyData instanceof \Closure ? $emptyData($form, $data) : $emptyData;
}
// Convert the submitted data to a string, if scalar, before
// casting it to an array
if (!\is_array($data)) {
if ($options['multiple']) {
throw new TransformationFailedException('Expected an array.');
}
$data = (array) (string) $data;
}
// A map from submitted values to integers
$valueMap = array_flip($data);
// Make a copy of the value map to determine whether any unknown
// values were submitted
$unknownValues = $valueMap;
// Reconstruct the data as mapping from child names to values
$knownValues = [];
if ($options['expanded']) {
/** @var FormInterface $child */
foreach ($form as $child) {
$value = $child->getConfig()->getOption('value');
// Add the value to $data with the child's name as key
if (isset($valueMap[$value])) {
$knownValues[$child->getName()] = $value;
unset($unknownValues[$value]);
continue;
} else {
$knownValues[$child->getName()] = null;
}
}
} else {
foreach ($data as $value) {
if ($choiceList->getChoicesForValues([$value])) {
$knownValues[] = $value;
unset($unknownValues[$value]);
}
}
}
// The empty value is always known, independent of whether a
// field exists for it or not
unset($unknownValues['']);
// Throw exception if unknown values were submitted (multiple choices will be handled in a different event listener below)
if (\count($unknownValues) > 0 && !$options['multiple']) {
throw new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.', implode('", "', array_keys($unknownValues))));
}
$event->setData($knownValues);
});
}
if ($options['multiple']) {
$messageTemplate = $options['invalid_message'] ?? 'The value {{ value }} is not valid.';
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use (&$unknownValues, $messageTemplate) {
// Throw exception if unknown values were submitted
if (\count($unknownValues) > 0) {
$form = $event->getForm();
$clientDataAsString = is_scalar($form->getViewData()) ? (string) $form->getViewData() : (\is_array($form->getViewData()) ? implode('", "', array_keys($unknownValues)) : \gettype($form->getViewData()));
if (null !== $this->translator) {
$message = $this->translator->trans($messageTemplate, ['{{ value }}' => $clientDataAsString], 'validators');
} else {
$message = strtr($messageTemplate, ['{{ value }}' => $clientDataAsString]);
}
$form->addError(new FormError($message, $messageTemplate, ['{{ value }}' => $clientDataAsString], null, new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.', $clientDataAsString))));
}
});
// <select> tag with "multiple" option or list of checkbox inputs
$builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));
} else {
// <select> tag without "multiple" option or list of radio inputs
$builder->addViewTransformer(new ChoiceToValueTransformer($choiceList));
}
if ($options['multiple'] && $options['by_reference']) {
// Make sure the collection created during the client->norm
// transformation is merged back into the original collection
$builder->addEventSubscriber(new MergeCollectionListener(true, true));
}
// To avoid issues when the submitted choices are arrays (i.e. array to string conversions),
// we have to ensure that all elements of the submitted choice data are NULL, strings or ints.
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if (!\is_array($data)) {
return;
}
foreach ($data as $v) {
if (null !== $v && !\is_string($v) && !\is_int($v)) {
throw new TransformationFailedException('All choices submitted must be NULL, strings or ints.');
}
}
}, 256);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$choiceTranslationDomain = $options['choice_translation_domain'];
if ($view->parent && null === $choiceTranslationDomain) {
$choiceTranslationDomain = $view->vars['translation_domain'];
}
/** @var ChoiceListInterface $choiceList */
$choiceList = $form->getConfig()->getAttribute('choice_list');
/** @var ChoiceListView $choiceListView */
$choiceListView = $form->getConfig()->hasAttribute('choice_list_view')
? $form->getConfig()->getAttribute('choice_list_view')
: $this->createChoiceListView($choiceList, $options);
$view->vars = array_replace($view->vars, [
'multiple' => $options['multiple'],
'expanded' => $options['expanded'],
'preferred_choices' => $choiceListView->preferredChoices,
'choices' => $choiceListView->choices,
'separator' => '-------------------',
'placeholder' => null,
'choice_translation_domain' => $choiceTranslationDomain,
'choice_translation_parameters' => $options['choice_translation_parameters'],
]);
// The decision, whether a choice is selected, is potentially done
// thousand of times during the rendering of a template. Provide a
// closure here that is optimized for the value of the form, to
// avoid making the type check inside the closure.
if ($options['multiple']) {
$view->vars['is_selected'] = function ($choice, array $values) {
return \in_array($choice, $values, true);
};
} else {
$view->vars['is_selected'] = function ($choice, $value) {
return $choice === $value;
};
}
// Check if the choices already contain the empty value
$view->vars['placeholder_in_choices'] = $choiceListView->hasPlaceholder();
// Only add the empty value option if this is not the case
if (null !== $options['placeholder'] && !$view->vars['placeholder_in_choices']) {
$view->vars['placeholder'] = $options['placeholder'];
}
if ($options['multiple'] && !$options['expanded']) {
// Add "[]" to the name in case a select tag with multiple options is
// displayed. Otherwise only one of the selected options is sent in the
// POST request.
$view->vars['full_name'] .= '[]';
}
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
if ($options['expanded']) {
// Radio buttons should have the same name as the parent
$childName = $view->vars['full_name'];
// Checkboxes should append "[]" to allow multiple selection
if ($options['multiple']) {
$childName .= '[]';
}
foreach ($view as $childView) {
$childView->vars['full_name'] = $childName;
}
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$emptyData = function (Options $options) {
if ($options['expanded'] && !$options['multiple']) {
return null;
}
if ($options['multiple']) {
return [];
}
return '';
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$placeholderNormalizer = function (Options $options, $placeholder) {
if ($options['multiple']) {
// never use an empty value for this case
return null;
} elseif ($options['required'] && ($options['expanded'] || isset($options['attr']['size']) && $options['attr']['size'] > 1)) {
// placeholder for required radio buttons or a select with size > 1 does not make sense
return null;
} elseif (false === $placeholder) {
// an empty value should be added but the user decided otherwise
return null;
} elseif ($options['expanded'] && '' === $placeholder) {
// never use an empty label for radio buttons
return 'None';
}
// empty value has been set explicitly
return $placeholder;
};
$compound = function (Options $options) {
return $options['expanded'];
};
$choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) {
if (true === $choiceTranslationDomain) {
return $options['translation_domain'];
}
return $choiceTranslationDomain;
};
$resolver->setDefaults([
'multiple' => false,
'expanded' => false,
'choices' => [],
'choice_filter' => null,
'choice_loader' => null,
'choice_label' => null,
'choice_name' => null,
'choice_value' => null,
'choice_attr' => null,
'choice_translation_parameters' => [],
'preferred_choices' => [],
'group_by' => null,
'empty_data' => $emptyData,
'placeholder' => $placeholderDefault,
'error_bubbling' => false,
'compound' => $compound,
// The view data is always a string or an array of strings,
// even if the "data" option is manually set to an object.
// See https://github.com/symfony/symfony/pull/5582
'data_class' => null,
'choice_translation_domain' => true,
'trim' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'The selected choice is invalid.';
},
]);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setAllowedTypes('choices', ['null', 'array', \Traversable::class]);
$resolver->setAllowedTypes('choice_translation_domain', ['null', 'bool', 'string']);
$resolver->setAllowedTypes('choice_loader', ['null', ChoiceLoaderInterface::class, ChoiceLoader::class]);
$resolver->setAllowedTypes('choice_filter', ['null', 'callable', 'string', PropertyPath::class, ChoiceFilter::class]);
$resolver->setAllowedTypes('choice_label', ['null', 'bool', 'callable', 'string', PropertyPath::class, ChoiceLabel::class]);
$resolver->setAllowedTypes('choice_name', ['null', 'callable', 'string', PropertyPath::class, ChoiceFieldName::class]);
$resolver->setAllowedTypes('choice_value', ['null', 'callable', 'string', PropertyPath::class, ChoiceValue::class]);
$resolver->setAllowedTypes('choice_attr', ['null', 'array', 'callable', 'string', PropertyPath::class, ChoiceAttr::class]);
$resolver->setAllowedTypes('choice_translation_parameters', ['null', 'array', 'callable', ChoiceTranslationParameters::class]);
$resolver->setAllowedTypes('preferred_choices', ['array', \Traversable::class, 'callable', 'string', PropertyPath::class, PreferredChoice::class]);
$resolver->setAllowedTypes('group_by', ['null', 'callable', 'string', PropertyPath::class, GroupBy::class]);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'choice';
}
/**
* Adds the sub fields for an expanded choice field.
*/
private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
{
foreach ($choiceViews as $name => $choiceView) {
// Flatten groups
if (\is_array($choiceView)) {
$this->addSubForms($builder, $choiceView, $options);
continue;
}
if ($choiceView instanceof ChoiceGroupView) {
$this->addSubForms($builder, $choiceView->choices, $options);
continue;
}
$this->addSubForm($builder, $name, $choiceView, $options);
}
}
private function addSubForm(FormBuilderInterface $builder, string $name, ChoiceView $choiceView, array $options)
{
$choiceOpts = [
'value' => $choiceView->value,
'label' => $choiceView->label,
'label_html' => $options['label_html'],
'attr' => $choiceView->attr,
'label_translation_parameters' => $choiceView->labelTranslationParameters,
'translation_domain' => $options['choice_translation_domain'],
'block_name' => 'entry',
];
if ($options['multiple']) {
$choiceType = CheckboxType::class;
// The user can check 0 or more checkboxes. If required
// is true, they are required to check all of them.
$choiceOpts['required'] = false;
} else {
$choiceType = RadioType::class;
}
$builder->add($name, $choiceType, $choiceOpts);
}
private function createChoiceList(array $options)
{
if (null !== $options['choice_loader']) {
return $this->choiceListFactory->createListFromLoader(
$options['choice_loader'],
$options['choice_value'],
$options['choice_filter']
);
}
// Harden against NULL values (like in EntityType and ModelType)
$choices = null !== $options['choices'] ? $options['choices'] : [];
return $this->choiceListFactory->createListFromChoices(
$choices,
$options['choice_value'],
$options['choice_filter']
);
}
private function createChoiceListView(ChoiceListInterface $choiceList, array $options)
{
return $this->choiceListFactory->createView(
$choiceList,
$options['preferred_choices'],
$options['choice_label'],
$options['choice_name'],
$options['group_by'],
$options['choice_attr'],
$options['choice_translation_parameters']
);
}
}

View File

@ -0,0 +1,142 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CollectionType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['allow_add'] && $options['prototype']) {
$prototypeOptions = array_replace([
'required' => $options['required'],
'label' => $options['prototype_name'].'label__',
], $options['entry_options']);
if (null !== $options['prototype_data']) {
$prototypeOptions['data'] = $options['prototype_data'];
}
$prototype = $builder->create($options['prototype_name'], $options['entry_type'], $prototypeOptions);
$builder->setAttribute('prototype', $prototype->getForm());
}
$resizeListener = new ResizeFormListener(
$options['entry_type'],
$options['entry_options'],
$options['allow_add'],
$options['allow_delete'],
$options['delete_empty']
);
$builder->addEventSubscriber($resizeListener);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, [
'allow_add' => $options['allow_add'],
'allow_delete' => $options['allow_delete'],
]);
if ($form->getConfig()->hasAttribute('prototype')) {
$prototype = $form->getConfig()->getAttribute('prototype');
$view->vars['prototype'] = $prototype->setParent($form)->createView($view);
}
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
$prefixOffset = -2;
// check if the entry type also defines a block prefix
/** @var FormInterface $entry */
foreach ($form as $entry) {
if ($entry->getConfig()->getOption('block_prefix')) {
--$prefixOffset;
}
break;
}
foreach ($view as $entryView) {
array_splice($entryView->vars['block_prefixes'], $prefixOffset, 0, 'collection_entry');
}
/** @var FormInterface $prototype */
if ($prototype = $form->getConfig()->getAttribute('prototype')) {
if ($view->vars['prototype']->vars['multipart']) {
$view->vars['multipart'] = true;
}
if ($prefixOffset > -3 && $prototype->getConfig()->getOption('block_prefix')) {
--$prefixOffset;
}
array_splice($view->vars['prototype']->vars['block_prefixes'], $prefixOffset, 0, 'collection_entry');
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$entryOptionsNormalizer = function (Options $options, $value) {
$value['block_name'] = 'entry';
return $value;
};
$resolver->setDefaults([
'allow_add' => false,
'allow_delete' => false,
'prototype' => true,
'prototype_data' => null,
'prototype_name' => '__name__',
'entry_type' => TextType::class,
'entry_options' => [],
'delete_empty' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'The collection is invalid.';
},
]);
$resolver->setNormalizer('entry_options', $entryOptionsNormalizer);
$resolver->setAllowedTypes('delete_empty', ['bool', 'callable']);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'collection';
}
}

View File

@ -0,0 +1,98 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class ColorType extends AbstractType
{
/**
* @see https://www.w3.org/TR/html52/sec-forms.html#color-state-typecolor
*/
private const HTML5_PATTERN = '/^#[0-9a-f]{6}$/i';
private $translator;
public function __construct(TranslatorInterface $translator = null)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['html5']) {
return;
}
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event): void {
$value = $event->getData();
if (null === $value || '' === $value) {
return;
}
if (\is_string($value) && preg_match(self::HTML5_PATTERN, $value)) {
return;
}
$messageTemplate = 'This value is not a valid HTML5 color.';
$messageParameters = [
'{{ value }}' => is_scalar($value) ? (string) $value : \gettype($value),
];
$message = $this->translator ? $this->translator->trans($messageTemplate, $messageParameters, 'validators') : $messageTemplate;
$event->getForm()->addError(new FormError($message, $messageTemplate, $messageParameters));
});
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'html5' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please select a valid color.';
},
]);
$resolver->setAllowedTypes('html5', 'bool');
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'color';
}
}

View File

@ -0,0 +1,72 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Intl\Countries;
use Symfony\Component\Intl\Intl;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CountryType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'choice_loader' => function (Options $options) {
if (!class_exists(Intl::class)) {
throw new LogicException(sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
}
$choiceTranslationLocale = $options['choice_translation_locale'];
$alpha3 = $options['alpha3'];
return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale, $alpha3) {
return array_flip($alpha3 ? Countries::getAlpha3Names($choiceTranslationLocale) : Countries::getNames($choiceTranslationLocale));
}), [$choiceTranslationLocale, $alpha3]);
},
'choice_translation_domain' => false,
'choice_translation_locale' => null,
'alpha3' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please select a valid country.';
},
]);
$resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
$resolver->setAllowedTypes('alpha3', 'bool');
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return ChoiceType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'country';
}
}

View File

@ -0,0 +1,69 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Intl\Currencies;
use Symfony\Component\Intl\Intl;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CurrencyType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'choice_loader' => function (Options $options) {
if (!class_exists(Intl::class)) {
throw new LogicException(sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
}
$choiceTranslationLocale = $options['choice_translation_locale'];
return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale) {
return array_flip(Currencies::getNames($choiceTranslationLocale));
}), $choiceTranslationLocale);
},
'choice_translation_domain' => false,
'choice_translation_locale' => null,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please select a valid currency.';
},
]);
$resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return ChoiceType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'currency';
}
}

View File

@ -0,0 +1,291 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
*/
class DateIntervalType extends AbstractType
{
private const TIME_PARTS = [
'years',
'months',
'weeks',
'days',
'hours',
'minutes',
'seconds',
];
private const WIDGETS = [
'text' => TextType::class,
'integer' => IntegerType::class,
'choice' => ChoiceType::class,
];
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['with_years'] && !$options['with_months'] && !$options['with_weeks'] && !$options['with_days'] && !$options['with_hours'] && !$options['with_minutes'] && !$options['with_seconds']) {
throw new InvalidConfigurationException('You must enable at least one interval field.');
}
if ($options['with_invert'] && 'single_text' === $options['widget']) {
throw new InvalidConfigurationException('The single_text widget does not support invertible intervals.');
}
if ($options['with_weeks'] && $options['with_days']) {
throw new InvalidConfigurationException('You cannot enable weeks and days fields together.');
}
$format = 'P';
$parts = [];
if ($options['with_years']) {
$format .= '%yY';
$parts[] = 'years';
}
if ($options['with_months']) {
$format .= '%mM';
$parts[] = 'months';
}
if ($options['with_weeks']) {
$format .= '%wW';
$parts[] = 'weeks';
}
if ($options['with_days']) {
$format .= '%dD';
$parts[] = 'days';
}
if ($options['with_hours'] || $options['with_minutes'] || $options['with_seconds']) {
$format .= 'T';
}
if ($options['with_hours']) {
$format .= '%hH';
$parts[] = 'hours';
}
if ($options['with_minutes']) {
$format .= '%iM';
$parts[] = 'minutes';
}
if ($options['with_seconds']) {
$format .= '%sS';
$parts[] = 'seconds';
}
if ($options['with_invert']) {
$parts[] = 'invert';
}
if ('single_text' === $options['widget']) {
$builder->addViewTransformer(new DateIntervalToStringTransformer($format));
} else {
foreach (self::TIME_PARTS as $part) {
if ($options['with_'.$part]) {
$childOptions = [
'error_bubbling' => true,
'label' => $options['labels'][$part],
// Append generic carry-along options
'required' => $options['required'],
'translation_domain' => $options['translation_domain'],
// when compound the array entries are ignored, we need to cascade the configuration here
'empty_data' => $options['empty_data'][$part] ?? null,
];
if ('choice' === $options['widget']) {
$childOptions['choice_translation_domain'] = false;
$childOptions['choices'] = $options[$part];
$childOptions['placeholder'] = $options['placeholder'][$part];
}
$childForm = $builder->create($part, self::WIDGETS[$options['widget']], $childOptions);
if ('integer' === $options['widget']) {
$childForm->addModelTransformer(
new ReversedTransformer(
new IntegerToLocalizedStringTransformer()
)
);
}
$builder->add($childForm);
}
}
if ($options['with_invert']) {
$builder->add('invert', CheckboxType::class, [
'label' => $options['labels']['invert'],
'error_bubbling' => true,
'required' => false,
'translation_domain' => $options['translation_domain'],
]);
}
$builder->addViewTransformer(new DateIntervalToArrayTransformer($parts, 'text' === $options['widget']));
}
if ('string' === $options['input']) {
$builder->addModelTransformer(
new ReversedTransformer(
new DateIntervalToStringTransformer($format)
)
);
} elseif ('array' === $options['input']) {
$builder->addModelTransformer(
new ReversedTransformer(
new DateIntervalToArrayTransformer($parts)
)
);
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$vars = [
'widget' => $options['widget'],
'with_invert' => $options['with_invert'],
];
foreach (self::TIME_PARTS as $part) {
$vars['with_'.$part] = $options['with_'.$part];
}
$view->vars = array_replace($view->vars, $vars);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$compound = function (Options $options) {
return 'single_text' !== $options['widget'];
};
$emptyData = function (Options $options) {
return 'single_text' === $options['widget'] ? '' : [];
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) {
if (\is_array($placeholder)) {
$default = $placeholderDefault($options);
return array_merge(array_fill_keys(self::TIME_PARTS, $default), $placeholder);
}
return array_fill_keys(self::TIME_PARTS, $placeholder);
};
$labelsNormalizer = function (Options $options, array $labels) {
return array_replace([
'years' => null,
'months' => null,
'days' => null,
'weeks' => null,
'hours' => null,
'minutes' => null,
'seconds' => null,
'invert' => 'Negative interval',
], array_filter($labels, function ($label) {
return null !== $label;
}));
};
$resolver->setDefaults([
'with_years' => true,
'with_months' => true,
'with_days' => true,
'with_weeks' => false,
'with_hours' => false,
'with_minutes' => false,
'with_seconds' => false,
'with_invert' => false,
'years' => range(0, 100),
'months' => range(0, 12),
'weeks' => range(0, 52),
'days' => range(0, 31),
'hours' => range(0, 24),
'minutes' => range(0, 60),
'seconds' => range(0, 60),
'widget' => 'choice',
'input' => 'dateinterval',
'placeholder' => $placeholderDefault,
'by_reference' => true,
'error_bubbling' => false,
// If initialized with a \DateInterval object, FormType initializes
// this option to "\DateInterval". Since the internal, normalized
// representation is not \DateInterval, but an array, we need to unset
// this option.
'data_class' => null,
'compound' => $compound,
'empty_data' => $emptyData,
'labels' => [],
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please choose a valid date interval.';
},
]);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('labels', $labelsNormalizer);
$resolver->setAllowedValues(
'input',
[
'dateinterval',
'string',
'array',
]
);
$resolver->setAllowedValues(
'widget',
[
'single_text',
'text',
'integer',
'choice',
]
);
// Don't clone \DateInterval classes, as i.e. format()
// does not work after that
$resolver->setAllowedValues('by_reference', true);
$resolver->setAllowedTypes('years', 'array');
$resolver->setAllowedTypes('months', 'array');
$resolver->setAllowedTypes('weeks', 'array');
$resolver->setAllowedTypes('days', 'array');
$resolver->setAllowedTypes('hours', 'array');
$resolver->setAllowedTypes('minutes', 'array');
$resolver->setAllowedTypes('seconds', 'array');
$resolver->setAllowedTypes('with_years', 'bool');
$resolver->setAllowedTypes('with_months', 'bool');
$resolver->setAllowedTypes('with_weeks', 'bool');
$resolver->setAllowedTypes('with_days', 'bool');
$resolver->setAllowedTypes('with_hours', 'bool');
$resolver->setAllowedTypes('with_minutes', 'bool');
$resolver->setAllowedTypes('with_seconds', 'bool');
$resolver->setAllowedTypes('with_invert', 'bool');
$resolver->setAllowedTypes('labels', 'array');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'dateinterval';
}
}

View File

@ -0,0 +1,362 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToHtml5LocalDateTimeTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DateTimeType extends AbstractType
{
public const DEFAULT_DATE_FORMAT = \IntlDateFormatter::MEDIUM;
public const DEFAULT_TIME_FORMAT = \IntlDateFormatter::MEDIUM;
/**
* The HTML5 datetime-local format as defined in
* http://w3c.github.io/html-reference/datatypes.html#form.data.datetime-local.
*/
public const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
private const ACCEPTED_FORMATS = [
\IntlDateFormatter::FULL,
\IntlDateFormatter::LONG,
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::SHORT,
];
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$parts = ['year', 'month', 'day', 'hour'];
$dateParts = ['year', 'month', 'day'];
$timeParts = ['hour'];
if ($options['with_minutes']) {
$parts[] = 'minute';
$timeParts[] = 'minute';
}
if ($options['with_seconds']) {
$parts[] = 'second';
$timeParts[] = 'second';
}
$dateFormat = \is_int($options['date_format']) ? $options['date_format'] : self::DEFAULT_DATE_FORMAT;
$timeFormat = self::DEFAULT_TIME_FORMAT;
$calendar = \IntlDateFormatter::GREGORIAN;
$pattern = \is_string($options['format']) ? $options['format'] : null;
if (!\in_array($dateFormat, self::ACCEPTED_FORMATS, true)) {
throw new InvalidOptionsException('The "date_format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
}
if ('single_text' === $options['widget']) {
if (self::HTML5_FORMAT === $pattern) {
$builder->addViewTransformer(new DateTimeToHtml5LocalDateTimeTransformer(
$options['model_timezone'],
$options['view_timezone']
));
} else {
$builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
$options['model_timezone'],
$options['view_timezone'],
$dateFormat,
$timeFormat,
$calendar,
$pattern
));
}
} else {
// when the form is compound the entries of the array are ignored in favor of children data
// so we need to handle the cascade setting here
$emptyData = $builder->getEmptyData() ?: [];
// Only pass a subset of the options to children
$dateOptions = array_intersect_key($options, array_flip([
'years',
'months',
'days',
'placeholder',
'choice_translation_domain',
'required',
'translation_domain',
'html5',
'invalid_message',
'invalid_message_parameters',
]));
if ($emptyData instanceof \Closure) {
$lazyEmptyData = static function ($option) use ($emptyData) {
return static function (FormInterface $form) use ($emptyData, $option) {
$emptyData = $emptyData($form->getParent());
return $emptyData[$option] ?? '';
};
};
$dateOptions['empty_data'] = $lazyEmptyData('date');
} elseif (isset($emptyData['date'])) {
$dateOptions['empty_data'] = $emptyData['date'];
}
$timeOptions = array_intersect_key($options, array_flip([
'hours',
'minutes',
'seconds',
'with_minutes',
'with_seconds',
'placeholder',
'choice_translation_domain',
'required',
'translation_domain',
'html5',
'invalid_message',
'invalid_message_parameters',
]));
if ($emptyData instanceof \Closure) {
$timeOptions['empty_data'] = $lazyEmptyData('time');
} elseif (isset($emptyData['time'])) {
$timeOptions['empty_data'] = $emptyData['time'];
}
if (false === $options['label']) {
$dateOptions['label'] = false;
$timeOptions['label'] = false;
}
if (null !== $options['date_widget']) {
$dateOptions['widget'] = $options['date_widget'];
}
if (null !== $options['date_label']) {
$dateOptions['label'] = $options['date_label'];
}
if (null !== $options['time_widget']) {
$timeOptions['widget'] = $options['time_widget'];
}
if (null !== $options['time_label']) {
$timeOptions['label'] = $options['time_label'];
}
if (null !== $options['date_format']) {
$dateOptions['format'] = $options['date_format'];
}
$dateOptions['input'] = $timeOptions['input'] = 'array';
$dateOptions['error_bubbling'] = $timeOptions['error_bubbling'] = true;
$builder
->addViewTransformer(new DataTransformerChain([
new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts),
new ArrayToPartsTransformer([
'date' => $dateParts,
'time' => $timeParts,
]),
]))
->add('date', DateType::class, $dateOptions)
->add('time', TimeType::class, $timeOptions)
;
}
if ('datetime_immutable' === $options['input']) {
$builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
} elseif ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format'])
));
} elseif ('timestamp' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
));
} elseif ('array' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts)
));
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['widget'] = $options['widget'];
// Change the input to an HTML5 datetime input if
// * the widget is set to "single_text"
// * the format matches the one expected by HTML5
// * the html5 is set to true
if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
$view->vars['type'] = 'datetime-local';
// we need to force the browser to display the seconds by
// adding the HTML attribute step if not already defined.
// Otherwise the browser will not display and so not send the seconds
// therefore the value will always be considered as invalid.
if ($options['with_seconds'] && !isset($view->vars['attr']['step'])) {
$view->vars['attr']['step'] = 1;
}
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$compound = function (Options $options) {
return 'single_text' !== $options['widget'];
};
// Defaults to the value of "widget"
$dateWidget = function (Options $options) {
return 'single_text' === $options['widget'] ? null : $options['widget'];
};
// Defaults to the value of "widget"
$timeWidget = function (Options $options) {
return 'single_text' === $options['widget'] ? null : $options['widget'];
};
$resolver->setDefaults([
'input' => 'datetime',
'model_timezone' => null,
'view_timezone' => null,
'format' => self::HTML5_FORMAT,
'date_format' => null,
'widget' => null,
'date_widget' => $dateWidget,
'time_widget' => $timeWidget,
'with_minutes' => true,
'with_seconds' => false,
'html5' => true,
// Don't modify \DateTime classes by reference, we treat
// them like immutable value objects
'by_reference' => false,
'error_bubbling' => false,
// If initialized with a \DateTime object, FormType initializes
// this option to "\DateTime". Since the internal, normalized
// representation is not \DateTime, but an array, we need to unset
// this option.
'data_class' => null,
'compound' => $compound,
'date_label' => null,
'time_label' => null,
'empty_data' => function (Options $options) {
return $options['compound'] ? [] : '';
},
'input_format' => 'Y-m-d H:i:s',
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a valid date and time.';
},
]);
// Don't add some defaults in order to preserve the defaults
// set in DateType and TimeType
$resolver->setDefined([
'placeholder',
'choice_translation_domain',
'years',
'months',
'days',
'hours',
'minutes',
'seconds',
]);
$resolver->setAllowedValues('input', [
'datetime',
'datetime_immutable',
'string',
'timestamp',
'array',
]);
$resolver->setAllowedValues('date_widget', [
null, // inherit default from DateType
'single_text',
'text',
'choice',
]);
$resolver->setAllowedValues('time_widget', [
null, // inherit default from TimeType
'single_text',
'text',
'choice',
]);
// This option will overwrite "date_widget" and "time_widget" options
$resolver->setAllowedValues('widget', [
null, // default, don't overwrite options
'single_text',
'text',
'choice',
]);
$resolver->setAllowedTypes('input_format', 'string');
$resolver->setNormalizer('date_format', function (Options $options, $dateFormat) {
if (null !== $dateFormat && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
throw new LogicException(sprintf('Cannot use the "date_format" option of the "%s" with an HTML5 date.', self::class));
}
return $dateFormat;
});
$resolver->setNormalizer('date_widget', function (Options $options, $dateWidget) {
if (null !== $dateWidget && 'single_text' === $options['widget']) {
throw new LogicException(sprintf('Cannot use the "date_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class));
}
return $dateWidget;
});
$resolver->setNormalizer('time_widget', function (Options $options, $timeWidget) {
if (null !== $timeWidget && 'single_text' === $options['widget']) {
throw new LogicException(sprintf('Cannot use the "time_widget" option of the "%s" when the "widget" option is set to "single_text".', self::class));
}
return $timeWidget;
});
$resolver->setNormalizer('html5', function (Options $options, $html5) {
if ($html5 && self::HTML5_FORMAT !== $options['format']) {
throw new LogicException(sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class));
}
return $html5;
});
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'datetime';
}
}

View File

@ -0,0 +1,405 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DateType extends AbstractType
{
public const DEFAULT_FORMAT = \IntlDateFormatter::MEDIUM;
public const HTML5_FORMAT = 'yyyy-MM-dd';
private const ACCEPTED_FORMATS = [
\IntlDateFormatter::FULL,
\IntlDateFormatter::LONG,
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::SHORT,
];
private const WIDGETS = [
'text' => TextType::class,
'choice' => ChoiceType::class,
];
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$dateFormat = \is_int($options['format']) ? $options['format'] : self::DEFAULT_FORMAT;
$timeFormat = \IntlDateFormatter::NONE;
$calendar = \IntlDateFormatter::GREGORIAN;
$pattern = \is_string($options['format']) ? $options['format'] : '';
if (!\in_array($dateFormat, self::ACCEPTED_FORMATS, true)) {
throw new InvalidOptionsException('The "format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
}
if ('single_text' === $options['widget']) {
if ('' !== $pattern && !str_contains($pattern, 'y') && !str_contains($pattern, 'M') && !str_contains($pattern, 'd')) {
throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" or "d". Its current value is "%s".', $pattern));
}
$builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
$options['model_timezone'],
$options['view_timezone'],
$dateFormat,
$timeFormat,
$calendar,
$pattern
));
} else {
if ('' !== $pattern && (!str_contains($pattern, 'y') || !str_contains($pattern, 'M') || !str_contains($pattern, 'd'))) {
throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".', $pattern));
}
$yearOptions = $monthOptions = $dayOptions = [
'error_bubbling' => true,
'empty_data' => '',
];
// when the form is compound the entries of the array are ignored in favor of children data
// so we need to handle the cascade setting here
$emptyData = $builder->getEmptyData() ?: [];
if ($emptyData instanceof \Closure) {
$lazyEmptyData = static function ($option) use ($emptyData) {
return static function (FormInterface $form) use ($emptyData, $option) {
$emptyData = $emptyData($form->getParent());
return $emptyData[$option] ?? '';
};
};
$yearOptions['empty_data'] = $lazyEmptyData('year');
$monthOptions['empty_data'] = $lazyEmptyData('month');
$dayOptions['empty_data'] = $lazyEmptyData('day');
} else {
if (isset($emptyData['year'])) {
$yearOptions['empty_data'] = $emptyData['year'];
}
if (isset($emptyData['month'])) {
$monthOptions['empty_data'] = $emptyData['month'];
}
if (isset($emptyData['day'])) {
$dayOptions['empty_data'] = $emptyData['day'];
}
}
if (isset($options['invalid_message'])) {
$dayOptions['invalid_message'] = $options['invalid_message'];
$monthOptions['invalid_message'] = $options['invalid_message'];
$yearOptions['invalid_message'] = $options['invalid_message'];
}
if (isset($options['invalid_message_parameters'])) {
$dayOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
$monthOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
$yearOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
}
$formatter = new \IntlDateFormatter(
\Locale::getDefault(),
$dateFormat,
$timeFormat,
// see https://bugs.php.net/66323
class_exists(\IntlTimeZone::class, false) ? \IntlTimeZone::createDefault() : null,
$calendar,
$pattern
);
// new \IntlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/66323
if (!$formatter) {
throw new InvalidOptionsException(intl_get_error_message(), intl_get_error_code());
}
$formatter->setLenient(false);
if ('choice' === $options['widget']) {
// Only pass a subset of the options to children
$yearOptions['choices'] = $this->formatTimestamps($formatter, '/y+/', $this->listYears($options['years']));
$yearOptions['placeholder'] = $options['placeholder']['year'];
$yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year'];
$monthOptions['choices'] = $this->formatTimestamps($formatter, '/[M|L]+/', $this->listMonths($options['months']));
$monthOptions['placeholder'] = $options['placeholder']['month'];
$monthOptions['choice_translation_domain'] = $options['choice_translation_domain']['month'];
$dayOptions['choices'] = $this->formatTimestamps($formatter, '/d+/', $this->listDays($options['days']));
$dayOptions['placeholder'] = $options['placeholder']['day'];
$dayOptions['choice_translation_domain'] = $options['choice_translation_domain']['day'];
}
// Append generic carry-along options
foreach (['required', 'translation_domain'] as $passOpt) {
$yearOptions[$passOpt] = $monthOptions[$passOpt] = $dayOptions[$passOpt] = $options[$passOpt];
}
$builder
->add('year', self::WIDGETS[$options['widget']], $yearOptions)
->add('month', self::WIDGETS[$options['widget']], $monthOptions)
->add('day', self::WIDGETS[$options['widget']], $dayOptions)
->addViewTransformer(new DateTimeToArrayTransformer(
$options['model_timezone'], $options['view_timezone'], ['year', 'month', 'day']
))
->setAttribute('formatter', $formatter)
;
}
if ('datetime_immutable' === $options['input']) {
$builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
} elseif ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format'])
));
} elseif ('timestamp' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
));
} elseif ('array' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], ['year', 'month', 'day'])
));
}
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
$view->vars['widget'] = $options['widget'];
// Change the input to an HTML5 date input if
// * the widget is set to "single_text"
// * the format matches the one expected by HTML5
// * the html5 is set to true
if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
$view->vars['type'] = 'date';
}
if ($form->getConfig()->hasAttribute('formatter')) {
$pattern = $form->getConfig()->getAttribute('formatter')->getPattern();
// remove special characters unless the format was explicitly specified
if (!\is_string($options['format'])) {
// remove quoted strings first
$pattern = preg_replace('/\'[^\']+\'/', '', $pattern);
// remove remaining special chars
$pattern = preg_replace('/[^yMd]+/', '', $pattern);
}
// set right order with respect to locale (e.g.: de_DE=dd.MM.yy; en_US=M/d/yy)
// lookup various formats at http://userguide.icu-project.org/formatparse/datetime
if (preg_match('/^([yMd]+)[^yMd]*([yMd]+)[^yMd]*([yMd]+)$/', $pattern)) {
$pattern = preg_replace(['/y+/', '/M+/', '/d+/'], ['{{ year }}', '{{ month }}', '{{ day }}'], $pattern);
} else {
// default fallback
$pattern = '{{ year }}{{ month }}{{ day }}';
}
$view->vars['date_pattern'] = $pattern;
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$compound = function (Options $options) {
return 'single_text' !== $options['widget'];
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) {
if (\is_array($placeholder)) {
$default = $placeholderDefault($options);
return array_merge(
['year' => $default, 'month' => $default, 'day' => $default],
$placeholder
);
}
return [
'year' => $placeholder,
'month' => $placeholder,
'day' => $placeholder,
];
};
$choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) {
if (\is_array($choiceTranslationDomain)) {
$default = false;
return array_replace(
['year' => $default, 'month' => $default, 'day' => $default],
$choiceTranslationDomain
);
}
return [
'year' => $choiceTranslationDomain,
'month' => $choiceTranslationDomain,
'day' => $choiceTranslationDomain,
];
};
$format = function (Options $options) {
return 'single_text' === $options['widget'] ? self::HTML5_FORMAT : self::DEFAULT_FORMAT;
};
$resolver->setDefaults([
'years' => range((int) date('Y') - 5, (int) date('Y') + 5),
'months' => range(1, 12),
'days' => range(1, 31),
'widget' => 'choice',
'input' => 'datetime',
'format' => $format,
'model_timezone' => null,
'view_timezone' => null,
'placeholder' => $placeholderDefault,
'html5' => true,
// Don't modify \DateTime classes by reference, we treat
// them like immutable value objects
'by_reference' => false,
'error_bubbling' => false,
// If initialized with a \DateTime object, FormType initializes
// this option to "\DateTime". Since the internal, normalized
// representation is not \DateTime, but an array, we need to unset
// this option.
'data_class' => null,
'compound' => $compound,
'empty_data' => function (Options $options) {
return $options['compound'] ? [] : '';
},
'choice_translation_domain' => false,
'input_format' => 'Y-m-d',
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a valid date.';
},
]);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setAllowedValues('input', [
'datetime',
'datetime_immutable',
'string',
'timestamp',
'array',
]);
$resolver->setAllowedValues('widget', [
'single_text',
'text',
'choice',
]);
$resolver->setAllowedTypes('format', ['int', 'string']);
$resolver->setAllowedTypes('years', 'array');
$resolver->setAllowedTypes('months', 'array');
$resolver->setAllowedTypes('days', 'array');
$resolver->setAllowedTypes('input_format', 'string');
$resolver->setNormalizer('html5', function (Options $options, $html5) {
if ($html5 && 'single_text' === $options['widget'] && self::HTML5_FORMAT !== $options['format']) {
throw new LogicException(sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.', self::class));
}
return $html5;
});
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'date';
}
private function formatTimestamps(\IntlDateFormatter $formatter, string $regex, array $timestamps)
{
$pattern = $formatter->getPattern();
$timezone = $formatter->getTimeZoneId();
$formattedTimestamps = [];
$formatter->setTimeZone('UTC');
if (preg_match($regex, $pattern, $matches)) {
$formatter->setPattern($matches[0]);
foreach ($timestamps as $timestamp => $choice) {
$formattedTimestamps[$formatter->format($timestamp)] = $choice;
}
// I'd like to clone the formatter above, but then we get a
// segmentation fault, so let's restore the old state instead
$formatter->setPattern($pattern);
}
$formatter->setTimeZone($timezone);
return $formattedTimestamps;
}
private function listYears(array $years)
{
$result = [];
foreach ($years as $year) {
$result[\PHP_INT_SIZE === 4 ? \DateTime::createFromFormat('Y e', $year.' UTC')->format('U') : gmmktime(0, 0, 0, 6, 15, $year)] = $year;
}
return $result;
}
private function listMonths(array $months)
{
$result = [];
foreach ($months as $month) {
$result[gmmktime(0, 0, 0, $month, 15)] = $month;
}
return $result;
}
private function listDays(array $days)
{
$result = [];
foreach ($days as $day) {
$result[gmmktime(0, 0, 0, 5, $day)] = $day;
}
return $result;
}
}

View File

@ -0,0 +1,49 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class EmailType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a valid email address.';
},
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'email';
}
}

View File

@ -0,0 +1,57 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* A choice type for native PHP enums.
*
* @author Alexander M. Turek <me@derrabus.de>
*/
final class EnumType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setRequired(['class'])
->setAllowedTypes('class', 'string')
->setAllowedValues('class', \Closure::fromCallable('enum_exists'))
->setDefault('choices', static function (Options $options): array {
return $options['class']::cases();
})
->setDefault('choice_label', static function (\UnitEnum $choice): string {
return $choice->name;
})
->setDefault('choice_value', static function (Options $options): ?\Closure {
if (!is_a($options['class'], \BackedEnum::class, true)) {
return null;
}
return static function (?\BackedEnum $choice): ?string {
if (null === $choice) {
return null;
}
return (string) $choice->value;
};
})
;
}
public function getParent(): string
{
return ChoiceType::class;
}
}

View File

@ -0,0 +1,255 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FileUploadError;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class FileType extends AbstractType
{
public const KIB_BYTES = 1024;
public const MIB_BYTES = 1048576;
private const SUFFIXES = [
1 => 'bytes',
self::KIB_BYTES => 'KiB',
self::MIB_BYTES => 'MiB',
];
private $translator;
public function __construct(TranslatorInterface $translator = null)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Ensure that submitted data is always an uploaded file or an array of some
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
$form = $event->getForm();
$requestHandler = $form->getConfig()->getRequestHandler();
if ($options['multiple']) {
$data = [];
$files = $event->getData();
if (!\is_array($files)) {
$files = [];
}
foreach ($files as $file) {
if ($requestHandler->isFileUpload($file)) {
$data[] = $file;
if (method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($file)) {
$form->addError($this->getFileUploadError($errorCode));
}
}
}
// Since the array is never considered empty in the view data format
// on submission, we need to evaluate the configured empty data here
if ([] === $data) {
$emptyData = $form->getConfig()->getEmptyData();
$data = $emptyData instanceof \Closure ? $emptyData($form, $data) : $emptyData;
}
$event->setData($data);
} elseif ($requestHandler->isFileUpload($event->getData()) && method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($event->getData())) {
$form->addError($this->getFileUploadError($errorCode));
} elseif (!$requestHandler->isFileUpload($event->getData())) {
$event->setData(null);
}
});
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if ($options['multiple']) {
$view->vars['full_name'] .= '[]';
$view->vars['attr']['multiple'] = 'multiple';
}
$view->vars = array_replace($view->vars, [
'type' => 'file',
'value' => '',
]);
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
$view->vars['multipart'] = true;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$dataClass = null;
if (class_exists(\Symfony\Component\HttpFoundation\File\File::class)) {
$dataClass = function (Options $options) {
return $options['multiple'] ? null : 'Symfony\Component\HttpFoundation\File\File';
};
}
$emptyData = function (Options $options) {
return $options['multiple'] ? [] : null;
};
$resolver->setDefaults([
'compound' => false,
'data_class' => $dataClass,
'empty_data' => $emptyData,
'multiple' => false,
'allow_file_upload' => true,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please select a valid file.';
},
]);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'file';
}
private function getFileUploadError(int $errorCode)
{
$messageParameters = [];
if (\UPLOAD_ERR_INI_SIZE === $errorCode) {
[$limitAsString, $suffix] = $this->factorizeSizes(0, self::getMaxFilesize());
$messageTemplate = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.';
$messageParameters = [
'{{ limit }}' => $limitAsString,
'{{ suffix }}' => $suffix,
];
} elseif (\UPLOAD_ERR_FORM_SIZE === $errorCode) {
$messageTemplate = 'The file is too large.';
} else {
$messageTemplate = 'The file could not be uploaded.';
}
if (null !== $this->translator) {
$message = $this->translator->trans($messageTemplate, $messageParameters, 'validators');
} else {
$message = strtr($messageTemplate, $messageParameters);
}
return new FileUploadError($message, $messageTemplate, $messageParameters);
}
/**
* Returns the maximum size of an uploaded file as configured in php.ini.
*
* This method should be kept in sync with Symfony\Component\HttpFoundation\File\UploadedFile::getMaxFilesize().
*
* @return int|float The maximum size of an uploaded file in bytes (returns float if size > PHP_INT_MAX)
*/
private static function getMaxFilesize()
{
$iniMax = strtolower(ini_get('upload_max_filesize'));
if ('' === $iniMax) {
return \PHP_INT_MAX;
}
$max = ltrim($iniMax, '+');
if (str_starts_with($max, '0x')) {
$max = \intval($max, 16);
} elseif (str_starts_with($max, '0')) {
$max = \intval($max, 8);
} else {
$max = (int) $max;
}
switch (substr($iniMax, -1)) {
case 't': $max *= 1024;
// no break
case 'g': $max *= 1024;
// no break
case 'm': $max *= 1024;
// no break
case 'k': $max *= 1024;
}
return $max;
}
/**
* Converts the limit to the smallest possible number
* (i.e. try "MB", then "kB", then "bytes").
*
* This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::factorizeSizes().
*
* @param int|float $limit
*/
private function factorizeSizes(int $size, $limit)
{
$coef = self::MIB_BYTES;
$coefFactor = self::KIB_BYTES;
$limitAsString = (string) ($limit / $coef);
// Restrict the limit to 2 decimals (without rounding! we
// need the precise value)
while (self::moreDecimalsThan($limitAsString, 2)) {
$coef /= $coefFactor;
$limitAsString = (string) ($limit / $coef);
}
// Convert size to the same measure, but round to 2 decimals
$sizeAsString = (string) round($size / $coef, 2);
// If the size and limit produce the same string output
// (due to rounding), reduce the coefficient
while ($sizeAsString === $limitAsString) {
$coef /= $coefFactor;
$limitAsString = (string) ($limit / $coef);
$sizeAsString = (string) round($size / $coef, 2);
}
return [$limitAsString, self::SUFFIXES[$coef]];
}
/**
* This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::moreDecimalsThan().
*/
private static function moreDecimalsThan(string $double, int $numberOfDecimals): bool
{
return \strlen($double) > \strlen(round($double, $numberOfDecimals));
}
}

View File

@ -0,0 +1,257 @@
<?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\Type;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataAccessor\CallbackAccessor;
use Symfony\Component\Form\Extension\Core\DataAccessor\ChainAccessor;
use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Extension\Core\EventListener\TrimListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormConfigBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Translation\TranslatableMessage;
class FormType extends BaseType
{
private $dataMapper;
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->dataMapper = new DataMapper(new ChainAccessor([
new CallbackAccessor(),
new PropertyPathAccessor($propertyAccessor ?? PropertyAccess::createPropertyAccessor()),
]));
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$isDataOptionSet = \array_key_exists('data', $options);
$builder
->setRequired($options['required'])
->setErrorBubbling($options['error_bubbling'])
->setEmptyData($options['empty_data'])
->setPropertyPath($options['property_path'])
->setMapped($options['mapped'])
->setByReference($options['by_reference'])
->setInheritData($options['inherit_data'])
->setCompound($options['compound'])
->setData($isDataOptionSet ? $options['data'] : null)
->setDataLocked($isDataOptionSet)
->setDataMapper($options['compound'] ? $this->dataMapper : null)
->setMethod($options['method'])
->setAction($options['action']);
if ($options['trim']) {
$builder->addEventSubscriber(new TrimListener());
}
if (!method_exists($builder, 'setIsEmptyCallback')) {
trigger_deprecation('symfony/form', '5.1', 'Not implementing the "%s::setIsEmptyCallback()" method in "%s" is deprecated.', FormConfigBuilderInterface::class, get_debug_type($builder));
return;
}
$builder->setIsEmptyCallback($options['is_empty_callback']);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
parent::buildView($view, $form, $options);
$name = $form->getName();
$helpTranslationParameters = $options['help_translation_parameters'];
if ($view->parent) {
if ('' === $name) {
throw new LogicException('Form node with empty name can be used only as root form node.');
}
// Complex fields are read-only if they themselves or their parents are.
if (!isset($view->vars['attr']['readonly']) && isset($view->parent->vars['attr']['readonly']) && false !== $view->parent->vars['attr']['readonly']) {
$view->vars['attr']['readonly'] = true;
}
$helpTranslationParameters = array_merge($view->parent->vars['help_translation_parameters'], $helpTranslationParameters);
$rootFormAttrOption = $form->getRoot()->getConfig()->getOption('form_attr');
if ($options['form_attr'] || $rootFormAttrOption) {
$view->vars['attr']['form'] = \is_string($rootFormAttrOption) ? $rootFormAttrOption : $form->getRoot()->getName();
if (empty($view->vars['attr']['form'])) {
throw new LogicException('"form_attr" option must be a string identifier on root form when it has no id.');
}
}
} elseif (\is_string($options['form_attr'])) {
$view->vars['id'] = $options['form_attr'];
}
$formConfig = $form->getConfig();
$view->vars = array_replace($view->vars, [
'errors' => $form->getErrors(),
'valid' => $form->isSubmitted() ? $form->isValid() : true,
'value' => $form->getViewData(),
'data' => $form->getNormData(),
'required' => $form->isRequired(),
'size' => null,
'label_attr' => $options['label_attr'],
'help' => $options['help'],
'help_attr' => $options['help_attr'],
'help_html' => $options['help_html'],
'help_translation_parameters' => $helpTranslationParameters,
'compound' => $formConfig->getCompound(),
'method' => $formConfig->getMethod(),
'action' => $formConfig->getAction(),
'submitted' => $form->isSubmitted(),
]);
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
$multipart = false;
foreach ($view->children as $child) {
if ($child->vars['multipart']) {
$multipart = true;
break;
}
}
$view->vars['multipart'] = $multipart;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
// Derive "data_class" option from passed "data" object
$dataClass = function (Options $options) {
return isset($options['data']) && \is_object($options['data']) ? \get_class($options['data']) : null;
};
// Derive "empty_data" closure from "data_class" option
$emptyData = function (Options $options) {
$class = $options['data_class'];
if (null !== $class) {
return function (FormInterface $form) use ($class) {
return $form->isEmpty() && !$form->isRequired() ? null : new $class();
};
}
return function (FormInterface $form) {
return $form->getConfig()->getCompound() ? [] : '';
};
};
// Wrap "post_max_size_message" in a closure to translate it lazily
$uploadMaxSizeMessage = function (Options $options) {
return function () use ($options) {
return $options['post_max_size_message'];
};
};
// For any form that is not represented by a single HTML control,
// errors should bubble up by default
$errorBubbling = function (Options $options) {
return $options['compound'] && !$options['inherit_data'];
};
// If data is given, the form is locked to that data
// (independent of its value)
$resolver->setDefined([
'data',
]);
$resolver->setDefaults([
'data_class' => $dataClass,
'empty_data' => $emptyData,
'trim' => true,
'required' => true,
'property_path' => null,
'mapped' => true,
'by_reference' => true,
'error_bubbling' => $errorBubbling,
'label_attr' => [],
'inherit_data' => false,
'compound' => true,
'method' => 'POST',
// According to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt)
// section 4.2., empty URIs are considered same-document references
'action' => '',
'attr' => [],
'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.',
'upload_max_size_message' => $uploadMaxSizeMessage, // internal
'allow_file_upload' => false,
'help' => null,
'help_attr' => [],
'help_html' => false,
'help_translation_parameters' => [],
'invalid_message' => 'This value is not valid.',
'invalid_message_parameters' => [],
'is_empty_callback' => null,
'getter' => null,
'setter' => null,
'form_attr' => false,
]);
$resolver->setAllowedTypes('label_attr', 'array');
$resolver->setAllowedTypes('action', 'string');
$resolver->setAllowedTypes('upload_max_size_message', ['callable']);
$resolver->setAllowedTypes('help', ['string', 'null', TranslatableMessage::class]);
$resolver->setAllowedTypes('help_attr', 'array');
$resolver->setAllowedTypes('help_html', 'bool');
$resolver->setAllowedTypes('is_empty_callback', ['null', 'callable']);
$resolver->setAllowedTypes('getter', ['null', 'callable']);
$resolver->setAllowedTypes('setter', ['null', 'callable']);
$resolver->setAllowedTypes('form_attr', ['bool', 'string']);
$resolver->setInfo('getter', 'A callable that accepts two arguments (the view data and the current form field) and must return a value.');
$resolver->setInfo('setter', 'A callable that accepts three arguments (a reference to the view data, the submitted value and the current form field).');
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return null;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'form';
}
}

View File

@ -0,0 +1,46 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class HiddenType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// hidden fields cannot have a required attribute
'required' => false,
// Pass errors to the parent
'error_bubbling' => true,
'compound' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'The hidden field is invalid.';
},
]);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'hidden';
}
}

View File

@ -0,0 +1,77 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class IntegerType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new IntegerToLocalizedStringTransformer($options['grouping'], $options['rounding_mode'], !$options['grouping'] ? 'en' : null));
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if ($options['grouping']) {
$view->vars['type'] = 'text';
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'grouping' => false,
// Integer cast rounds towards 0, so do the same when displaying fractions
'rounding_mode' => \NumberFormatter::ROUND_DOWN,
'compound' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter an integer.';
},
]);
$resolver->setAllowedValues('rounding_mode', [
\NumberFormatter::ROUND_FLOOR,
\NumberFormatter::ROUND_DOWN,
\NumberFormatter::ROUND_HALFDOWN,
\NumberFormatter::ROUND_HALFEVEN,
\NumberFormatter::ROUND_HALFUP,
\NumberFormatter::ROUND_UP,
\NumberFormatter::ROUND_CEILING,
]);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'integer';
}
}

View File

@ -0,0 +1,96 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Intl\Exception\MissingResourceException;
use Symfony\Component\Intl\Intl;
use Symfony\Component\Intl\Languages;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LanguageType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'choice_loader' => function (Options $options) {
if (!class_exists(Intl::class)) {
throw new LogicException(sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
}
$choiceTranslationLocale = $options['choice_translation_locale'];
$useAlpha3Codes = $options['alpha3'];
$choiceSelfTranslation = $options['choice_self_translation'];
return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation) {
if (true === $choiceSelfTranslation) {
foreach (Languages::getLanguageCodes() as $alpha2Code) {
try {
$languageCode = $useAlpha3Codes ? Languages::getAlpha3Code($alpha2Code) : $alpha2Code;
$languagesList[$languageCode] = Languages::getName($alpha2Code, $alpha2Code);
} catch (MissingResourceException $e) {
// ignore errors like "Couldn't read the indices for the locale 'meta'"
}
}
} else {
$languagesList = $useAlpha3Codes ? Languages::getAlpha3Names($choiceTranslationLocale) : Languages::getNames($choiceTranslationLocale);
}
return array_flip($languagesList);
}), [$choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation]);
},
'choice_translation_domain' => false,
'choice_translation_locale' => null,
'alpha3' => false,
'choice_self_translation' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please select a valid language.';
},
]);
$resolver->setAllowedTypes('choice_self_translation', ['bool']);
$resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
$resolver->setAllowedTypes('alpha3', 'bool');
$resolver->setNormalizer('choice_self_translation', function (Options $options, $value) {
if (true === $value && $options['choice_translation_locale']) {
throw new LogicException('Cannot use the "choice_self_translation" and "choice_translation_locale" options at the same time. Remove one of them.');
}
return $value;
});
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return ChoiceType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'language';
}
}

View File

@ -0,0 +1,69 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Intl\Intl;
use Symfony\Component\Intl\Locales;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LocaleType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'choice_loader' => function (Options $options) {
if (!class_exists(Intl::class)) {
throw new LogicException(sprintf('The "symfony/intl" component is required to use "%s". Try running "composer require symfony/intl".', static::class));
}
$choiceTranslationLocale = $options['choice_translation_locale'];
return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale) {
return array_flip(Locales::getNames($choiceTranslationLocale));
}), $choiceTranslationLocale);
},
'choice_translation_domain' => false,
'choice_translation_locale' => null,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please select a valid locale.';
},
]);
$resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return ChoiceType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'locale';
}
}

View File

@ -0,0 +1,149 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MoneyType extends AbstractType
{
protected static $patterns = [];
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Values used in HTML5 number inputs should be formatted as in "1234.5", ie. 'en' format without grouping,
// according to https://www.w3.org/TR/html51/sec-forms.html#date-time-and-number-formats
$builder
->addViewTransformer(new MoneyToLocalizedStringTransformer(
$options['scale'],
$options['grouping'],
$options['rounding_mode'],
$options['divisor'],
$options['html5'] ? 'en' : null
))
;
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['money_pattern'] = self::getPattern($options['currency']);
if ($options['html5']) {
$view->vars['type'] = 'number';
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'scale' => 2,
'grouping' => false,
'rounding_mode' => \NumberFormatter::ROUND_HALFUP,
'divisor' => 1,
'currency' => 'EUR',
'compound' => false,
'html5' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a valid money amount.';
},
]);
$resolver->setAllowedValues('rounding_mode', [
\NumberFormatter::ROUND_FLOOR,
\NumberFormatter::ROUND_DOWN,
\NumberFormatter::ROUND_HALFDOWN,
\NumberFormatter::ROUND_HALFEVEN,
\NumberFormatter::ROUND_HALFUP,
\NumberFormatter::ROUND_UP,
\NumberFormatter::ROUND_CEILING,
]);
$resolver->setAllowedTypes('scale', 'int');
$resolver->setAllowedTypes('html5', 'bool');
$resolver->setNormalizer('grouping', function (Options $options, $value) {
if ($value && $options['html5']) {
throw new LogicException('Cannot use the "grouping" option when the "html5" option is enabled.');
}
return $value;
});
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'money';
}
/**
* Returns the pattern for this locale in UTF-8.
*
* The pattern contains the placeholder "{{ widget }}" where the HTML tag should
* be inserted
*/
protected static function getPattern(?string $currency)
{
if (!$currency) {
return '{{ widget }}';
}
$locale = \Locale::getDefault();
if (!isset(self::$patterns[$locale])) {
self::$patterns[$locale] = [];
}
if (!isset(self::$patterns[$locale][$currency])) {
$format = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
$pattern = $format->formatCurrency('123', $currency);
// the spacings between currency symbol and number are ignored, because
// a single space leads to better readability in combination with input
// fields
// the regex also considers non-break spaces (0xC2 or 0xA0 in UTF-8)
preg_match('/^([^\s\xc2\xa0]*)[\s\xc2\xa0]*123(?:[,.]0+)?[\s\xc2\xa0]*([^\s\xc2\xa0]*)$/u', $pattern, $matches);
if (!empty($matches[1])) {
self::$patterns[$locale][$currency] = $matches[1].' {{ widget }}';
} elseif (!empty($matches[2])) {
self::$patterns[$locale][$currency] = '{{ widget }} '.$matches[2];
} else {
self::$patterns[$locale][$currency] = '{{ widget }}';
}
}
return self::$patterns[$locale][$currency];
}
}

View File

@ -0,0 +1,102 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\StringToFloatTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class NumberType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new NumberToLocalizedStringTransformer(
$options['scale'],
$options['grouping'],
$options['rounding_mode'],
$options['html5'] ? 'en' : null
));
if ('string' === $options['input']) {
$builder->addModelTransformer(new StringToFloatTransformer($options['scale']));
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if ($options['html5']) {
$view->vars['type'] = 'number';
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// default scale is locale specific (usually around 3)
'scale' => null,
'grouping' => false,
'rounding_mode' => \NumberFormatter::ROUND_HALFUP,
'compound' => false,
'input' => 'number',
'html5' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a number.';
},
]);
$resolver->setAllowedValues('rounding_mode', [
\NumberFormatter::ROUND_FLOOR,
\NumberFormatter::ROUND_DOWN,
\NumberFormatter::ROUND_HALFDOWN,
\NumberFormatter::ROUND_HALFEVEN,
\NumberFormatter::ROUND_HALFUP,
\NumberFormatter::ROUND_UP,
\NumberFormatter::ROUND_CEILING,
]);
$resolver->setAllowedValues('input', ['number', 'string']);
$resolver->setAllowedTypes('scale', ['null', 'int']);
$resolver->setAllowedTypes('html5', 'bool');
$resolver->setNormalizer('grouping', function (Options $options, $value) {
if (true === $value && $options['html5']) {
throw new LogicException('Cannot use the "grouping" option when the "html5" option is enabled.');
}
return $value;
});
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'number';
}
}

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\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PasswordType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if ($options['always_empty'] || !$form->isSubmitted()) {
$view->vars['value'] = '';
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'always_empty' => true,
'trim' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'The password is invalid.';
},
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'password';
}
}

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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PercentType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new PercentToLocalizedStringTransformer(
$options['scale'],
$options['type'],
$options['rounding_mode'],
$options['html5']
));
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['symbol'] = $options['symbol'];
if ($options['html5']) {
$view->vars['type'] = 'number';
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'scale' => 0,
'rounding_mode' => function (Options $options) {
trigger_deprecation('symfony/form', '5.1', 'Not configuring the "rounding_mode" option is deprecated. It will default to "\NumberFormatter::ROUND_HALFUP" in Symfony 6.0.');
return null;
},
'symbol' => '%',
'type' => 'fractional',
'compound' => false,
'html5' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a percentage value.';
},
]);
$resolver->setAllowedValues('type', [
'fractional',
'integer',
]);
$resolver->setAllowedValues('rounding_mode', [
null,
\NumberFormatter::ROUND_FLOOR,
\NumberFormatter::ROUND_DOWN,
\NumberFormatter::ROUND_HALFDOWN,
\NumberFormatter::ROUND_HALFEVEN,
\NumberFormatter::ROUND_HALFUP,
\NumberFormatter::ROUND_UP,
\NumberFormatter::ROUND_CEILING,
]);
$resolver->setAllowedTypes('scale', 'int');
$resolver->setAllowedTypes('symbol', ['bool', 'string']);
$resolver->setDeprecated('rounding_mode', 'symfony/form', '5.1', function (Options $options, $roundingMode) {
if (null === $roundingMode) {
return 'Not configuring the "rounding_mode" option is deprecated. It will default to "\NumberFormatter::ROUND_HALFUP" in Symfony 6.0.';
}
return '';
});
$resolver->setAllowedTypes('html5', 'bool');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'percent';
}
}

View File

@ -0,0 +1,49 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class RadioType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please select a valid option.';
},
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return CheckboxType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'radio';
}
}

View File

@ -0,0 +1,49 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class RangeType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please choose a valid range.';
},
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'range';
}
}

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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\DataTransformer\ValueToDuplicatesTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class RepeatedType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Overwrite required option for child fields
$options['first_options']['required'] = $options['required'];
$options['second_options']['required'] = $options['required'];
if (!isset($options['options']['error_bubbling'])) {
$options['options']['error_bubbling'] = $options['error_bubbling'];
}
// children fields must always be mapped
$defaultOptions = ['mapped' => true];
$builder
->addViewTransformer(new ValueToDuplicatesTransformer([
$options['first_name'],
$options['second_name'],
]))
->add($options['first_name'], $options['type'], array_merge($options['options'], $options['first_options'], $defaultOptions))
->add($options['second_name'], $options['type'], array_merge($options['options'], $options['second_options'], $defaultOptions))
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'type' => TextType::class,
'options' => [],
'first_options' => [],
'second_options' => [],
'first_name' => 'first',
'second_name' => 'second',
'error_bubbling' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'The values do not match.';
},
]);
$resolver->setAllowedTypes('options', 'array');
$resolver->setAllowedTypes('first_options', 'array');
$resolver->setAllowedTypes('second_options', 'array');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'repeated';
}
}

View File

@ -0,0 +1,39 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ButtonTypeInterface;
/**
* A reset button.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ResetType extends AbstractType implements ButtonTypeInterface
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return ButtonType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'reset';
}
}

View File

@ -0,0 +1,49 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SearchType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a valid search term.';
},
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'search';
}
}

View File

@ -0,0 +1,60 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\SubmitButtonTypeInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* A submit button.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class SubmitType extends AbstractType implements SubmitButtonTypeInterface
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['clicked'] = $form->isClicked();
if (!$options['validate']) {
$view->vars['attr']['formnovalidate'] = true;
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('validate', true);
$resolver->setAllowedTypes('validate', 'bool');
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return ButtonType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'submit';
}
}

View File

@ -0,0 +1,49 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TelType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please provide a valid phone number.';
},
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'tel';
}
}

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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TextType extends AbstractType implements DataTransformerInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// When empty_data is explicitly set to an empty string,
// a string should always be returned when NULL is submitted
// This gives more control and thus helps preventing some issues
// with PHP 7 which allows type hinting strings in functions
// See https://github.com/symfony/symfony/issues/5906#issuecomment-203189375
if ('' === $options['empty_data']) {
$builder->addViewTransformer($this);
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'compound' => false,
]);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'text';
}
/**
* {@inheritdoc}
*/
public function transform($data)
{
// Model data should not be transformed
return $data;
}
/**
* {@inheritdoc}
*/
public function reverseTransform($data)
{
return $data ?? '';
}
}

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\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
class TextareaType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['pattern'] = null;
unset($view->vars['attr']['pattern']);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'textarea';
}
}

View File

@ -0,0 +1,386 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TimeType extends AbstractType
{
private const WIDGETS = [
'text' => TextType::class,
'choice' => ChoiceType::class,
];
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$parts = ['hour'];
$format = 'H';
if ($options['with_seconds'] && !$options['with_minutes']) {
throw new InvalidConfigurationException('You cannot disable minutes if you have enabled seconds.');
}
if (null !== $options['reference_date'] && $options['reference_date']->getTimezone()->getName() !== $options['model_timezone']) {
throw new InvalidConfigurationException(sprintf('The configured "model_timezone" (%s) must match the timezone of the "reference_date" (%s).', $options['model_timezone'], $options['reference_date']->getTimezone()->getName()));
}
if ($options['with_minutes']) {
$format .= ':i';
$parts[] = 'minute';
}
if ($options['with_seconds']) {
$format .= ':s';
$parts[] = 'second';
}
if ('single_text' === $options['widget']) {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $e) use ($options) {
$data = $e->getData();
if ($data && preg_match('/^(?P<hours>\d{2}):(?P<minutes>\d{2})(?::(?P<seconds>\d{2})(?:\.\d+)?)?$/', $data, $matches)) {
if ($options['with_seconds']) {
// handle seconds ignored by user's browser when with_seconds enabled
// https://codereview.chromium.org/450533009/
$e->setData(sprintf('%s:%s:%s', $matches['hours'], $matches['minutes'], $matches['seconds'] ?? '00'));
} else {
$e->setData(sprintf('%s:%s', $matches['hours'], $matches['minutes']));
}
}
});
if (null !== $options['reference_date']) {
$format = 'Y-m-d '.$format;
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
$data = $event->getData();
if (preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $data)) {
$event->setData($options['reference_date']->format('Y-m-d ').$data);
}
});
}
$builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format));
} else {
$hourOptions = $minuteOptions = $secondOptions = [
'error_bubbling' => true,
'empty_data' => '',
];
// when the form is compound the entries of the array are ignored in favor of children data
// so we need to handle the cascade setting here
$emptyData = $builder->getEmptyData() ?: [];
if ($emptyData instanceof \Closure) {
$lazyEmptyData = static function ($option) use ($emptyData) {
return static function (FormInterface $form) use ($emptyData, $option) {
$emptyData = $emptyData($form->getParent());
return $emptyData[$option] ?? '';
};
};
$hourOptions['empty_data'] = $lazyEmptyData('hour');
} elseif (isset($emptyData['hour'])) {
$hourOptions['empty_data'] = $emptyData['hour'];
}
if (isset($options['invalid_message'])) {
$hourOptions['invalid_message'] = $options['invalid_message'];
$minuteOptions['invalid_message'] = $options['invalid_message'];
$secondOptions['invalid_message'] = $options['invalid_message'];
}
if (isset($options['invalid_message_parameters'])) {
$hourOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
$minuteOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
$secondOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
}
if ('choice' === $options['widget']) {
$hours = $minutes = [];
foreach ($options['hours'] as $hour) {
$hours[str_pad($hour, 2, '0', \STR_PAD_LEFT)] = $hour;
}
// Only pass a subset of the options to children
$hourOptions['choices'] = $hours;
$hourOptions['placeholder'] = $options['placeholder']['hour'];
$hourOptions['choice_translation_domain'] = $options['choice_translation_domain']['hour'];
if ($options['with_minutes']) {
foreach ($options['minutes'] as $minute) {
$minutes[str_pad($minute, 2, '0', \STR_PAD_LEFT)] = $minute;
}
$minuteOptions['choices'] = $minutes;
$minuteOptions['placeholder'] = $options['placeholder']['minute'];
$minuteOptions['choice_translation_domain'] = $options['choice_translation_domain']['minute'];
}
if ($options['with_seconds']) {
$seconds = [];
foreach ($options['seconds'] as $second) {
$seconds[str_pad($second, 2, '0', \STR_PAD_LEFT)] = $second;
}
$secondOptions['choices'] = $seconds;
$secondOptions['placeholder'] = $options['placeholder']['second'];
$secondOptions['choice_translation_domain'] = $options['choice_translation_domain']['second'];
}
// Append generic carry-along options
foreach (['required', 'translation_domain'] as $passOpt) {
$hourOptions[$passOpt] = $options[$passOpt];
if ($options['with_minutes']) {
$minuteOptions[$passOpt] = $options[$passOpt];
}
if ($options['with_seconds']) {
$secondOptions[$passOpt] = $options[$passOpt];
}
}
}
$builder->add('hour', self::WIDGETS[$options['widget']], $hourOptions);
if ($options['with_minutes']) {
if ($emptyData instanceof \Closure) {
$minuteOptions['empty_data'] = $lazyEmptyData('minute');
} elseif (isset($emptyData['minute'])) {
$minuteOptions['empty_data'] = $emptyData['minute'];
}
$builder->add('minute', self::WIDGETS[$options['widget']], $minuteOptions);
}
if ($options['with_seconds']) {
if ($emptyData instanceof \Closure) {
$secondOptions['empty_data'] = $lazyEmptyData('second');
} elseif (isset($emptyData['second'])) {
$secondOptions['empty_data'] = $emptyData['second'];
}
$builder->add('second', self::WIDGETS[$options['widget']], $secondOptions);
}
$builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget'], $options['reference_date']));
}
if ('datetime_immutable' === $options['input']) {
$builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
} elseif ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format'])
));
} elseif ('timestamp' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
));
} elseif ('array' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts, 'text' === $options['widget'], $options['reference_date'])
));
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, [
'widget' => $options['widget'],
'with_minutes' => $options['with_minutes'],
'with_seconds' => $options['with_seconds'],
]);
// Change the input to an HTML5 time input if
// * the widget is set to "single_text"
// * the html5 is set to true
if ($options['html5'] && 'single_text' === $options['widget']) {
$view->vars['type'] = 'time';
// we need to force the browser to display the seconds by
// adding the HTML attribute step if not already defined.
// Otherwise the browser will not display and so not send the seconds
// therefore the value will always be considered as invalid.
if ($options['with_seconds'] && !isset($view->vars['attr']['step'])) {
$view->vars['attr']['step'] = 1;
}
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$compound = function (Options $options) {
return 'single_text' !== $options['widget'];
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) {
if (\is_array($placeholder)) {
$default = $placeholderDefault($options);
return array_merge(
['hour' => $default, 'minute' => $default, 'second' => $default],
$placeholder
);
}
return [
'hour' => $placeholder,
'minute' => $placeholder,
'second' => $placeholder,
];
};
$choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) {
if (\is_array($choiceTranslationDomain)) {
$default = false;
return array_replace(
['hour' => $default, 'minute' => $default, 'second' => $default],
$choiceTranslationDomain
);
}
return [
'hour' => $choiceTranslationDomain,
'minute' => $choiceTranslationDomain,
'second' => $choiceTranslationDomain,
];
};
$modelTimezone = static function (Options $options, $value): ?string {
if (null !== $value) {
return $value;
}
if (null !== $options['reference_date']) {
return $options['reference_date']->getTimezone()->getName();
}
return null;
};
$viewTimezone = static function (Options $options, $value): ?string {
if (null !== $value) {
return $value;
}
if (null !== $options['model_timezone'] && null === $options['reference_date']) {
return $options['model_timezone'];
}
return null;
};
$resolver->setDefaults([
'hours' => range(0, 23),
'minutes' => range(0, 59),
'seconds' => range(0, 59),
'widget' => 'choice',
'input' => 'datetime',
'input_format' => 'H:i:s',
'with_minutes' => true,
'with_seconds' => false,
'model_timezone' => $modelTimezone,
'view_timezone' => $viewTimezone,
'reference_date' => null,
'placeholder' => $placeholderDefault,
'html5' => true,
// Don't modify \DateTime classes by reference, we treat
// them like immutable value objects
'by_reference' => false,
'error_bubbling' => false,
// If initialized with a \DateTime object, FormType initializes
// this option to "\DateTime". Since the internal, normalized
// representation is not \DateTime, but an array, we need to unset
// this option.
'data_class' => null,
'empty_data' => function (Options $options) {
return $options['compound'] ? [] : '';
},
'compound' => $compound,
'choice_translation_domain' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a valid time.';
},
]);
$resolver->setNormalizer('view_timezone', function (Options $options, $viewTimezone): ?string {
if (null !== $options['model_timezone'] && $viewTimezone !== $options['model_timezone'] && null === $options['reference_date']) {
throw new LogicException('Using different values for the "model_timezone" and "view_timezone" options without configuring a reference date is not supported.');
}
return $viewTimezone;
});
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setAllowedValues('input', [
'datetime',
'datetime_immutable',
'string',
'timestamp',
'array',
]);
$resolver->setAllowedValues('widget', [
'single_text',
'text',
'choice',
]);
$resolver->setAllowedTypes('hours', 'array');
$resolver->setAllowedTypes('minutes', 'array');
$resolver->setAllowedTypes('seconds', 'array');
$resolver->setAllowedTypes('input_format', 'string');
$resolver->setAllowedTypes('model_timezone', ['null', 'string']);
$resolver->setAllowedTypes('view_timezone', ['null', 'string']);
$resolver->setAllowedTypes('reference_date', ['null', \DateTimeInterface::class]);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'time';
}
}

View File

@ -0,0 +1,143 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeZoneToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\IntlTimeZoneToStringTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Intl\Intl;
use Symfony\Component\Intl\Timezones;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TimezoneType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ('datetimezone' === $options['input']) {
$builder->addModelTransformer(new DateTimeZoneToStringTransformer($options['multiple']));
} elseif ('intltimezone' === $options['input']) {
$builder->addModelTransformer(new IntlTimeZoneToStringTransformer($options['multiple']));
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'intl' => false,
'choice_loader' => function (Options $options) {
$input = $options['input'];
if ($options['intl']) {
if (!class_exists(Intl::class)) {
throw new LogicException(sprintf('The "symfony/intl" component is required to use "%s" with option "intl=true". Try running "composer require symfony/intl".', static::class));
}
$choiceTranslationLocale = $options['choice_translation_locale'];
return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($input, $choiceTranslationLocale) {
return self::getIntlTimezones($input, $choiceTranslationLocale);
}), [$input, $choiceTranslationLocale]);
}
return ChoiceList::lazy($this, function () use ($input) {
return self::getPhpTimezones($input);
}, $input);
},
'choice_translation_domain' => false,
'choice_translation_locale' => null,
'input' => 'string',
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please select a valid timezone.';
},
'regions' => \DateTimeZone::ALL,
]);
$resolver->setAllowedTypes('intl', ['bool']);
$resolver->setAllowedTypes('choice_translation_locale', ['null', 'string']);
$resolver->setNormalizer('choice_translation_locale', function (Options $options, $value) {
if (null !== $value && !$options['intl']) {
throw new LogicException('The "choice_translation_locale" option can only be used if the "intl" option is set to true.');
}
return $value;
});
$resolver->setAllowedValues('input', ['string', 'datetimezone', 'intltimezone']);
$resolver->setNormalizer('input', function (Options $options, $value) {
if ('intltimezone' === $value && !class_exists(\IntlTimeZone::class)) {
throw new LogicException('Cannot use "intltimezone" input because the PHP intl extension is not available.');
}
return $value;
});
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return ChoiceType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'timezone';
}
private static function getPhpTimezones(string $input): array
{
$timezones = [];
foreach (\DateTimeZone::listIdentifiers(\DateTimeZone::ALL) as $timezone) {
if ('intltimezone' === $input && 'Etc/Unknown' === \IntlTimeZone::createTimeZone($timezone)->getID()) {
continue;
}
$timezones[str_replace(['/', '_'], [' / ', ' '], $timezone)] = $timezone;
}
return $timezones;
}
private static function getIntlTimezones(string $input, string $locale = null): array
{
$timezones = array_flip(Timezones::getNames($locale));
if ('intltimezone' === $input) {
foreach ($timezones as $name => $timezone) {
if ('Etc/Unknown' === \IntlTimeZone::createTimeZone($timezone)->getID()) {
unset($timezones[$name]);
}
}
}
return $timezones;
}
}

View File

@ -0,0 +1,45 @@
<?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\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\EventListener\TransformationFailureListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*/
class TransformationFailureExtension extends AbstractTypeExtension
{
private $translator;
public function __construct(TranslatorInterface $translator = null)
{
$this->translator = $translator;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!isset($options['constraints'])) {
$builder->addEventSubscriber(new TransformationFailureListener($this->translator));
}
}
/**
* {@inheritdoc}
*/
public static function getExtendedTypes(): iterable
{
return [FormType::class];
}
}

View File

@ -0,0 +1,49 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\DataTransformer\UlidToStringTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Pavel Dyakonov <wapinet@mail.ru>
*/
class UlidType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addViewTransformer(new UlidToStringTransformer())
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'compound' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a valid ULID.';
},
]);
}
}

View File

@ -0,0 +1,77 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\EventListener\FixUrlProtocolListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UrlType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (null !== $options['default_protocol']) {
$builder->addEventSubscriber(new FixUrlProtocolListener($options['default_protocol']));
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if ($options['default_protocol']) {
$view->vars['attr']['inputmode'] = 'url';
$view->vars['type'] = 'text';
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'default_protocol' => 'http',
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a valid URL.';
},
]);
$resolver->setAllowedTypes('default_protocol', ['null', 'string']);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'url';
}
}

View File

@ -0,0 +1,49 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\DataTransformer\UuidToStringTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Pavel Dyakonov <wapinet@mail.ru>
*/
class UuidType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addViewTransformer(new UuidToStringTransformer())
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'compound' => false,
'invalid_message' => function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true)
? $previousValue
: 'Please enter a valid UUID.';
},
]);
}
}

View File

@ -0,0 +1,195 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\WeekToArrayTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class WeekType extends AbstractType
{
private const WIDGETS = [
'text' => IntegerType::class,
'choice' => ChoiceType::class,
];
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ('string' === $options['input']) {
$builder->addModelTransformer(new WeekToArrayTransformer());
}
if ('single_text' === $options['widget']) {
$builder->addViewTransformer(new ReversedTransformer(new WeekToArrayTransformer()));
} else {
$yearOptions = $weekOptions = [
'error_bubbling' => true,
'empty_data' => '',
];
// when the form is compound the entries of the array are ignored in favor of children data
// so we need to handle the cascade setting here
$emptyData = $builder->getEmptyData() ?: [];
$yearOptions['empty_data'] = $emptyData['year'] ?? '';
$weekOptions['empty_data'] = $emptyData['week'] ?? '';
if (isset($options['invalid_message'])) {
$yearOptions['invalid_message'] = $options['invalid_message'];
$weekOptions['invalid_message'] = $options['invalid_message'];
}
if (isset($options['invalid_message_parameters'])) {
$yearOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
$weekOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
}
if ('choice' === $options['widget']) {
// Only pass a subset of the options to children
$yearOptions['choices'] = array_combine($options['years'], $options['years']);
$yearOptions['placeholder'] = $options['placeholder']['year'];
$yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year'];
$weekOptions['choices'] = array_combine($options['weeks'], $options['weeks']);
$weekOptions['placeholder'] = $options['placeholder']['week'];
$weekOptions['choice_translation_domain'] = $options['choice_translation_domain']['week'];
// Append generic carry-along options
foreach (['required', 'translation_domain'] as $passOpt) {
$yearOptions[$passOpt] = $options[$passOpt];
$weekOptions[$passOpt] = $options[$passOpt];
}
}
$builder->add('year', self::WIDGETS[$options['widget']], $yearOptions);
$builder->add('week', self::WIDGETS[$options['widget']], $weekOptions);
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['widget'] = $options['widget'];
if ($options['html5']) {
$view->vars['type'] = 'week';
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$compound = function (Options $options) {
return 'single_text' !== $options['widget'];
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) {
if (\is_array($placeholder)) {
$default = $placeholderDefault($options);
return array_merge(
['year' => $default, 'week' => $default],
$placeholder
);
}
return [
'year' => $placeholder,
'week' => $placeholder,
];
};
$choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) {
if (\is_array($choiceTranslationDomain)) {
$default = false;
return array_replace(
['year' => $default, 'week' => $default],
$choiceTranslationDomain
);
}
return [
'year' => $choiceTranslationDomain,
'week' => $choiceTranslationDomain,
];
};
$resolver->setDefaults([
'years' => range(date('Y') - 10, date('Y') + 10),
'weeks' => array_combine(range(1, 53), range(1, 53)),
'widget' => 'single_text',
'input' => 'array',
'placeholder' => $placeholderDefault,
'html5' => static function (Options $options) {
return 'single_text' === $options['widget'];
},
'error_bubbling' => false,
'empty_data' => function (Options $options) {
return $options['compound'] ? [] : '';
},
'compound' => $compound,
'choice_translation_domain' => false,
'invalid_message' => static function (Options $options, $previousValue) {
return ($options['legacy_error_messages'] ?? true) ? $previousValue : 'Please enter a valid week.';
},
]);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setNormalizer('html5', function (Options $options, $html5) {
if ($html5 && 'single_text' !== $options['widget']) {
throw new LogicException(sprintf('The "widget" option of "%s" must be set to "single_text" when the "html5" option is enabled.', self::class));
}
return $html5;
});
$resolver->setAllowedValues('input', [
'string',
'array',
]);
$resolver->setAllowedValues('widget', [
'single_text',
'text',
'choice',
]);
$resolver->setAllowedTypes('years', 'int[]');
$resolver->setAllowedTypes('weeks', 'int[]');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'week';
}
}

View File

@ -0,0 +1,45 @@
<?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\Csrf;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* This extension protects forms by using a CSRF token.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CsrfExtension extends AbstractExtension
{
private $tokenManager;
private $translator;
private $translationDomain;
public function __construct(CsrfTokenManagerInterface $tokenManager, TranslatorInterface $translator = null, string $translationDomain = null)
{
$this->tokenManager = $tokenManager;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
}
/**
* {@inheritdoc}
*/
protected function loadTypeExtensions()
{
return [
new Type\FormTypeCsrfExtension($this->tokenManager, true, '_token', $this->translator, $this->translationDomain),
];
}
}

View File

@ -0,0 +1,81 @@
<?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\Csrf\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CsrfValidationListener implements EventSubscriberInterface
{
private $fieldName;
private $tokenManager;
private $tokenId;
private $errorMessage;
private $translator;
private $translationDomain;
private $serverParams;
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SUBMIT => 'preSubmit',
];
}
public function __construct(string $fieldName, CsrfTokenManagerInterface $tokenManager, string $tokenId, string $errorMessage, TranslatorInterface $translator = null, string $translationDomain = null, ServerParams $serverParams = null)
{
$this->fieldName = $fieldName;
$this->tokenManager = $tokenManager;
$this->tokenId = $tokenId;
$this->errorMessage = $errorMessage;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
$this->serverParams = $serverParams ?? new ServerParams();
}
public function preSubmit(FormEvent $event)
{
$form = $event->getForm();
$postRequestSizeExceeded = 'POST' === $form->getConfig()->getMethod() && $this->serverParams->hasPostMaxSizeBeenExceeded();
if ($form->isRoot() && $form->getConfig()->getOption('compound') && !$postRequestSizeExceeded) {
$data = $event->getData();
$csrfValue = \is_string($data[$this->fieldName] ?? null) ? $data[$this->fieldName] : null;
$csrfToken = new CsrfToken($this->tokenId, $csrfValue);
if (null === $csrfValue || !$this->tokenManager->isTokenValid($csrfToken)) {
$errorMessage = $this->errorMessage;
if (null !== $this->translator) {
$errorMessage = $this->translator->trans($errorMessage, [], $this->translationDomain);
}
$form->addError(new FormError($errorMessage, $errorMessage, [], null, $csrfToken));
}
if (\is_array($data)) {
unset($data[$this->fieldName]);
$event->setData($data);
}
}
}
}

View File

@ -0,0 +1,110 @@
<?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\Csrf\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormTypeCsrfExtension extends AbstractTypeExtension
{
private $defaultTokenManager;
private $defaultEnabled;
private $defaultFieldName;
private $translator;
private $translationDomain;
private $serverParams;
public function __construct(CsrfTokenManagerInterface $defaultTokenManager, bool $defaultEnabled = true, string $defaultFieldName = '_token', TranslatorInterface $translator = null, string $translationDomain = null, ServerParams $serverParams = null)
{
$this->defaultTokenManager = $defaultTokenManager;
$this->defaultEnabled = $defaultEnabled;
$this->defaultFieldName = $defaultFieldName;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
$this->serverParams = $serverParams;
}
/**
* Adds a CSRF field to the form when the CSRF protection is enabled.
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['csrf_protection']) {
return;
}
$builder
->addEventSubscriber(new CsrfValidationListener(
$options['csrf_field_name'],
$options['csrf_token_manager'],
$options['csrf_token_id'] ?: ($builder->getName() ?: \get_class($builder->getType()->getInnerType())),
$options['csrf_message'],
$this->translator,
$this->translationDomain,
$this->serverParams
))
;
}
/**
* Adds a CSRF field to the root form view.
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
if ($options['csrf_protection'] && !$view->parent && $options['compound']) {
$factory = $form->getConfig()->getFormFactory();
$tokenId = $options['csrf_token_id'] ?: ($form->getName() ?: \get_class($form->getConfig()->getType()->getInnerType()));
$data = (string) $options['csrf_token_manager']->getToken($tokenId);
$csrfForm = $factory->createNamed($options['csrf_field_name'], HiddenType::class, $data, [
'block_prefix' => 'csrf_token',
'mapped' => false,
]);
$view->children[$options['csrf_field_name']] = $csrfForm->createView($view);
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'csrf_protection' => $this->defaultEnabled,
'csrf_field_name' => $this->defaultFieldName,
'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.',
'csrf_token_manager' => $this->defaultTokenManager,
'csrf_token_id' => null,
]);
}
/**
* {@inheritdoc}
*/
public static function getExtendedTypes(): iterable
{
return [FormType::class];
}
}

View File

@ -0,0 +1,40 @@
<?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\DataCollector;
use Symfony\Component\Form\AbstractExtension;
/**
* Extension for collecting data of the forms on a page.
*
* @author Robert Schönthal <robert.schoenthal@gmail.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DataCollectorExtension extends AbstractExtension
{
private $dataCollector;
public function __construct(FormDataCollectorInterface $dataCollector)
{
$this->dataCollector = $dataCollector;
}
/**
* {@inheritdoc}
*/
protected function loadTypeExtensions()
{
return [
new Type\DataCollectorTypeExtension($this->dataCollector),
];
}
}

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\DataCollector\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
/**
* Listener that invokes a data collector for the {@link FormEvents::POST_SET_DATA}
* and {@link FormEvents::POST_SUBMIT} events.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DataCollectorListener implements EventSubscriberInterface
{
private $dataCollector;
public function __construct(FormDataCollectorInterface $dataCollector)
{
$this->dataCollector = $dataCollector;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
// High priority in order to be called as soon as possible
FormEvents::POST_SET_DATA => ['postSetData', 255],
// Low priority in order to be called as late as possible
FormEvents::POST_SUBMIT => ['postSubmit', -255],
];
}
/**
* Listener for the {@link FormEvents::POST_SET_DATA} event.
*/
public function postSetData(FormEvent $event)
{
if ($event->getForm()->isRoot()) {
// Collect basic information about each form
$this->dataCollector->collectConfiguration($event->getForm());
// Collect the default data
$this->dataCollector->collectDefaultData($event->getForm());
}
}
/**
* Listener for the {@link FormEvents::POST_SUBMIT} event.
*/
public function postSubmit(FormEvent $event)
{
if ($event->getForm()->isRoot()) {
// Collect the submitted data of each form
$this->dataCollector->collectSubmittedData($event->getForm());
// Assemble a form tree
// This is done again after the view is built, but we need it here as the view is not always created.
$this->dataCollector->buildPreliminaryFormTree($event->getForm());
}
}
}

View File

@ -0,0 +1,343 @@
<?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\DataCollector;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Caster\ClassStub;
use Symfony\Component\VarDumper\Caster\StubCaster;
use Symfony\Component\VarDumper\Cloner\Stub;
/**
* Data collector for {@link FormInterface} instances.
*
* @author Robert Schönthal <robert.schoenthal@gmail.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @final
*/
class FormDataCollector extends DataCollector implements FormDataCollectorInterface
{
private $dataExtractor;
/**
* Stores the collected data per {@link FormInterface} instance.
*
* Uses the hashes of the forms as keys. This is preferable over using
* {@link \SplObjectStorage}, because in this way no references are kept
* to the {@link FormInterface} instances.
*
* @var array
*/
private $dataByForm;
/**
* Stores the collected data per {@link FormView} instance.
*
* Uses the hashes of the views as keys. This is preferable over using
* {@link \SplObjectStorage}, because in this way no references are kept
* to the {@link FormView} instances.
*
* @var array
*/
private $dataByView;
/**
* Connects {@link FormView} with {@link FormInterface} instances.
*
* Uses the hashes of the views as keys and the hashes of the forms as
* values. This is preferable over storing the objects directly, because
* this way they can safely be discarded by the GC.
*
* @var array
*/
private $formsByView;
public function __construct(FormDataExtractorInterface $dataExtractor)
{
if (!class_exists(ClassStub::class)) {
throw new \LogicException(sprintf('The VarDumper component is needed for using the "%s" class. Install symfony/var-dumper version 3.4 or above.', __CLASS__));
}
$this->dataExtractor = $dataExtractor;
$this->reset();
}
/**
* Does nothing. The data is collected during the form event listeners.
*/
public function collect(Request $request, Response $response, \Throwable $exception = null)
{
}
public function reset()
{
$this->data = [
'forms' => [],
'forms_by_hash' => [],
'nb_errors' => 0,
];
}
/**
* {@inheritdoc}
*/
public function associateFormWithView(FormInterface $form, FormView $view)
{
$this->formsByView[spl_object_hash($view)] = spl_object_hash($form);
}
/**
* {@inheritdoc}
*/
public function collectConfiguration(FormInterface $form)
{
$hash = spl_object_hash($form);
if (!isset($this->dataByForm[$hash])) {
$this->dataByForm[$hash] = [];
}
$this->dataByForm[$hash] = array_replace(
$this->dataByForm[$hash],
$this->dataExtractor->extractConfiguration($form)
);
foreach ($form as $child) {
$this->collectConfiguration($child);
}
}
/**
* {@inheritdoc}
*/
public function collectDefaultData(FormInterface $form)
{
$hash = spl_object_hash($form);
if (!isset($this->dataByForm[$hash])) {
// field was created by form event
$this->collectConfiguration($form);
}
$this->dataByForm[$hash] = array_replace(
$this->dataByForm[$hash],
$this->dataExtractor->extractDefaultData($form)
);
foreach ($form as $child) {
$this->collectDefaultData($child);
}
}
/**
* {@inheritdoc}
*/
public function collectSubmittedData(FormInterface $form)
{
$hash = spl_object_hash($form);
if (!isset($this->dataByForm[$hash])) {
// field was created by form event
$this->collectConfiguration($form);
$this->collectDefaultData($form);
}
$this->dataByForm[$hash] = array_replace(
$this->dataByForm[$hash],
$this->dataExtractor->extractSubmittedData($form)
);
// Count errors
if (isset($this->dataByForm[$hash]['errors'])) {
$this->data['nb_errors'] += \count($this->dataByForm[$hash]['errors']);
}
foreach ($form as $child) {
$this->collectSubmittedData($child);
// Expand current form if there are children with errors
if (empty($this->dataByForm[$hash]['has_children_error'])) {
$childData = $this->dataByForm[spl_object_hash($child)];
$this->dataByForm[$hash]['has_children_error'] = !empty($childData['has_children_error']) || !empty($childData['errors']);
}
}
}
/**
* {@inheritdoc}
*/
public function collectViewVariables(FormView $view)
{
$hash = spl_object_hash($view);
if (!isset($this->dataByView[$hash])) {
$this->dataByView[$hash] = [];
}
$this->dataByView[$hash] = array_replace(
$this->dataByView[$hash],
$this->dataExtractor->extractViewVariables($view)
);
foreach ($view->children as $child) {
$this->collectViewVariables($child);
}
}
/**
* {@inheritdoc}
*/
public function buildPreliminaryFormTree(FormInterface $form)
{
$this->data['forms'][$form->getName()] = &$this->recursiveBuildPreliminaryFormTree($form, $this->data['forms_by_hash']);
}
/**
* {@inheritdoc}
*/
public function buildFinalFormTree(FormInterface $form, FormView $view)
{
$this->data['forms'][$form->getName()] = &$this->recursiveBuildFinalFormTree($form, $view, $this->data['forms_by_hash']);
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'form';
}
/**
* {@inheritdoc}
*/
public function getData()
{
return $this->data;
}
/**
* @internal
*/
public function __sleep(): array
{
foreach ($this->data['forms_by_hash'] as &$form) {
if (isset($form['type_class']) && !$form['type_class'] instanceof ClassStub) {
$form['type_class'] = new ClassStub($form['type_class']);
}
}
$this->data = $this->cloneVar($this->data);
return parent::__sleep();
}
/**
* {@inheritdoc}
*/
protected function getCasters(): array
{
return parent::getCasters() + [
\Exception::class => function (\Exception $e, array $a, Stub $s) {
foreach (["\0Exception\0previous", "\0Exception\0trace"] as $k) {
if (isset($a[$k])) {
unset($a[$k]);
++$s->cut;
}
}
return $a;
},
FormInterface::class => function (FormInterface $f, array $a) {
return [
Caster::PREFIX_VIRTUAL.'name' => $f->getName(),
Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub(\get_class($f->getConfig()->getType()->getInnerType())),
];
},
FormView::class => [StubCaster::class, 'cutInternals'],
ConstraintViolationInterface::class => function (ConstraintViolationInterface $v, array $a) {
return [
Caster::PREFIX_VIRTUAL.'root' => $v->getRoot(),
Caster::PREFIX_VIRTUAL.'path' => $v->getPropertyPath(),
Caster::PREFIX_VIRTUAL.'value' => $v->getInvalidValue(),
];
},
];
}
private function &recursiveBuildPreliminaryFormTree(FormInterface $form, array &$outputByHash)
{
$hash = spl_object_hash($form);
$output = &$outputByHash[$hash];
$output = $this->dataByForm[$hash]
?? [];
$output['children'] = [];
foreach ($form as $name => $child) {
$output['children'][$name] = &$this->recursiveBuildPreliminaryFormTree($child, $outputByHash);
}
return $output;
}
private function &recursiveBuildFinalFormTree(FormInterface $form = null, FormView $view, array &$outputByHash)
{
$viewHash = spl_object_hash($view);
$formHash = null;
if (null !== $form) {
$formHash = spl_object_hash($form);
} elseif (isset($this->formsByView[$viewHash])) {
// The FormInterface instance of the CSRF token is never contained in
// the FormInterface tree of the form, so we need to get the
// corresponding FormInterface instance for its view in a different way
$formHash = $this->formsByView[$viewHash];
}
if (null !== $formHash) {
$output = &$outputByHash[$formHash];
}
$output = $this->dataByView[$viewHash]
?? [];
if (null !== $formHash) {
$output = array_replace(
$output,
$this->dataByForm[$formHash]
?? []
);
}
$output['children'] = [];
foreach ($view->children as $name => $childView) {
// The CSRF token, for example, is never added to the form tree.
// It is only present in the view.
$childForm = null !== $form && $form->has($name)
? $form->get($name)
: null;
$output['children'][$name] = &$this->recursiveBuildFinalFormTree($childForm, $childView, $outputByHash);
}
return $output;
}
}

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\DataCollector;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
use Symfony\Component\VarDumper\Cloner\Data;
/**
* Collects and structures information about forms.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface FormDataCollectorInterface extends DataCollectorInterface
{
/**
* Stores configuration data of the given form and its children.
*/
public function collectConfiguration(FormInterface $form);
/**
* Stores the default data of the given form and its children.
*/
public function collectDefaultData(FormInterface $form);
/**
* Stores the submitted data of the given form and its children.
*/
public function collectSubmittedData(FormInterface $form);
/**
* Stores the view variables of the given form view and its children.
*/
public function collectViewVariables(FormView $view);
/**
* Specifies that the given objects represent the same conceptual form.
*/
public function associateFormWithView(FormInterface $form, FormView $view);
/**
* Assembles the data collected about the given form and its children as
* a tree-like data structure.
*
* The result can be queried using {@link getData()}.
*/
public function buildPreliminaryFormTree(FormInterface $form);
/**
* Assembles the data collected about the given form and its children as
* a tree-like data structure.
*
* The result can be queried using {@link getData()}.
*
* Contrary to {@link buildPreliminaryFormTree()}, a {@link FormView}
* object has to be passed. The tree structure of this view object will be
* used for structuring the resulting data. That means, if a child is
* present in the view, but not in the form, it will be present in the final
* data array anyway.
*
* When {@link FormView} instances are present in the view tree, for which
* no corresponding {@link FormInterface} objects can be found in the form
* tree, only the view data will be included in the result. If a
* corresponding {@link FormInterface} exists otherwise, call
* {@link associateFormWithView()} before calling this method.
*/
public function buildFinalFormTree(FormInterface $form, FormView $view);
/**
* Returns all collected data.
*
* @return array|Data
*/
public function getData();
}

View File

@ -0,0 +1,168 @@
<?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\DataCollector;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Default implementation of {@link FormDataExtractorInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormDataExtractor implements FormDataExtractorInterface
{
/**
* {@inheritdoc}
*/
public function extractConfiguration(FormInterface $form)
{
$data = [
'id' => $this->buildId($form),
'name' => $form->getName(),
'type_class' => \get_class($form->getConfig()->getType()->getInnerType()),
'synchronized' => $form->isSynchronized(),
'passed_options' => [],
'resolved_options' => [],
];
foreach ($form->getConfig()->getAttribute('data_collector/passed_options', []) as $option => $value) {
$data['passed_options'][$option] = $value;
}
foreach ($form->getConfig()->getOptions() as $option => $value) {
$data['resolved_options'][$option] = $value;
}
ksort($data['passed_options']);
ksort($data['resolved_options']);
return $data;
}
/**
* {@inheritdoc}
*/
public function extractDefaultData(FormInterface $form)
{
$data = [
'default_data' => [
'norm' => $form->getNormData(),
],
'submitted_data' => [],
];
if ($form->getData() !== $form->getNormData()) {
$data['default_data']['model'] = $form->getData();
}
if ($form->getViewData() !== $form->getNormData()) {
$data['default_data']['view'] = $form->getViewData();
}
return $data;
}
/**
* {@inheritdoc}
*/
public function extractSubmittedData(FormInterface $form)
{
$data = [
'submitted_data' => [
'norm' => $form->getNormData(),
],
'errors' => [],
];
if ($form->getViewData() !== $form->getNormData()) {
$data['submitted_data']['view'] = $form->getViewData();
}
if ($form->getData() !== $form->getNormData()) {
$data['submitted_data']['model'] = $form->getData();
}
foreach ($form->getErrors() as $error) {
$errorData = [
'message' => $error->getMessage(),
'origin' => \is_object($error->getOrigin())
? spl_object_hash($error->getOrigin())
: null,
'trace' => [],
];
$cause = $error->getCause();
while (null !== $cause) {
if ($cause instanceof ConstraintViolationInterface) {
$errorData['trace'][] = $cause;
$cause = method_exists($cause, 'getCause') ? $cause->getCause() : null;
continue;
}
if ($cause instanceof \Exception) {
$errorData['trace'][] = $cause;
$cause = $cause->getPrevious();
continue;
}
$errorData['trace'][] = $cause;
break;
}
$data['errors'][] = $errorData;
}
$data['synchronized'] = $form->isSynchronized();
return $data;
}
/**
* {@inheritdoc}
*/
public function extractViewVariables(FormView $view)
{
$data = [
'id' => $view->vars['id'] ?? null,
'name' => $view->vars['name'] ?? null,
'view_vars' => [],
];
foreach ($view->vars as $varName => $value) {
$data['view_vars'][$varName] = $value;
}
ksort($data['view_vars']);
return $data;
}
/**
* Recursively builds an HTML ID for a form.
*/
private function buildId(FormInterface $form): string
{
$id = $form->getName();
if (null !== $form->getParent()) {
$id = $this->buildId($form->getParent()).'_'.$id;
}
return $id;
}
}

View File

@ -0,0 +1,51 @@
<?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\DataCollector;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
/**
* Extracts arrays of information out of forms.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface FormDataExtractorInterface
{
/**
* Extracts the configuration data of a form.
*
* @return array
*/
public function extractConfiguration(FormInterface $form);
/**
* Extracts the default data of a form.
*
* @return array
*/
public function extractDefaultData(FormInterface $form);
/**
* Extracts the submitted data of a form.
*
* @return array
*/
public function extractSubmittedData(FormInterface $form);
/**
* Extracts the view variables of a form.
*
* @return array
*/
public function extractViewVariables(FormView $view);
}

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\Extension\DataCollector\Proxy;
use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\ResolvedFormTypeInterface;
/**
* Proxy that invokes a data collector when creating a form and its view.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ResolvedTypeDataCollectorProxy implements ResolvedFormTypeInterface
{
private $proxiedType;
private $dataCollector;
public function __construct(ResolvedFormTypeInterface $proxiedType, FormDataCollectorInterface $dataCollector)
{
$this->proxiedType = $proxiedType;
$this->dataCollector = $dataCollector;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return $this->proxiedType->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return $this->proxiedType->getParent();
}
/**
* {@inheritdoc}
*/
public function getInnerType()
{
return $this->proxiedType->getInnerType();
}
/**
* {@inheritdoc}
*/
public function getTypeExtensions()
{
return $this->proxiedType->getTypeExtensions();
}
/**
* {@inheritdoc}
*/
public function createBuilder(FormFactoryInterface $factory, string $name, array $options = [])
{
$builder = $this->proxiedType->createBuilder($factory, $name, $options);
$builder->setAttribute('data_collector/passed_options', $options);
$builder->setType($this);
return $builder;
}
/**
* {@inheritdoc}
*/
public function createView(FormInterface $form, FormView $parent = null)
{
return $this->proxiedType->createView($form, $parent);
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$this->proxiedType->buildForm($builder, $options);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$this->proxiedType->buildView($view, $form, $options);
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
$this->proxiedType->finishView($view, $form, $options);
// Remember which view belongs to which form instance, so that we can
// get the collected data for a view when its form instance is not
// available (e.g. CSRF token)
$this->dataCollector->associateFormWithView($form, $view);
// Since the CSRF token is only present in the FormView tree, we also
// need to check the FormView tree instead of calling isRoot() on the
// FormInterface tree
if (null === $view->parent) {
$this->dataCollector->collectViewVariables($view);
// Re-assemble data, in case FormView instances were added, for
// which no FormInterface instances were present (e.g. CSRF token).
// Since finishView() is called after finishing the views of all
// children, we can safely assume that information has been
// collected about the complete form tree.
$this->dataCollector->buildFinalFormTree($form, $view);
}
}
/**
* {@inheritdoc}
*/
public function getOptionsResolver()
{
return $this->proxiedType->getOptionsResolver();
}
}

View File

@ -0,0 +1,46 @@
<?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\DataCollector\Proxy;
use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\Form\ResolvedFormTypeFactoryInterface;
use Symfony\Component\Form\ResolvedFormTypeInterface;
/**
* Proxy that wraps resolved types into {@link ResolvedTypeDataCollectorProxy}
* instances.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ResolvedTypeFactoryDataCollectorProxy implements ResolvedFormTypeFactoryInterface
{
private $proxiedFactory;
private $dataCollector;
public function __construct(ResolvedFormTypeFactoryInterface $proxiedFactory, FormDataCollectorInterface $dataCollector)
{
$this->proxiedFactory = $proxiedFactory;
$this->dataCollector = $dataCollector;
}
/**
* {@inheritdoc}
*/
public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ResolvedFormTypeInterface $parent = null)
{
return new ResolvedTypeDataCollectorProxy(
$this->proxiedFactory->createResolvedType($type, $typeExtensions, $parent),
$this->dataCollector
);
}
}

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\DataCollector\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener;
use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Type extension for collecting data of a form with this type.
*
* @author Robert Schönthal <robert.schoenthal@gmail.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DataCollectorTypeExtension extends AbstractTypeExtension
{
/**
* @var DataCollectorListener
*/
private $listener;
public function __construct(FormDataCollectorInterface $dataCollector)
{
$this->listener = new DataCollectorListener($dataCollector);
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventSubscriber($this->listener);
}
/**
* {@inheritdoc}
*/
public static function getExtendedTypes(): iterable
{
return [FormType::class];
}
}

View File

@ -0,0 +1,111 @@
<?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\DependencyInjection;
use Psr\Container\ContainerInterface;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\FormExtensionInterface;
use Symfony\Component\Form\FormTypeGuesserChain;
class DependencyInjectionExtension implements FormExtensionInterface
{
private $guesser;
private $guesserLoaded = false;
private $typeContainer;
private $typeExtensionServices;
private $guesserServices;
/**
* @param iterable[] $typeExtensionServices
*/
public function __construct(ContainerInterface $typeContainer, array $typeExtensionServices, iterable $guesserServices)
{
$this->typeContainer = $typeContainer;
$this->typeExtensionServices = $typeExtensionServices;
$this->guesserServices = $guesserServices;
}
/**
* {@inheritdoc}
*/
public function getType(string $name)
{
if (!$this->typeContainer->has($name)) {
throw new InvalidArgumentException(sprintf('The field type "%s" is not registered in the service container.', $name));
}
return $this->typeContainer->get($name);
}
/**
* {@inheritdoc}
*/
public function hasType(string $name)
{
return $this->typeContainer->has($name);
}
/**
* {@inheritdoc}
*/
public function getTypeExtensions(string $name)
{
$extensions = [];
if (isset($this->typeExtensionServices[$name])) {
foreach ($this->typeExtensionServices[$name] as $extension) {
$extensions[] = $extension;
$extendedTypes = [];
foreach ($extension::getExtendedTypes() as $extendedType) {
$extendedTypes[] = $extendedType;
}
// validate the result of getExtendedTypes() to ensure it is consistent with the service definition
if (!\in_array($name, $extendedTypes, true)) {
throw new InvalidArgumentException(sprintf('The extended type "%s" specified for the type extension class "%s" does not match any of the actual extended types (["%s"]).', $name, \get_class($extension), implode('", "', $extendedTypes)));
}
}
}
return $extensions;
}
/**
* {@inheritdoc}
*/
public function hasTypeExtensions(string $name)
{
return isset($this->typeExtensionServices[$name]);
}
/**
* {@inheritdoc}
*/
public function getTypeGuesser()
{
if (!$this->guesserLoaded) {
$this->guesserLoaded = true;
$guessers = [];
foreach ($this->guesserServices as $serviceId => $service) {
$guessers[] = $service;
}
if ($guessers) {
$this->guesser = new FormTypeGuesserChain($guessers);
}
}
return $this->guesser;
}
}

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\Extension\HttpFoundation;
use Symfony\Component\Form\AbstractExtension;
/**
* Integrates the HttpFoundation component with the Form library.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class HttpFoundationExtension extends AbstractExtension
{
protected function loadTypeExtensions()
{
return [
new Type\FormTypeHttpFoundationExtension(),
];
}
}

View File

@ -0,0 +1,131 @@
<?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\HttpFoundation;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\RequestHandlerInterface;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
/**
* A request processor using the {@link Request} class of the HttpFoundation
* component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class HttpFoundationRequestHandler implements RequestHandlerInterface
{
private $serverParams;
public function __construct(ServerParams $serverParams = null)
{
$this->serverParams = $serverParams ?? new ServerParams();
}
/**
* {@inheritdoc}
*/
public function handleRequest(FormInterface $form, $request = null)
{
if (!$request instanceof Request) {
throw new UnexpectedTypeException($request, 'Symfony\Component\HttpFoundation\Request');
}
$name = $form->getName();
$method = $form->getConfig()->getMethod();
if ($method !== $request->getMethod()) {
return;
}
// For request methods that must not have a request body we fetch data
// from the query string. Otherwise we look for data in the request body.
if ('GET' === $method || 'HEAD' === $method || 'TRACE' === $method) {
if ('' === $name) {
$data = $request->query->all();
} else {
// Don't submit GET requests if the form's name does not exist
// in the request
if (!$request->query->has($name)) {
return;
}
$data = $request->query->all()[$name];
}
} else {
// Mark the form with an error if the uploaded size was too large
// This is done here and not in FormValidator because $_POST is
// empty when that error occurs. Hence the form is never submitted.
if ($this->serverParams->hasPostMaxSizeBeenExceeded()) {
// Submit the form, but don't clear the default values
$form->submit(null, false);
$form->addError(new FormError(
$form->getConfig()->getOption('upload_max_size_message')(),
null,
['{{ max }}' => $this->serverParams->getNormalizedIniPostMaxSize()]
));
return;
}
if ('' === $name) {
$params = $request->request->all();
$files = $request->files->all();
} elseif ($request->request->has($name) || $request->files->has($name)) {
$default = $form->getConfig()->getCompound() ? [] : null;
$params = $request->request->all()[$name] ?? $default;
$files = $request->files->get($name, $default);
} else {
// Don't submit the form if it is not present in the request
return;
}
if (\is_array($params) && \is_array($files)) {
$data = array_replace_recursive($params, $files);
} else {
$data = $params ?: $files;
}
}
// Don't auto-submit the form unless at least one field is present.
if ('' === $name && \count(array_intersect_key($data, $form->all())) <= 0) {
return;
}
$form->submit($data, 'PATCH' !== $method);
}
/**
* {@inheritdoc}
*/
public function isFileUpload($data)
{
return $data instanceof File;
}
/**
* @return int|null
*/
public function getUploadFileError($data)
{
if (!$data instanceof UploadedFile || $data->isValid()) {
return null;
}
return $data->getError();
}
}

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\Extension\HttpFoundation\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\RequestHandlerInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormTypeHttpFoundationExtension extends AbstractTypeExtension
{
private $requestHandler;
public function __construct(RequestHandlerInterface $requestHandler = null)
{
$this->requestHandler = $requestHandler ?? new HttpFoundationRequestHandler();
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->setRequestHandler($this->requestHandler);
}
/**
* {@inheritdoc}
*/
public static function getExtendedTypes(): iterable
{
return [FormType::class];
}
}

View File

@ -0,0 +1,36 @@
<?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\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class Form extends Constraint
{
public const NOT_SYNCHRONIZED_ERROR = '1dafa156-89e1-4736-b832-419c2e501fca';
public const NO_SUCH_FIELD_ERROR = '6e5212ed-a197-4339-99aa-5654798a4854';
protected static $errorNames = [
self::NOT_SYNCHRONIZED_ERROR => 'NOT_SYNCHRONIZED_ERROR',
self::NO_SUCH_FIELD_ERROR => 'NO_SUCH_FIELD_ERROR',
];
/**
* {@inheritdoc}
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}

View File

@ -0,0 +1,279 @@
<?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\Validator\Constraints;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Composite;
use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormValidator extends ConstraintValidator
{
/**
* @var \SplObjectStorage<FormInterface, array<int, string|string[]|GroupSequence>>
*/
private $resolvedGroups;
/**
* {@inheritdoc}
*/
public function validate($form, Constraint $formConstraint)
{
if (!$formConstraint instanceof Form) {
throw new UnexpectedTypeException($formConstraint, Form::class);
}
if (!$form instanceof FormInterface) {
return;
}
/* @var FormInterface $form */
$config = $form->getConfig();
$validator = $this->context->getValidator()->inContext($this->context);
if ($form->isSubmitted() && $form->isSynchronized()) {
// Validate the form data only if transformation succeeded
$groups = $this->getValidationGroups($form);
if (!$groups) {
return;
}
$data = $form->getData();
// Validate the data against its own constraints
$validateDataGraph = $form->isRoot()
&& (\is_object($data) || \is_array($data))
&& (($groups && \is_array($groups)) || ($groups instanceof GroupSequence && $groups->groups))
;
// Validate the data against the constraints defined in the form
/** @var Constraint[] $constraints */
$constraints = $config->getOption('constraints', []);
$hasChildren = $form->count() > 0;
if ($hasChildren && $form->isRoot()) {
$this->resolvedGroups = new \SplObjectStorage();
}
if ($groups instanceof GroupSequence) {
// Validate the data, the form AND nested fields in sequence
$violationsCount = $this->context->getViolations()->count();
foreach ($groups->groups as $group) {
if ($validateDataGraph) {
$validator->atPath('data')->validate($data, null, $group);
}
if ($groupedConstraints = self::getConstraintsInGroups($constraints, $group)) {
$validator->atPath('data')->validate($data, $groupedConstraints, $group);
}
foreach ($form->all() as $field) {
if ($field->isSubmitted()) {
// remember to validate this field in one group only
// otherwise resolving the groups would reuse the same
// sequence recursively, thus some fields could fail
// in different steps without breaking early enough
$this->resolvedGroups[$field] = (array) $group;
$fieldFormConstraint = new Form();
$fieldFormConstraint->groups = $group;
$this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath());
$validator->atPath(sprintf('children[%s]', $field->getName()))->validate($field, $fieldFormConstraint, $group);
}
}
if ($violationsCount < $this->context->getViolations()->count()) {
break;
}
}
} else {
if ($validateDataGraph) {
$validator->atPath('data')->validate($data, null, $groups);
}
$groupedConstraints = [];
foreach ($constraints as $constraint) {
// For the "Valid" constraint, validate the data in all groups
if ($constraint instanceof Valid) {
if (\is_object($data) || \is_array($data)) {
$validator->atPath('data')->validate($data, $constraint, $groups);
}
continue;
}
// Otherwise validate a constraint only once for the first
// matching group
foreach ($groups as $group) {
if (\in_array($group, $constraint->groups)) {
$groupedConstraints[$group][] = $constraint;
// Prevent duplicate validation
if (!$constraint instanceof Composite) {
continue 2;
}
}
}
}
foreach ($groupedConstraints as $group => $constraint) {
$validator->atPath('data')->validate($data, $constraint, $group);
}
foreach ($form->all() as $field) {
if ($field->isSubmitted()) {
$this->resolvedGroups[$field] = $groups;
$this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath());
$validator->atPath(sprintf('children[%s]', $field->getName()))->validate($field, $formConstraint);
}
}
}
if ($hasChildren && $form->isRoot()) {
// destroy storage to avoid memory leaks
$this->resolvedGroups = new \SplObjectStorage();
}
} elseif (!$form->isSynchronized()) {
$childrenSynchronized = true;
/** @var FormInterface $child */
foreach ($form as $child) {
if (!$child->isSynchronized()) {
$childrenSynchronized = false;
$this->context->setNode($this->context->getValue(), $child, $this->context->getMetadata(), $this->context->getPropertyPath());
$validator->atPath(sprintf('children[%s]', $child->getName()))->validate($child, $formConstraint);
}
}
// Mark the form with an error if it is not synchronized BUT all
// of its children are synchronized. If any child is not
// synchronized, an error is displayed there already and showing
// a second error in its parent form is pointless, or worse, may
// lead to duplicate errors if error bubbling is enabled on the
// child.
// See also https://github.com/symfony/symfony/issues/4359
if ($childrenSynchronized) {
$clientDataAsString = is_scalar($form->getViewData())
? (string) $form->getViewData()
: get_debug_type($form->getViewData());
$failure = $form->getTransformationFailure();
$this->context->setConstraint($formConstraint);
$this->context->buildViolation($failure->getInvalidMessage() ?? $config->getOption('invalid_message'))
->setParameters(array_replace(
['{{ value }}' => $clientDataAsString],
$config->getOption('invalid_message_parameters'),
$failure->getInvalidMessageParameters()
))
->setInvalidValue($form->getViewData())
->setCode(Form::NOT_SYNCHRONIZED_ERROR)
->setCause($failure)
->addViolation();
}
}
// Mark the form with an error if it contains extra fields
if (!$config->getOption('allow_extra_fields') && \count($form->getExtraData()) > 0) {
$this->context->setConstraint($formConstraint);
$this->context->buildViolation($config->getOption('extra_fields_message', ''))
->setParameter('{{ extra_fields }}', '"'.implode('", "', array_keys($form->getExtraData())).'"')
->setPlural(\count($form->getExtraData()))
->setInvalidValue($form->getExtraData())
->setCode(Form::NO_SUCH_FIELD_ERROR)
->addViolation();
}
}
/**
* Returns the validation groups of the given form.
*
* @return string|GroupSequence|array<string|GroupSequence>
*/
private function getValidationGroups(FormInterface $form)
{
// Determine the clicked button of the complete form tree
$clickedButton = null;
if (method_exists($form, 'getClickedButton')) {
$clickedButton = $form->getClickedButton();
}
if (null !== $clickedButton) {
$groups = $clickedButton->getConfig()->getOption('validation_groups');
if (null !== $groups) {
return self::resolveValidationGroups($groups, $form);
}
}
do {
$groups = $form->getConfig()->getOption('validation_groups');
if (null !== $groups) {
return self::resolveValidationGroups($groups, $form);
}
if (isset($this->resolvedGroups[$form])) {
return $this->resolvedGroups[$form];
}
$form = $form->getParent();
} while (null !== $form);
return [Constraint::DEFAULT_GROUP];
}
/**
* Post-processes the validation groups option for a given form.
*
* @param string|GroupSequence|array<string|GroupSequence>|callable $groups The validation groups
*
* @return GroupSequence|array<string|GroupSequence>
*/
private static function resolveValidationGroups($groups, FormInterface $form)
{
if (!\is_string($groups) && \is_callable($groups)) {
$groups = $groups($form);
}
if ($groups instanceof GroupSequence) {
return $groups;
}
return (array) $groups;
}
private static function getConstraintsInGroups($constraints, $group)
{
$groups = (array) $group;
return array_filter($constraints, static function (Constraint $constraint) use ($groups) {
foreach ($groups as $group) {
if (\in_array($group, $constraint->groups, true)) {
return true;
}
}
return false;
});
}
}

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\Validator\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ValidationListener implements EventSubscriberInterface
{
private $validator;
private $violationMapper;
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [FormEvents::POST_SUBMIT => 'validateForm'];
}
public function __construct(ValidatorInterface $validator, ViolationMapperInterface $violationMapper)
{
$this->validator = $validator;
$this->violationMapper = $violationMapper;
}
public function validateForm(FormEvent $event)
{
$form = $event->getForm();
if ($form->isRoot()) {
// Form groups are validated internally (FormValidator). Here we don't set groups as they are retrieved into the validator.
foreach ($this->validator->validate($form) as $violation) {
// Allow the "invalid" constraint to be put onto
// non-synchronized forms
$allowNonSynchronized = $violation->getConstraint() instanceof Form && Form::NOT_SYNCHRONIZED_ERROR === $violation->getCode();
$this->violationMapper->mapViolation($violation, $form, $allowNonSynchronized);
}
}
}
}

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\Validator\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\GroupSequence;
/**
* Encapsulates common logic of {@link FormTypeValidatorExtension} and
* {@link SubmitTypeValidatorExtension}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class BaseValidatorExtension extends AbstractTypeExtension
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
// Make sure that validation groups end up as null, closure or array
$validationGroupsNormalizer = function (Options $options, $groups) {
if (false === $groups) {
return [];
}
if (empty($groups)) {
return null;
}
if (\is_callable($groups)) {
return $groups;
}
if ($groups instanceof GroupSequence) {
return $groups;
}
return (array) $groups;
};
$resolver->setDefaults([
'validation_groups' => null,
]);
$resolver->setNormalizer('validation_groups', $validationGroupsNormalizer);
}
}

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\Validator\Type;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormRendererInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormTypeValidatorExtension extends BaseValidatorExtension
{
private $validator;
private $violationMapper;
private $legacyErrorMessages;
public function __construct(ValidatorInterface $validator, bool $legacyErrorMessages = true, FormRendererInterface $formRenderer = null, TranslatorInterface $translator = null)
{
$this->validator = $validator;
$this->violationMapper = new ViolationMapper($formRenderer, $translator);
$this->legacyErrorMessages = $legacyErrorMessages;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventSubscriber(new ValidationListener($this->validator, $this->violationMapper));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
// Constraint should always be converted to an array
$constraintsNormalizer = function (Options $options, $constraints) {
return \is_object($constraints) ? [$constraints] : (array) $constraints;
};
$resolver->setDefaults([
'error_mapping' => [],
'constraints' => [],
'invalid_message' => 'This value is not valid.',
'invalid_message_parameters' => [],
'legacy_error_messages' => $this->legacyErrorMessages,
'allow_extra_fields' => false,
'extra_fields_message' => 'This form should not contain extra fields.',
]);
$resolver->setAllowedTypes('constraints', [Constraint::class, Constraint::class.'[]']);
$resolver->setAllowedTypes('legacy_error_messages', 'bool');
$resolver->setDeprecated('legacy_error_messages', 'symfony/form', '5.2', function (Options $options, $value) {
if (true === $value) {
return 'Setting the "legacy_error_messages" option to "true" is deprecated. It will be disabled in Symfony 6.0.';
}
return '';
});
$resolver->setNormalizer('constraints', $constraintsNormalizer);
}
/**
* {@inheritdoc}
*/
public static function getExtendedTypes(): iterable
{
return [FormType::class];
}
}

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