Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

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;
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\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* Represents the main form extension, which loads the core functionality.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CoreExtension extends AbstractExtension
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* @var ChoiceListFactoryInterface
*/
private $choiceListFactory;
public function __construct(PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
}
protected function loadTypes()
{
return array(
new Type\FormType($this->propertyAccessor),
new Type\BirthdayType(),
new Type\CheckboxType(),
new Type\ChoiceType($this->choiceListFactory),
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(),
new Type\ButtonType(),
new Type\SubmitType(),
new Type\ResetType(),
new Type\CurrencyType(),
);
}
}

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\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, $checkboxes)
{
if (null === $choices) {
$choices = array();
}
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($checkboxes, &$choices)
{
if (!is_array($choices)) {
throw new UnexpectedTypeException($choices, 'array');
}
$values = array();
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,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\DataMapper;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* Maps arrays/objects to/from forms using property paths.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathMapper implements DataMapperInterface
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* Creates a new property path mapper.
*
* @param PropertyAccessorInterface $propertyAccessor The property accessor
*/
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
/**
* {@inheritdoc}
*/
public function mapDataToForms($data, $forms)
{
$empty = null === $data || array() === $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->propertyAccessor->getValue($data, $propertyPath));
} else {
$form->setData($form->getConfig()->getData());
}
}
}
/**
* {@inheritdoc}
*/
public function mapFormsToData($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()) {
// If the field is of type DateTime and the data is the same skip the update to
// keep the original object hash
if ($form->getData() instanceof \DateTime && $form->getData() == $this->propertyAccessor->getValue($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() || $form->getData() !== $this->propertyAccessor->getValue($data, $propertyPath)) {
$this->propertyAccessor->setValue($data, $propertyPath, $form->getData());
}
}
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\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, $radios)
{
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($radios, &$choice)
{
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,86 @@
<?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 = array();
}
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
$result = array();
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 = array();
$emptyKeys = array();
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;
}
throw new TransformationFailedException(
sprintf('The keys "%s" should not be empty', implode('", "', $emptyKeys)
));
}
return $result;
}
}

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\InvalidArgumentException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
abstract class BaseDateTimeTransformer implements DataTransformerInterface
{
protected static $formats = array(
\IntlDateFormatter::NONE,
\IntlDateFormatter::FULL,
\IntlDateFormatter::LONG,
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::SHORT,
);
protected $inputTimezone;
protected $outputTimezone;
/**
* Constructor.
*
* @param string $inputTimezone The name of the input timezone
* @param string $outputTimezone The name of the output timezone
*
* @throws UnexpectedTypeException if a timezone is not a string
* @throws InvalidArgumentException if a timezone is not valid
*/
public function __construct($inputTimezone = null, $outputTimezone = null)
{
if (null !== $inputTimezone && !is_string($inputTimezone)) {
throw new UnexpectedTypeException($inputTimezone, 'string');
}
if (null !== $outputTimezone && !is_string($outputTimezone)) {
throw new UnexpectedTypeException($outputTimezone, 'string');
}
$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\TransformationFailedException;
/**
* Transforms between a Boolean and a string.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class BooleanToStringTransformer implements DataTransformerInterface
{
/**
* The value emitted upon transform if the input is true.
*
* @var string
*/
private $trueValue;
/**
* Sets the value emitted upon transform if the input is true.
*
* @param string $trueValue
*/
public function __construct($trueValue)
{
$this->trueValue = $trueValue;
}
/**
* Transforms a Boolean into a string.
*
* @param bool $value Boolean value
*
* @return string String value
*
* @throws TransformationFailedException If the given value is not a Boolean.
*/
public function transform($value)
{
if (null === $value) {
return;
}
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 Boolean value
*
* @throws TransformationFailedException If the given value is not a string.
*/
public function reverseTransform($value)
{
if (null === $value) {
return false;
}
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
return true;
}
}

View File

@@ -0,0 +1,58 @@
<?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\ChoiceList\ChoiceListInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceToValueTransformer implements DataTransformerInterface
{
private $choiceList;
/**
* Constructor.
*
* @param ChoiceListInterface $choiceList
*/
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
public function transform($choice)
{
return (string) current($this->choiceList->getValuesForChoices(array($choice)));
}
public function reverseTransform($value)
{
if (null !== $value && !is_string($value)) {
throw new TransformationFailedException('Expected a string or null.');
}
$choices = $this->choiceList->getChoicesForValues(array((string) $value));
if (1 !== count($choices)) {
if (null === $value || '' === $value) {
return;
}
throw new TransformationFailedException(sprintf('The choice "%s" does not exist or is not unique', $value));
}
return current($choices);
}
}

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\Exception\TransformationFailedException;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoicesToValuesTransformer implements DataTransformerInterface
{
private $choiceList;
/**
* Constructor.
*
* @param ChoiceListInterface $choiceList
*/
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
/**
* @param array $array
*
* @return array
*
* @throws TransformationFailedException If the given value is not an array.
*/
public function transform($array)
{
if (null === $array) {
return array();
}
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
return $this->choiceList->getValuesForChoices($array);
}
/**
* @param array $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 array();
}
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,95 @@
<?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
{
/**
* The value transformers.
*
* @var DataTransformerInterface[]
*/
protected $transformers;
/**
* Uses the given value transformers to transform values.
*
* @param array $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 The transformed value
*
* @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 The reverse-transformed value
*
* @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,174 @@
<?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
{
const YEARS = 'years';
const MONTHS = 'months';
const DAYS = 'days';
const HOURS = 'hours';
const MINUTES = 'minutes';
const SECONDS = 'seconds';
const INVERT = 'invert';
private static $availableFields = array(
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[] $fields The date fields
* @param bool $pad Whether to use padding
*/
public function __construct(array $fields = null, $pad = false)
{
if (null === $fields) {
$fields = array('years', 'months', 'days', 'hours', 'minutes', 'seconds', 'invert');
}
$this->fields = $fields;
$this->pad = (bool) $pad;
}
/**
* Transforms a normalized date interval into an interval array.
*
* @param \DateInterval $dateInterval Normalized date interval
*
* @return array Interval array
*
* @throws UnexpectedTypeException If the given value is not a \DateInterval instance.
*/
public function transform($dateInterval)
{
if (null === $dateInterval) {
return array_intersect_key(
array(
'years' => '',
'months' => '',
'weeks' => '',
'days' => '',
'hours' => '',
'minutes' => '',
'seconds' => '',
'invert' => false,
),
array_flip($this->fields)
);
}
if (!$dateInterval instanceof \DateInterval) {
throw new UnexpectedTypeException($dateInterval, '\DateInterval');
}
$result = array();
foreach (self::$availableFields 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 Normalized date interval
*
* @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;
}
if (!is_array($value)) {
throw new UnexpectedTypeException($value, 'array');
}
if ('' === implode('', $value)) {
return;
}
$emptyFields = array();
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::$availableFields as $field => $char) {
if ($field !== 'invert' && 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,104 @@
<?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;
private $parseSigned;
/**
* Transforms a \DateInterval instance to a string.
*
* @see \DateInterval::format() for supported formats
*
* @param string $format The date format
* @param bool $parseSigned Whether to parse as a signed interval
*/
public function __construct($format = 'P%yY%mM%dDT%hH%iM%sS', $parseSigned = false)
{
$this->format = $format;
$this->parseSigned = $parseSigned;
}
/**
* Transforms a DateInterval object into a date string with the configured format.
*
* @param \DateInterval $value A DateInterval object
*
* @return string An ISO 8601 or relative date string like date interval presentation
*
* @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');
}
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 An instance of \DateInterval
*
* @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;
}
if (!is_string($value)) {
throw new UnexpectedTypeException($value, 'string');
}
if ('' === $value) {
return;
}
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)
{
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,194 @@
<?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/array.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
*/
class DateTimeToArrayTransformer extends BaseDateTimeTransformer
{
private $pad;
private $fields;
/**
* Constructor.
*
* @param string $inputTimezone The input timezone
* @param string $outputTimezone The output timezone
* @param array $fields The date fields
* @param bool $pad Whether to use padding
*
* @throws UnexpectedTypeException if a timezone is not a string
*/
public function __construct($inputTimezone = null, $outputTimezone = null, array $fields = null, $pad = false)
{
parent::__construct($inputTimezone, $outputTimezone);
if (null === $fields) {
$fields = array('year', 'month', 'day', 'hour', 'minute', 'second');
}
$this->fields = $fields;
$this->pad = (bool) $pad;
}
/**
* Transforms a normalized date into a localized date.
*
* @param \DateTimeInterface $dateTime A DateTimeInterface object
*
* @return array Localized date
*
* @throws TransformationFailedException If the given value is not a \DateTimeInterface
*/
public function transform($dateTime)
{
if (null === $dateTime) {
return array_intersect_key(array(
'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(array(
'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 Normalized date
*
* @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;
}
if (!is_array($value)) {
throw new TransformationFailedException('Expected an array.');
}
if ('' === implode('', $value)) {
return;
}
$emptyFields = array();
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']) ? '1970' : $value['year'],
empty($value['month']) ? '1' : $value['month'],
empty($value['day']) ? '1' : $value['day'],
empty($value['hour']) ? '0' : $value['hour'],
empty($value['minute']) ? '0' : $value['minute'],
empty($value['second']) ? '0' : $value['second']
),
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,201 @@
<?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;
/**
* Constructor.
*
* @see BaseDateTimeTransformer::formats for available format options
*
* @param string $inputTimezone The name of the input timezone
* @param string $outputTimezone The name of the output timezone
* @param int $dateFormat The date format
* @param int $timeFormat The time format
* @param int $calendar One of the \IntlDateFormatter calendar constants
* @param string $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($inputTimezone = null, $outputTimezone = null, $dateFormat = null, $timeFormat = null, $calendar = \IntlDateFormatter::GREGORIAN, $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|array Localized date string/array
*
* @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 (intl_get_error_code() != 0) {
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 Normalized date
*
* @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;
}
// 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();
$timestamp = $this->getIntlDateFormatter($dateOnly)->parse($value);
if (intl_get_error_code() != 0) {
throw new TransformationFailedException(intl_get_error_message());
}
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 http://php.net/manual/en/datetime.construct.php
$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 can not be constructed.
*/
protected function getIntlDateFormatter($ignoreTimezone = false)
{
$dateFormat = $this->dateFormat;
$timeFormat = $this->timeFormat;
$timezone = $ignoreTimezone ? 'UTC' : $this->outputTimezone;
if (class_exists('IntlTimeZone', false)) {
// see https://bugs.php.net/bug.php?id=66323
$timezone = \IntlTimeZone::createTimeZone($timezone);
}
$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/bug.php?id=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,94 @@
<?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 The formatted date
*
* @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 Normalized date
*
* @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;
}
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 (preg_match('/(\d{4})-(\d{2})-(\d{2})/', $rfc3339, $matches)) {
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,146 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\DataTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* 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 $inputTimezone The name of the input timezone
* @param string $outputTimezone The name of the output timezone
* @param string $format The date format
*
* @throws UnexpectedTypeException if a timezone is not a string
*/
public function __construct($inputTimezone = null, $outputTimezone = null, $format = 'Y-m-d H:i:s')
{
parent::__construct($inputTimezone, $outputTimezone);
$this->generateFormat = $this->parseFormat = $format;
// See http://php.net/manual/en/datetime.createfromformat.php
// 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 (false === strpos($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 A value as produced by PHP's date() function
*
* @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 An instance of \DateTime
*
* @throws TransformationFailedException If the given value is not a string,
* or could not be transformed
*/
public function reverseTransform($value)
{
if (empty($value)) {
return;
}
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 A timestamp
*
* @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.');
}
return $dateTime->getTimestamp();
}
/**
* Transforms a timestamp in the configured timezone into a DateTime object.
*
* @param string $value A timestamp
*
* @return \DateTime A \DateTime object
*
* @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;
}
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,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\Core\DataTransformer;
/**
* 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 int $scale Unused
* @param bool $grouping Whether thousands should be grouped
* @param int $roundingMode One of the ROUND_ constants in this class
*/
public function __construct($scale = 0, $grouping = false, $roundingMode = self::ROUND_DOWN)
{
if (null === $roundingMode) {
$roundingMode = self::ROUND_DOWN;
}
parent::__construct(0, $grouping, $roundingMode);
}
/**
* {@inheritdoc}
*/
public function reverseTransform($value)
{
$result = parent::reverseTransform($value);
return null !== $result ? (int) $result : null;
}
}

View File

@@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\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($scale = 2, $grouping = true, $roundingMode = self::ROUND_HALF_UP, $divisor = 1)
{
if (null === $grouping) {
$grouping = true;
}
if (null === $scale) {
$scale = 2;
}
parent::__construct($scale, $grouping, $roundingMode);
if (null === $divisor) {
$divisor = 1;
}
$this->divisor = $divisor;
}
/**
* Transforms a normalized format into a localized money string.
*
* @param int|float $value Normalized number
*
* @return string Localized money string
*
* @throws TransformationFailedException If the given value is not numeric or
* if the value can not be transformed.
*/
public function transform($value)
{
if (null !== $value) {
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 Normalized number
*
* @throws TransformationFailedException If the given value is not a string
* or if the value can not be transformed.
*/
public function reverseTransform($value)
{
$value = parent::reverseTransform($value);
if (null !== $value) {
$value *= $this->divisor;
}
return $value;
}
}

View File

@@ -0,0 +1,277 @@
<?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
{
/**
* Rounds a number towards positive infinity.
*
* Rounds 1.4 to 2 and -1.4 to -1.
*/
const ROUND_CEILING = \NumberFormatter::ROUND_CEILING;
/**
* Rounds a number towards negative infinity.
*
* Rounds 1.4 to 1 and -1.4 to -2.
*/
const ROUND_FLOOR = \NumberFormatter::ROUND_FLOOR;
/**
* Rounds a number away from zero.
*
* Rounds 1.4 to 2 and -1.4 to -2.
*/
const ROUND_UP = \NumberFormatter::ROUND_UP;
/**
* Rounds a number towards zero.
*
* Rounds 1.4 to 1 and -1.4 to -1.
*/
const ROUND_DOWN = \NumberFormatter::ROUND_DOWN;
/**
* Rounds to the nearest number and halves to the next even number.
*
* Rounds 2.5, 1.6 and 1.5 to 2 and 1.4 to 1.
*/
const ROUND_HALF_EVEN = \NumberFormatter::ROUND_HALFEVEN;
/**
* Rounds to the nearest number and halves away from zero.
*
* Rounds 2.5 to 3, 1.6 and 1.5 to 2 and 1.4 to 1.
*/
const ROUND_HALF_UP = \NumberFormatter::ROUND_HALFUP;
/**
* Rounds to the nearest number and halves towards zero.
*
* Rounds 2.5 and 1.6 to 2, 1.5 and 1.4 to 1.
*/
const ROUND_HALF_DOWN = \NumberFormatter::ROUND_HALFDOWN;
protected $grouping;
protected $roundingMode;
private $scale;
public function __construct($scale = null, $grouping = false, $roundingMode = self::ROUND_HALF_UP)
{
if (null === $grouping) {
$grouping = false;
}
if (null === $roundingMode) {
$roundingMode = self::ROUND_HALF_UP;
}
$this->scale = $scale;
$this->grouping = $grouping;
$this->roundingMode = $roundingMode;
}
/**
* Transforms a number type into localized number.
*
* @param int|float $value Number value
*
* @return string Localized value
*
* @throws TransformationFailedException If the given value is not numeric
* or if the value can not 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 fixed spaces to normal ones
$value = str_replace("\xc2\xa0", ' ', $value);
return $value;
}
/**
* Transforms a localized number into an integer or float.
*
* @param string $value The localized value
*
* @return int|float The numeric value
*
* @throws TransformationFailedException If the given value is not a string
* or if the value can not be transformed.
*/
public function reverseTransform($value)
{
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
if ('' === $value) {
return;
}
if ('NaN' === $value) {
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 (false !== strpos($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');
}
if (is_int($result) && $result === (int) $float = (float) $result) {
$result = $float;
}
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(\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;
}
/**
* Rounds a number according to the configured scale and rounding mode.
*
* @param int|float $number A number
*
* @return int|float The rounded number
*/
private function round($number)
{
if (null !== $this->scale && null !== $this->roundingMode) {
// shift number to maintain the correct scale during rounding
$roundingCoef = pow(10, $this->scale);
// string representation to avoid rounding errors, similar to bcmul()
$number = (string) ($number * $roundingCoef);
switch ($this->roundingMode) {
case self::ROUND_CEILING:
$number = ceil($number);
break;
case self::ROUND_FLOOR:
$number = floor($number);
break;
case self::ROUND_UP:
$number = $number > 0 ? ceil($number) : floor($number);
break;
case self::ROUND_DOWN:
$number = $number > 0 ? floor($number) : ceil($number);
break;
case self::ROUND_HALF_EVEN:
$number = round($number, 0, PHP_ROUND_HALF_EVEN);
break;
case self::ROUND_HALF_UP:
$number = round($number, 0, PHP_ROUND_HALF_UP);
break;
case self::ROUND_HALF_DOWN:
$number = round($number, 0, PHP_ROUND_HALF_DOWN);
break;
}
$number /= $roundingCoef;
}
return $number;
}
}

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\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
{
const FRACTIONAL = 'fractional';
const INTEGER = 'integer';
protected static $types = array(
self::FRACTIONAL,
self::INTEGER,
);
private $type;
private $scale;
/**
* Constructor.
*
* @see self::$types for a list of supported types
*
* @param int $scale The scale
* @param string $type One of the supported types
*
* @throws UnexpectedTypeException if the given value of type is unknown
*/
public function __construct($scale = null, $type = null)
{
if (null === $scale) {
$scale = 0;
}
if (null === $type) {
$type = self::FRACTIONAL;
}
if (!in_array($type, self::$types, true)) {
throw new UnexpectedTypeException($type, implode('", "', self::$types));
}
$this->type = $type;
$this->scale = $scale;
}
/**
* Transforms between a normalized format (integer or float) into a percentage value.
*
* @param int|float $value Normalized value
*
* @return string Percentage value
*
* @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 Normalized value
*
* @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;
}
$formatter = $this->getNumberFormatter();
// replace normal spaces so that the formatter can read them
$value = $formatter->parse(str_replace(' ', "\xc2\xa0", $value));
if (intl_is_failure($formatter->getErrorCode())) {
throw new TransformationFailedException($formatter->getErrorMessage());
}
if (self::FRACTIONAL == $this->type) {
$value /= 100;
}
return $value;
}
/**
* Returns a preconfigured \NumberFormatter instance.
*
* @return \NumberFormatter
*/
protected function getNumberFormatter()
{
$formatter = new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::DECIMAL);
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
return $formatter;
}
}

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\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 The array
*/
public function transform($value)
{
$result = array();
foreach ($this->keys as $key) {
$result[$key] = $value;
}
return $result;
}
/**
* Extracts the duplicated value from an array.
*
* @param array $array
*
* @return mixed The value
*
* @throws TransformationFailedException If the given value is not an array or
* if the given array can not be transformed.
*/
public function reverseTransform($array)
{
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
$result = current($array);
$emptyKeys = array();
foreach ($this->keys as $key) {
if (isset($array[$key]) && '' !== $array[$key] && false !== $array[$key] && array() !== $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;
}
throw new TransformationFailedException(
sprintf('The keys "%s" should not be empty', implode('", "', $emptyKeys)
));
}
return $result;
}
}

View File

@@ -0,0 +1,50 @@
<?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\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* 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;
/**
* Constructor.
*
* @param string|null $defaultProtocol The URL scheme to add when there is none or null to not modify the data
*/
public function __construct($defaultProtocol = 'http')
{
$this->defaultProtocol = $defaultProtocol;
}
public function onSubmit(FormEvent $event)
{
$data = $event->getData();
if ($this->defaultProtocol && $data && !preg_match('~^[\w+.-]+://~', $data)) {
$event->setData($this->defaultProtocol.'://'.$data);
}
}
public static function getSubscribedEvents()
{
return array(FormEvents::SUBMIT => 'onSubmit');
}
}

View File

@@ -0,0 +1,128 @@
<?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\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class MergeCollectionListener implements EventSubscriberInterface
{
/**
* Whether elements may be added to the collection.
*
* @var bool
*/
private $allowAdd;
/**
* Whether elements may be removed from the collection.
*
* @var bool
*/
private $allowDelete;
/**
* Creates a new listener.
*
* @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($allowAdd = false, $allowDelete = false)
{
$this->allowAdd = $allowAdd;
$this->allowDelete = $allowDelete;
}
public static function getSubscribedEvents()
{
return array(
FormEvents::SUBMIT => 'onSubmit',
);
}
public function onSubmit(FormEvent $event)
{
$dataToMergeInto = $event->getForm()->getNormData();
$data = $event->getData();
if (null === $data) {
$data = array();
}
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 (!$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 = array();
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,183 @@
<?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\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Resize a collection form element based on the data sent from the client.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ResizeFormListener implements EventSubscriberInterface
{
/**
* @var string
*/
protected $type;
/**
* @var array
*/
protected $options;
/**
* Whether children could be added to the group.
*
* @var bool
*/
protected $allowAdd;
/**
* Whether children could be removed from the group.
*
* @var bool
*/
protected $allowDelete;
/**
* @var bool
*/
private $deleteEmpty;
public function __construct($type, array $options = array(), $allowAdd = false, $allowDelete = false, $deleteEmpty = false)
{
$this->type = $type;
$this->allowAdd = $allowAdd;
$this->allowDelete = $allowDelete;
$this->options = $options;
$this->deleteEmpty = $deleteEmpty;
}
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit',
// (MergeCollectionListener, MergeDoctrineCollectionListener)
FormEvents::SUBMIT => array('onSubmit', 50),
);
}
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
if (null === $data) {
$data = array();
}
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(array(
'property_path' => '['.$name.']',
), $this->options));
}
}
public function preSubmit(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
if ($data instanceof \Traversable && $data instanceof \ArrayAccess) {
@trigger_error('Support for objects implementing both \Traversable and \ArrayAccess is deprecated since version 3.1 and will be removed in 4.0. Use an array instead.', E_USER_DEPRECATED);
}
if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
$data = array();
}
// 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(array(
'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 = array();
}
if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
}
if ($this->deleteEmpty) {
$previousData = $event->getForm()->getData();
foreach ($form as $name => $child) {
$isNew = !isset($previousData[$name]);
// $isNew can only be true if allowAdd is true, so we don't
// need to check allowAdd again
if ($child->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 = array();
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,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\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
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 array(FormEvents::PRE_SUBMIT => 'preSubmit');
}
}

View File

@@ -0,0 +1,124 @@
<?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\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'];
$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'];
}
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.
// http://www.w3.org/TR/html401/struct/global.html#adef-id
$id = ltrim($id, '_0123456789');
}
$blockPrefixes = array();
for ($type = $form->getConfig()->getType(); null !== $type; $type = $type->getParent()) {
array_unshift($blockPrefixes, $type->getBlockPrefix());
}
$blockPrefixes[] = $uniqueBlockPrefix;
$view->vars = array_replace($view->vars, array(
'form' => $view,
'id' => $id,
'name' => $name,
'full_name' => $fullName,
'disabled' => $form->isDisabled(),
'label' => $options['label'],
'label_format' => $labelFormat,
'multipart' => false,
'attr' => $options['attr'],
'block_prefixes' => $blockPrefixes,
'unique_block_prefix' => $uniqueBlockPrefix,
'translation_domain' => $translationDomain,
// 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
'cache_key' => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(),
));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'block_name' => null,
'disabled' => false,
'label' => null,
'label_format' => null,
'attr' => array(),
'translation_domain' => null,
'auto_initialize' => true,
));
$resolver->setAllowedTypes('attr', 'array');
}
}

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\OptionsResolver\OptionsResolver;
class BirthdayType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('years', range(date('Y') - 120, date('Y')));
$resolver->setAllowedTypes('years', 'array');
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\DateType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'birthday';
}
}

View File

@@ -0,0 +1,50 @@
<?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()
{
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'button';
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults(array(
'auto_initialize' => false,
));
}
}

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\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\BooleanToStringTransformer;
use Symfony\Component\Form\FormView;
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(isset($options['data']) ? $options['data'] : false);
$builder->addViewTransformer(new BooleanToStringTransformer($options['value']));
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, array(
'value' => $options['value'],
'checked' => null !== $form->getViewData(),
));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$emptyData = function (FormInterface $form, $viewData) {
return $viewData;
};
$resolver->setDefaults(array(
'value' => '1',
'empty_data' => $emptyData,
'compound' => false,
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'checkbox';
}
}

View File

@@ -0,0 +1,450 @@
<?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\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
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\RadioListMapper;
use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper;
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\Extension\Core\EventListener\MergeCollectionListener;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ChoiceType extends AbstractType
{
/**
* Caches created choice lists.
*
* @var ChoiceListFactoryInterface
*/
private $choiceListFactory;
public function __construct(ChoiceListFactoryInterface $choiceListFactory = null)
{
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(
new PropertyAccessDecorator(
new DefaultChoiceListFactory()
)
);
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$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(array('')))) {
$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);
// 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) {
$form = $event->getForm();
$data = $event->getData();
if (null === $data) {
$emptyData = $form->getConfig()->getEmptyData();
if (false === FormUtil::isEmpty($emptyData) && array() !== $emptyData) {
$data = is_callable($emptyData) ? call_user_func($emptyData, $form, $data) : $emptyData;
}
}
// Convert the submitted data to a string, if scalar, before
// casting it to an array
if (!is_array($data)) {
$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
$data = array();
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])) {
$data[$child->getName()] = $value;
unset($unknownValues[$value]);
continue;
}
}
// 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
if (count($unknownValues) > 0) {
throw new TransformationFailedException(sprintf(
'The choices "%s" do not exist in the choice list.',
implode('", "', array_keys($unknownValues))
));
}
$event->setData($data);
});
}
if ($options['multiple']) {
// <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, array(
'multiple' => $options['multiple'],
'expanded' => $options['expanded'],
'preferred_choices' => $choiceListView->preferredChoices,
'choices' => $choiceListView->choices,
'separator' => '-------------------',
'placeholder' => null,
'choice_translation_domain' => $choiceTranslationDomain,
));
// 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;
}
if ($options['multiple']) {
return array();
}
return '';
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$choicesAsValuesNormalizer = function (Options $options, $choicesAsValues) {
// Not set by the user
if (null === $choicesAsValues) {
return true;
}
// Set by the user
if (true !== $choicesAsValues) {
throw new \RuntimeException(sprintf('The "choices_as_values" option of the %s should not be used. Remove it and flip the contents of the "choices" option instead.', get_class($this)));
}
@trigger_error('The "choices_as_values" option is deprecated since version 3.1 and will be removed in 4.0. You should not use it anymore.', E_USER_DEPRECATED);
return true;
};
$placeholderNormalizer = function (Options $options, $placeholder) {
if ($options['multiple']) {
// never use an empty value for this case
return;
} 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;
} elseif (false === $placeholder) {
// an empty value should be added but the user decided otherwise
return;
} 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(array(
'multiple' => false,
'expanded' => false,
'choices' => array(),
'choices_as_values' => null, // deprecated since 3.1
'choice_loader' => null,
'choice_label' => null,
'choice_name' => null,
'choice_value' => null,
'choice_attr' => null,
'preferred_choices' => array(),
'group_by' => null,
'empty_data' => $emptyData,
'placeholder' => $placeholderDefault,
'error_bubbling' => false,
'compound' => $compound,
// The view data is always a string, 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,
));
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setNormalizer('choices_as_values', $choicesAsValuesNormalizer);
$resolver->setAllowedTypes('choices', array('null', 'array', '\Traversable'));
$resolver->setAllowedTypes('choice_translation_domain', array('null', 'bool', 'string'));
$resolver->setAllowedTypes('choice_loader', array('null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'));
$resolver->setAllowedTypes('choice_label', array('null', 'bool', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_name', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_value', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_attr', array('null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('preferred_choices', array('array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('group_by', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'choice';
}
/**
* Adds the sub fields for an expanded choice field.
*
* @param FormBuilderInterface $builder The form builder
* @param array $choiceViews The choice view objects
* @param array $options The build options
*/
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);
}
}
/**
* @param FormBuilderInterface $builder
* @param $name
* @param $choiceView
* @param array $options
*
* @return mixed
*/
private function addSubForm(FormBuilderInterface $builder, $name, ChoiceView $choiceView, array $options)
{
$choiceOpts = array(
'value' => $choiceView->value,
'label' => $choiceView->label,
'attr' => $choiceView->attr,
'translation_domain' => $options['translation_domain'],
'block_name' => 'entry',
);
if ($options['multiple']) {
$choiceType = __NAMESPACE__.'\CheckboxType';
// The user can check 0 or more checkboxes. If required
// is true, he is required to check all of them.
$choiceOpts['required'] = false;
} else {
$choiceType = __NAMESPACE__.'\RadioType';
}
$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']
);
}
// Harden against NULL values (like in EntityType and ModelType)
$choices = null !== $options['choices'] ? $options['choices'] : array();
return $this->choiceListFactory->createListFromChoices($choices, $options['choice_value']);
}
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']
);
}
}

View File

@@ -0,0 +1,112 @@
<?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\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
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(array(
'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, array(
'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)
{
if ($form->getConfig()->hasAttribute('prototype') && $view->vars['prototype']->vars['multipart']) {
$view->vars['multipart'] = true;
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$entryOptionsNormalizer = function (Options $options, $value) {
$value['block_name'] = 'entry';
return $value;
};
$resolver->setDefaults(array(
'allow_add' => false,
'allow_delete' => false,
'prototype' => true,
'prototype_data' => null,
'prototype_name' => '__name__',
'entry_type' => __NAMESPACE__.'\TextType',
'entry_options' => array(),
'delete_empty' => false,
));
$resolver->setNormalizer('entry_options', $entryOptionsNormalizer);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'collection';
}
}

View File

@@ -0,0 +1,118 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Intl\Intl;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CountryType extends AbstractType implements ChoiceLoaderInterface
{
/**
* Country loaded choice list.
*
* The choices are lazy loaded and generated from the Intl component.
*
* {@link \Symfony\Component\Intl\Intl::getRegionBundle()}.
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_loader' => function (Options $options) {
if ($options['choices']) {
@trigger_error(sprintf('Using the "choices" option in %s has been deprecated since version 3.3 and will be ignored in 4.0. Override the "choice_loader" option instead or set it to null.', __CLASS__), E_USER_DEPRECATED);
return null;
}
return $this;
},
'choice_translation_domain' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ChoiceType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'country';
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
return $this->choiceList = new ArrayChoiceList(array_flip(Intl::getRegionBundle()->getCountryNames()), $value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
$values = array_filter($values);
if (empty($values)) {
return array();
}
// If no callable is set, values are the same as choices
if (null === $value) {
return $values;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Optimize
$choices = array_filter($choices);
if (empty($choices)) {
return array();
}
// If no callable is set, choices are the same as values
if (null === $value) {
return $choices;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
}

View File

@@ -0,0 +1,118 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Intl\Intl;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CurrencyType extends AbstractType implements ChoiceLoaderInterface
{
/**
* Currency loaded choice list.
*
* The choices are lazy loaded and generated from the Intl component.
*
* {@link \Symfony\Component\Intl\Intl::getCurrencyBundle()}.
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_loader' => function (Options $options) {
if ($options['choices']) {
@trigger_error(sprintf('Using the "choices" option in %s has been deprecated since version 3.3 and will be ignored in 4.0. Override the "choice_loader" option instead or set it to null.', __CLASS__), E_USER_DEPRECATED);
return null;
}
return $this;
},
'choice_translation_domain' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ChoiceType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'currency';
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
return $this->choiceList = new ArrayChoiceList(array_flip(Intl::getCurrencyBundle()->getCurrencyNames()), $value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
$values = array_filter($values);
if (empty($values)) {
return array();
}
// If no callable is set, values are the same as choices
if (null === $value) {
return $values;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Optimize
$choices = array_filter($choices);
if (empty($choices)) {
return array();
}
// If no callable is set, choices are the same as values
if (null === $value) {
return $choices;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
}

View File

@@ -0,0 +1,293 @@
<?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 $timeParts = array(
'years',
'months',
'weeks',
'days',
'hours',
'minutes',
'seconds',
);
private static $widgets = array(
'text' => 'Symfony\Component\Form\Extension\Core\Type\TextType',
'integer' => 'Symfony\Component\Form\Extension\Core\Type\IntegerType',
'choice' => 'Symfony\Component\Form\Extension\Core\Type\ChoiceType',
);
/**
* {@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 can not enable weeks and days fields together.');
}
$format = 'P';
$parts = array();
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 {
$childOptions = array();
foreach ($this->timeParts as $part) {
if ($options['with_'.$part]) {
$childOptions[$part] = array(
'error_bubbling' => true,
'label' => $options['labels'][$part],
);
if ('choice' === $options['widget']) {
$childOptions[$part]['choice_translation_domain'] = false;
$childOptions[$part]['choices'] = $options[$part];
$childOptions[$part]['placeholder'] = $options['placeholder'][$part];
}
}
}
// Append generic carry-along options
foreach (array('required', 'translation_domain') as $passOpt) {
foreach ($this->timeParts as $part) {
if ($options['with_'.$part]) {
$childOptions[$part][$passOpt] = $options[$passOpt];
}
}
}
foreach ($this->timeParts as $part) {
if ($options['with_'.$part]) {
$childForm = $builder->create($part, self::$widgets[$options['widget']], $childOptions[$part]);
if ('integer' === $options['widget']) {
$childForm->addModelTransformer(
new ReversedTransformer(
new IntegerToLocalizedStringTransformer()
)
);
}
$builder->add($childForm);
}
}
if ($options['with_invert']) {
$builder->add('invert', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', array(
'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 = array(
'widget' => $options['widget'],
'with_invert' => $options['with_invert'],
);
foreach ($this->timeParts as $part) {
$vars['with_'.$part] = $options['with_'.$part];
}
$view->vars = array_replace($view->vars, $vars);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$timeParts = $this->timeParts;
$compound = function (Options $options) {
return $options['widget'] !== 'single_text';
};
$placeholderDefault = function (Options $options) {
return $options['required'] ? null : '';
};
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault, $timeParts) {
if (is_array($placeholder)) {
$default = $placeholderDefault($options);
return array_merge(array_fill_keys($timeParts, $default), $placeholder);
}
return array_fill_keys($timeParts, $placeholder);
};
$labelsNormalizer = function (Options $options, array $labels) {
return array_replace(array(
'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(
array(
'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,
'labels' => array(),
)
);
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('labels', $labelsNormalizer);
$resolver->setAllowedValues(
'input',
array(
'dateinterval',
'string',
'array',
)
);
$resolver->setAllowedValues(
'widget',
array(
'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,290 @@
<?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\Exception\InvalidOptionsException;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToRfc3339Transformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DateTimeType extends AbstractType
{
const DEFAULT_DATE_FORMAT = \IntlDateFormatter::MEDIUM;
const DEFAULT_TIME_FORMAT = \IntlDateFormatter::MEDIUM;
/**
* This is not quite the HTML5 format yet, because ICU lacks the
* capability of parsing and generating RFC 3339 dates, which
* are like the below pattern but with a timezone suffix. The
* timezone suffix is.
*
* * "Z" for UTC
* * "(-|+)HH:mm" for other timezones (note the colon!)
*
* For more information see:
*
* http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax
* http://www.w3.org/TR/html-markup/input.datetime.html
* http://tools.ietf.org/html/rfc3339
*
* An ICU ticket was created:
* http://icu-project.org/trac/ticket/9421
*
* It was supposedly fixed, but is not available in all PHP installations
* yet. To temporarily circumvent this issue, DateTimeToRfc3339Transformer
* is used when the format matches this constant.
*/
const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZZZZZ";
private static $acceptedFormats = array(
\IntlDateFormatter::FULL,
\IntlDateFormatter::LONG,
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::SHORT,
);
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$parts = array('year', 'month', 'day', 'hour');
$dateParts = array('year', 'month', 'day');
$timeParts = array('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::$acceptedFormats, 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 DateTimeToRfc3339Transformer(
$options['model_timezone'],
$options['view_timezone']
));
} else {
$builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
$options['model_timezone'],
$options['view_timezone'],
$dateFormat,
$timeFormat,
$calendar,
$pattern
));
}
} else {
// Only pass a subset of the options to children
$dateOptions = array_intersect_key($options, array_flip(array(
'years',
'months',
'days',
'placeholder',
'choice_translation_domain',
'required',
'translation_domain',
'html5',
'invalid_message',
'invalid_message_parameters',
)));
$timeOptions = array_intersect_key($options, array_flip(array(
'hours',
'minutes',
'seconds',
'with_minutes',
'with_seconds',
'placeholder',
'choice_translation_domain',
'required',
'translation_domain',
'html5',
'invalid_message',
'invalid_message_parameters',
)));
if (null !== $options['date_widget']) {
$dateOptions['widget'] = $options['date_widget'];
}
if (null !== $options['time_widget']) {
$timeOptions['widget'] = $options['time_widget'];
}
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(array(
new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts),
new ArrayToPartsTransformer(array(
'date' => $dateParts,
'time' => $timeParts,
)),
)))
->add('date', __NAMESPACE__.'\DateType', $dateOptions)
->add('time', __NAMESPACE__.'\TimeType', $timeOptions)
;
}
if ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'])
));
} 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 a 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';
}
}
/**
* {@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 $options['widget'];
};
// Defaults to the value of "widget"
$timeWidget = function (Options $options) {
return $options['widget'];
};
$resolver->setDefaults(array(
'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,
));
// Don't add some defaults in order to preserve the defaults
// set in DateType and TimeType
$resolver->setDefined(array(
'placeholder',
'choice_translation_domain',
'years',
'months',
'days',
'hours',
'minutes',
'seconds',
));
$resolver->setAllowedValues('input', array(
'datetime',
'string',
'timestamp',
'array',
));
$resolver->setAllowedValues('date_widget', array(
null, // inherit default from DateType
'single_text',
'text',
'choice',
));
$resolver->setAllowedValues('time_widget', array(
null, // inherit default from TimeType
'single_text',
'text',
'choice',
));
// This option will overwrite "date_widget" and "time_widget" options
$resolver->setAllowedValues('widget', array(
null, // default, don't overwrite options
'single_text',
'text',
'choice',
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'datetime';
}
}

View File

@@ -0,0 +1,345 @@
<?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\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
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\ReversedTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
class DateType extends AbstractType
{
const DEFAULT_FORMAT = \IntlDateFormatter::MEDIUM;
const HTML5_FORMAT = 'yyyy-MM-dd';
private static $acceptedFormats = array(
\IntlDateFormatter::FULL,
\IntlDateFormatter::LONG,
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::SHORT,
);
private static $widgets = array(
'text' => 'Symfony\Component\Form\Extension\Core\Type\TextType',
'choice' => 'Symfony\Component\Form\Extension\Core\Type\ChoiceType',
);
/**
* {@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'] : null;
if (!in_array($dateFormat, self::$acceptedFormats, 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 (null !== $pattern && false === strpos($pattern, 'y') && false === strpos($pattern, 'M') && false === strpos($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 (null !== $pattern && (false === strpos($pattern, 'y') || false === strpos($pattern, 'M') || false === strpos($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 = array(
'error_bubbling' => true,
);
$formatter = new \IntlDateFormatter(
\Locale::getDefault(),
$dateFormat,
$timeFormat,
// see https://bugs.php.net/bug.php?id=66323
class_exists('IntlTimeZone', false) ? \IntlTimeZone::createDefault() : null,
$calendar,
$pattern
);
// new \IntlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/bug.php?id=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 (array('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'], array('year', 'month', 'day')
))
->setAttribute('formatter', $formatter)
;
}
if ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], 'Y-m-d')
));
} 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'], array('year', 'month', 'day'))
));
}
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
$view->vars['widget'] = $options['widget'];
// Change the input to a 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(array('/y+/', '/M+/', '/d+/'), array('{{ 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(
array('year' => $default, 'month' => $default, 'day' => $default),
$placeholder
);
}
return array(
'year' => $placeholder,
'month' => $placeholder,
'day' => $placeholder,
);
};
$choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) {
if (is_array($choiceTranslationDomain)) {
$default = false;
return array_replace(
array('year' => $default, 'month' => $default, 'day' => $default),
$choiceTranslationDomain
);
}
return array(
'year' => $choiceTranslationDomain,
'month' => $choiceTranslationDomain,
'day' => $choiceTranslationDomain,
);
};
$format = function (Options $options) {
return 'single_text' === $options['widget'] ? DateType::HTML5_FORMAT : DateType::DEFAULT_FORMAT;
};
$resolver->setDefaults(array(
'years' => range(date('Y') - 5, 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,
'choice_translation_domain' => false,
));
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setAllowedValues('input', array(
'datetime',
'string',
'timestamp',
'array',
));
$resolver->setAllowedValues('widget', array(
'single_text',
'text',
'choice',
));
$resolver->setAllowedTypes('format', array('int', 'string'));
$resolver->setAllowedTypes('years', 'array');
$resolver->setAllowedTypes('months', 'array');
$resolver->setAllowedTypes('days', 'array');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'date';
}
private function formatTimestamps(\IntlDateFormatter $formatter, $regex, array $timestamps)
{
$pattern = $formatter->getPattern();
$timezone = $formatter->getTimezoneId();
$formattedTimestamps = array();
$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 = array();
foreach ($years as $year) {
if (false !== $y = gmmktime(0, 0, 0, 6, 15, $year)) {
$result[$y] = $year;
}
}
return $result;
}
private function listMonths(array $months)
{
$result = array();
foreach ($months as $month) {
$result[gmmktime(0, 0, 0, $month, 15)] = $month;
}
return $result;
}
private function listDays(array $days)
{
$result = array();
foreach ($days as $day) {
$result[gmmktime(0, 0, 0, 5, $day)] = $day;
}
return $result;
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
class EmailType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'email';
}
}

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\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;
class FileType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['multiple']) {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
// submitted data for an input file (not required) without choosing any file
if (array(null) === $data) {
$emptyData = $form->getConfig()->getEmptyData();
$data = is_callable($emptyData) ? call_user_func($emptyData, $form, $data) : $emptyData;
$event->setData($data);
}
});
}
}
/**
* {@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, array(
'type' => 'file',
'value' => '',
));
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
$view->vars['multipart'] = true;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$dataClass = function (Options $options) {
return $options['multiple'] ? null : 'Symfony\Component\HttpFoundation\File\File';
};
$emptyData = function (Options $options) {
return $options['multiple'] ? array() : null;
};
$resolver->setDefaults(array(
'compound' => false,
'data_class' => $dataClass,
'empty_data' => $emptyData,
'multiple' => false,
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'file';
}
}

View File

@@ -0,0 +1,203 @@
<?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\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\EventListener\TrimListener;
use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class FormType extends BaseType
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $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'] ? new PropertyPathMapper($this->propertyAccessor) : null)
->setMethod($options['method'])
->setAction($options['action']);
if ($options['trim']) {
$builder->addEventSubscriber(new TrimListener());
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
parent::buildView($view, $form, $options);
$name = $form->getName();
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;
}
}
$view->vars = array_replace($view->vars, array(
'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'],
'compound' => $form->getConfig()->getCompound(),
'method' => $form->getConfig()->getMethod(),
'action' => $form->getConfig()->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() ? array() : '';
};
};
// 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'];
};
// If data is given, the form is locked to that data
// (independent of its value)
$resolver->setDefined(array(
'data',
));
$resolver->setDefaults(array(
'data_class' => $dataClass,
'empty_data' => $emptyData,
'trim' => true,
'required' => true,
'property_path' => null,
'mapped' => true,
'by_reference' => true,
'error_bubbling' => $errorBubbling,
'label_attr' => array(),
'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' => array(),
'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.',
'upload_max_size_message' => $uploadMaxSizeMessage, // internal
));
$resolver->setAllowedTypes('label_attr', 'array');
$resolver->setAllowedTypes('upload_max_size_message', array('callable'));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'form';
}
}

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\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class HiddenType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
// hidden fields cannot have a required attribute
'required' => false,
// Pass errors to the parent
'error_bubbling' => true,
'compound' => false,
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'hidden';
}
}

View File

@@ -0,0 +1,68 @@
<?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\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
use Symfony\Component\OptionsResolver\OptionsResolver;
class IntegerType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(
new IntegerToLocalizedStringTransformer(
$options['scale'],
$options['grouping'],
$options['rounding_mode']
));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
// default scale is locale specific (usually around 3)
'scale' => null,
'grouping' => false,
// Integer cast rounds towards 0, so do the same when displaying fractions
'rounding_mode' => IntegerToLocalizedStringTransformer::ROUND_DOWN,
'compound' => false,
));
$resolver->setAllowedValues('rounding_mode', array(
IntegerToLocalizedStringTransformer::ROUND_FLOOR,
IntegerToLocalizedStringTransformer::ROUND_DOWN,
IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN,
IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN,
IntegerToLocalizedStringTransformer::ROUND_HALF_UP,
IntegerToLocalizedStringTransformer::ROUND_UP,
IntegerToLocalizedStringTransformer::ROUND_CEILING,
));
$resolver->setAllowedTypes('scale', array('null', 'int'));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'integer';
}
}

View File

@@ -0,0 +1,118 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Intl\Intl;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LanguageType extends AbstractType implements ChoiceLoaderInterface
{
/**
* Language loaded choice list.
*
* The choices are lazy loaded and generated from the Intl component.
*
* {@link \Symfony\Component\Intl\Intl::getLanguageBundle()}.
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_loader' => function (Options $options) {
if ($options['choices']) {
@trigger_error(sprintf('Using the "choices" option in %s has been deprecated since version 3.3 and will be ignored in 4.0. Override the "choice_loader" option instead or set it to null.', __CLASS__), E_USER_DEPRECATED);
return null;
}
return $this;
},
'choice_translation_domain' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ChoiceType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'language';
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
return $this->choiceList = new ArrayChoiceList(array_flip(Intl::getLanguageBundle()->getLanguageNames()), $value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
$values = array_filter($values);
if (empty($values)) {
return array();
}
// If no callable is set, values are the same as choices
if (null === $value) {
return $values;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Optimize
$choices = array_filter($choices);
if (empty($choices)) {
return array();
}
// If no callable is set, choices are the same as values
if (null === $value) {
return $choices;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
}

View File

@@ -0,0 +1,118 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Intl\Intl;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LocaleType extends AbstractType implements ChoiceLoaderInterface
{
/**
* Locale loaded choice list.
*
* The choices are lazy loaded and generated from the Intl component.
*
* {@link \Symfony\Component\Intl\Intl::getLocaleBundle()}.
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_loader' => function (Options $options) {
if ($options['choices']) {
@trigger_error(sprintf('Using the "choices" option in %s has been deprecated since version 3.3 and will be ignored in 4.0. Override the "choice_loader" option instead or set it to null.', __CLASS__), E_USER_DEPRECATED);
return null;
}
return $this;
},
'choice_translation_domain' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ChoiceType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'locale';
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
return $this->choiceList = new ArrayChoiceList(array_flip(Intl::getLocaleBundle()->getLocaleNames()), $value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
$values = array_filter($values);
if (empty($values)) {
return array();
}
// If no callable is set, values are the same as choices
if (null === $value) {
return $values;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Optimize
$choices = array_filter($choices);
if (empty($choices)) {
return array();
}
// If no callable is set, choices are the same as values
if (null === $value) {
return $choices;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
}

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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MoneyType extends AbstractType
{
protected static $patterns = array();
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addViewTransformer(new MoneyToLocalizedStringTransformer(
$options['scale'],
$options['grouping'],
null,
$options['divisor']
))
;
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['money_pattern'] = self::getPattern($options['currency']);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'scale' => 2,
'grouping' => false,
'divisor' => 1,
'currency' => 'EUR',
'compound' => false,
));
$resolver->setAllowedTypes('scale', 'int');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'money';
}
/**
* Returns the pattern for this locale.
*
* The pattern contains the placeholder "{{ widget }}" where the HTML tag should
* be inserted
*/
protected static function getPattern($currency)
{
if (!$currency) {
return '{{ widget }}';
}
$locale = \Locale::getDefault();
if (!isset(self::$patterns[$locale])) {
self::$patterns[$locale] = array();
}
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,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
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']
));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
// default scale is locale specific (usually around 3)
'scale' => null,
'grouping' => false,
'rounding_mode' => NumberToLocalizedStringTransformer::ROUND_HALF_UP,
'compound' => false,
));
$resolver->setAllowedValues('rounding_mode', array(
NumberToLocalizedStringTransformer::ROUND_FLOOR,
NumberToLocalizedStringTransformer::ROUND_DOWN,
NumberToLocalizedStringTransformer::ROUND_HALF_DOWN,
NumberToLocalizedStringTransformer::ROUND_HALF_EVEN,
NumberToLocalizedStringTransformer::ROUND_HALF_UP,
NumberToLocalizedStringTransformer::ROUND_UP,
NumberToLocalizedStringTransformer::ROUND_CEILING,
));
$resolver->setAllowedTypes('scale', array('null', 'int'));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'number';
}
}

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\Form\FormInterface;
use Symfony\Component\Form\FormView;
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(array(
'always_empty' => true,
'trim' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'password';
}
}

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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer;
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']));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'scale' => 0,
'type' => 'fractional',
'compound' => false,
));
$resolver->setAllowedValues('type', array(
'fractional',
'integer',
));
$resolver->setAllowedTypes('scale', 'int');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'percent';
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
class RadioType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\CheckboxType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'radio';
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
class RangeType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'range';
}
}

View File

@@ -0,0 +1,71 @@
<?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\Extension\Core\DataTransformer\ValueToDuplicatesTransformer;
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'];
}
$builder
->addViewTransformer(new ValueToDuplicatesTransformer(array(
$options['first_name'],
$options['second_name'],
)))
->add($options['first_name'], $options['type'], array_merge($options['options'], $options['first_options']))
->add($options['second_name'], $options['type'], array_merge($options['options'], $options['second_options']))
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'type' => __NAMESPACE__.'\TextType',
'options' => array(),
'first_options' => array(),
'second_options' => array(),
'first_name' => 'first',
'second_name' => 'second',
'error_bubbling' => false,
));
$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 __NAMESPACE__.'\ButtonType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'reset';
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
class SearchType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'search';
}
}

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\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\SubmitButtonTypeInterface;
/**
* 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();
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ButtonType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'submit';
}
}

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(array(
'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 null === $data ? '' : $data;
}
}

View File

@@ -0,0 +1,43 @@
<?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\FormView;
use Symfony\Component\Form\FormInterface;
class TextareaType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['pattern'] = null;
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'textarea';
}
}

View File

@@ -0,0 +1,277 @@
<?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\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TimeType extends AbstractType
{
private static $widgets = array(
'text' => 'Symfony\Component\Form\Extension\Core\Type\TextType',
'choice' => 'Symfony\Component\Form\Extension\Core\Type\ChoiceType',
);
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$parts = array('hour');
$format = 'H';
if ($options['with_seconds'] && !$options['with_minutes']) {
throw new InvalidConfigurationException('You can not disable minutes if you have enabled seconds.');
}
if ($options['with_minutes']) {
$format .= ':i';
$parts[] = 'minute';
}
if ($options['with_seconds']) {
$format .= ':s';
$parts[] = 'second';
}
if ('single_text' === $options['widget']) {
$builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format));
// handle seconds ignored by user's browser when with_seconds enabled
// https://codereview.chromium.org/450533009/
if ($options['with_seconds']) {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $e) {
$data = $e->getData();
if ($data && preg_match('/^\d{2}:\d{2}$/', $data)) {
$e->setData($data.':00');
}
});
}
} else {
$hourOptions = $minuteOptions = $secondOptions = array(
'error_bubbling' => true,
);
if ('choice' === $options['widget']) {
$hours = $minutes = array();
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 = array();
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 (array('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']) {
$builder->add('minute', self::$widgets[$options['widget']], $minuteOptions);
}
if ($options['with_seconds']) {
$builder->add('second', self::$widgets[$options['widget']], $secondOptions);
}
$builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget']));
}
if ('string' === $options['input']) {
$builder->addModelTransformer(new ReversedTransformer(
new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], 'H:i:s')
));
} 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 = array_replace($view->vars, array(
'widget' => $options['widget'],
'with_minutes' => $options['with_minutes'],
'with_seconds' => $options['with_seconds'],
));
// Change the input to a 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(
array('hour' => $default, 'minute' => $default, 'second' => $default),
$placeholder
);
}
return array(
'hour' => $placeholder,
'minute' => $placeholder,
'second' => $placeholder,
);
};
$choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) {
if (is_array($choiceTranslationDomain)) {
$default = false;
return array_replace(
array('hour' => $default, 'minute' => $default, 'second' => $default),
$choiceTranslationDomain
);
}
return array(
'hour' => $choiceTranslationDomain,
'minute' => $choiceTranslationDomain,
'second' => $choiceTranslationDomain,
);
};
$resolver->setDefaults(array(
'hours' => range(0, 23),
'minutes' => range(0, 59),
'seconds' => range(0, 59),
'widget' => 'choice',
'input' => 'datetime',
'with_minutes' => true,
'with_seconds' => false,
'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,
'choice_translation_domain' => false,
));
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
$resolver->setAllowedValues('input', array(
'datetime',
'string',
'timestamp',
'array',
));
$resolver->setAllowedValues('widget', array(
'single_text',
'text',
'choice',
));
$resolver->setAllowedTypes('hours', 'array');
$resolver->setAllowedTypes('minutes', 'array');
$resolver->setAllowedTypes('seconds', 'array');
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'time';
}
}

View File

@@ -0,0 +1,144 @@
<?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\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TimezoneType extends AbstractType implements ChoiceLoaderInterface
{
/**
* Timezone loaded choice list.
*
* The choices are generated from the ICU function \DateTimeZone::listIdentifiers().
*
* @var ArrayChoiceList
*/
private $choiceList;
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_loader' => function (Options $options) {
if ($options['choices']) {
@trigger_error(sprintf('Using the "choices" option in %s has been deprecated since version 3.3 and will be ignored in 4.0. Override the "choice_loader" option instead or set it to null.', __CLASS__), E_USER_DEPRECATED);
return null;
}
return $this;
},
'choice_translation_domain' => false,
));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\ChoiceType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'timezone';
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (null !== $this->choiceList) {
return $this->choiceList;
}
return $this->choiceList = new ArrayChoiceList($this->getTimezones(), $value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Optimize
$values = array_filter($values);
if (empty($values)) {
return array();
}
// If no callable is set, values are the same as choices
if (null === $value) {
return $values;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Optimize
$choices = array_filter($choices);
if (empty($choices)) {
return array();
}
// If no callable is set, choices are the same as values
if (null === $value) {
return $choices;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
/**
* Returns a normalized array of timezone choices.
*
* @return array The timezone choices
*/
private static function getTimezones()
{
$timezones = array();
foreach (\DateTimeZone::listIdentifiers() as $timezone) {
$parts = explode('/', $timezone);
if (count($parts) > 2) {
$region = $parts[0];
$name = $parts[1].' - '.$parts[2];
} elseif (count($parts) > 1) {
$region = $parts[0];
$name = $parts[1];
} else {
$region = 'Other';
$name = $parts[0];
}
$timezones[$region][str_replace('_', ' ', $name)] = $timezone;
}
return $timezones;
}
}

View File

@@ -0,0 +1,56 @@
<?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\Extension\Core\EventListener\FixUrlProtocolListener;
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 configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('default_protocol', 'http');
$resolver->setAllowedTypes('default_protocol', array('null', 'string'));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'url';
}
}

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\Csrf;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* This extension protects forms by using a CSRF token.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CsrfExtension extends AbstractExtension
{
/**
* @var CsrfTokenManagerInterface
*/
private $tokenManager;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var null|string
*/
private $translationDomain;
/**
* Constructor.
*
* @param CsrfTokenManagerInterface $tokenManager The CSRF token manager
* @param TranslatorInterface $translator The translator for translating error messages
* @param null|string $translationDomain The translation domain for translating
*/
public function __construct(CsrfTokenManagerInterface $tokenManager, TranslatorInterface $translator = null, $translationDomain = null)
{
$this->tokenManager = $tokenManager;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
}
/**
* {@inheritdoc}
*/
protected function loadTypeExtensions()
{
return array(
new Type\FormTypeCsrfExtension($this->tokenManager, true, '_token', $this->translator, $this->translationDomain),
);
}
}

View File

@@ -0,0 +1,116 @@
<?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\FormEvents;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CsrfValidationListener implements EventSubscriberInterface
{
/**
* The name of the CSRF field.
*
* @var string
*/
private $fieldName;
/**
* The generator for CSRF tokens.
*
* @var CsrfTokenManagerInterface
*/
private $tokenManager;
/**
* A text mentioning the tokenId of the CSRF token.
*
* Validation of the token will only succeed if it was generated in the
* same session and with the same tokenId.
*
* @var string
*/
private $tokenId;
/**
* The message displayed in case of an error.
*
* @var string
*/
private $errorMessage;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var null|string
*/
private $translationDomain;
/**
* @var ServerParams
*/
private $serverParams;
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SUBMIT => 'preSubmit',
);
}
public function __construct($fieldName, CsrfTokenManagerInterface $tokenManager, $tokenId, $errorMessage, TranslatorInterface $translator = null, $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 = $form->getConfig()->getMethod() === 'POST' && $this->serverParams->hasPostMaxSizeBeenExceeded();
if ($form->isRoot() && $form->getConfig()->getOption('compound') && !$postRequestSizeExceeded) {
$data = $event->getData();
if (!isset($data[$this->fieldName]) || !$this->tokenManager->isTokenValid(new CsrfToken($this->tokenId, $data[$this->fieldName]))) {
$errorMessage = $this->errorMessage;
if (null !== $this->translator) {
$errorMessage = $this->translator->trans($errorMessage, array(), $this->translationDomain);
}
$form->addError(new FormError($errorMessage));
}
if (is_array($data)) {
unset($data[$this->fieldName]);
$event->setData($data);
}
}
}
}

View File

@@ -0,0 +1,137 @@
<?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\Csrf\EventListener\CsrfValidationListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormTypeCsrfExtension extends AbstractTypeExtension
{
/**
* @var CsrfTokenManagerInterface
*/
private $defaultTokenManager;
/**
* @var bool
*/
private $defaultEnabled;
/**
* @var string
*/
private $defaultFieldName;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var null|string
*/
private $translationDomain;
/**
* @var ServerParams
*/
private $serverParams;
public function __construct(CsrfTokenManagerInterface $defaultTokenManager, $defaultEnabled = true, $defaultFieldName = '_token', TranslatorInterface $translator = null, $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.
*
* @param FormBuilderInterface $builder The form builder
* @param array $options The options
*/
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.
*
* @param FormView $view The form view
* @param FormInterface $form The form
* @param array $options The options
*/
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'], 'Symfony\Component\Form\Extension\Core\Type\HiddenType', $data, array(
'mapped' => false,
));
$view->children[$options['csrf_field_name']] = $csrfForm->createView($view);
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'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 function getExtendedType()
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
}
}

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\DataCollector;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
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
{
/**
* @var EventSubscriberInterface
*/
private $dataCollector;
public function __construct(FormDataCollectorInterface $dataCollector)
{
$this->dataCollector = $dataCollector;
}
/**
* {@inheritdoc}
*/
protected function loadTypeExtensions()
{
return array(
new Type\DataCollectorTypeExtension($this->dataCollector),
);
}
}

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\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
{
/**
* @var FormDataCollectorInterface
*/
private $dataCollector;
public function __construct(FormDataCollectorInterface $dataCollector)
{
$this->dataCollector = $dataCollector;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
// High priority in order to be called as soon as possible
FormEvents::POST_SET_DATA => array('postSetData', 255),
// Low priority in order to be called as late as possible
FormEvents::POST_SUBMIT => array('postSubmit', -255),
);
}
/**
* Listener for the {@link FormEvents::POST_SET_DATA} event.
*
* @param FormEvent $event The event object
*/
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.
*
* @param FormEvent $event The event object
*/
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,377 @@
<?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\HttpKernel\DataCollector\Util\ValueExporter;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Caster\ClassStub;
use Symfony\Component\VarDumper\Caster\CutStub;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\Stub;
use Symfony\Component\VarDumper\Cloner\VarCloner;
/**
* Data collector for {@link FormInterface} instances.
*
* @author Robert Schönthal <robert.schoenthal@gmail.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormDataCollector extends DataCollector implements FormDataCollectorInterface
{
/**
* @var FormDataExtractor
*/
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;
/**
* @var ValueExporter
*/
private $valueExporter;
/**
* @var ClonerInterface
*/
private $cloner;
private $hasVarDumper;
public function __construct(FormDataExtractorInterface $dataExtractor)
{
$this->dataExtractor = $dataExtractor;
$this->data = array(
'forms' => array(),
'forms_by_hash' => array(),
'nb_errors' => 0,
);
$this->hasVarDumper = class_exists(ClassStub::class);
}
/**
* Does nothing. The data is collected during the form event listeners.
*/
public function collect(Request $request, Response $response, \Exception $exception = null)
{
}
/**
* {@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] = array();
}
$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])) {
$this->dataByForm[$hash] = array();
}
$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] = array();
}
$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()
{
return 'form';
}
/**
* {@inheritdoc}
*/
public function getData()
{
return $this->data;
}
public function serialize()
{
if ($this->hasVarDumper) {
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']);
}
}
}
return serialize($this->cloneVar($this->data));
}
/**
* {@inheritdoc}
*/
protected function cloneVar($var, $isClass = false)
{
if ($var instanceof Data) {
return $var;
}
if (null === $this->cloner) {
if ($this->hasVarDumper) {
$this->cloner = new VarCloner();
$this->cloner->setMaxItems(-1);
$this->cloner->addCasters(array(
'*' => function ($v, array $a, Stub $s, $isNested) {
foreach ($a as &$v) {
if (is_object($v) && !$v instanceof \DateTimeInterface) {
$v = new CutStub($v);
}
}
return $a;
},
\Exception::class => function (\Exception $e, array $a, Stub $s) {
if (isset($a[$k = "\0Exception\0previous"])) {
unset($a[$k]);
++$s->cut;
}
return $a;
},
FormInterface::class => function (FormInterface $f, array $a) {
return array(
Caster::PREFIX_VIRTUAL.'name' => $f->getName(),
Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub(get_class($f->getConfig()->getType()->getInnerType())),
);
},
ConstraintViolationInterface::class => function (ConstraintViolationInterface $v, array $a) {
return array(
Caster::PREFIX_VIRTUAL.'root' => $v->getRoot(),
Caster::PREFIX_VIRTUAL.'path' => $v->getPropertyPath(),
Caster::PREFIX_VIRTUAL.'value' => $v->getInvalidValue(),
);
},
));
} else {
@trigger_error(sprintf('Using the %s() method without the VarDumper component is deprecated since version 3.2 and won\'t be supported in 4.0. Install symfony/var-dumper version 3.2 or above.', __METHOD__), E_USER_DEPRECATED);
$this->cloner = false;
}
}
if (false !== $this->cloner) {
return $this->cloner->cloneVar($var, Caster::EXCLUDE_VERBOSE);
}
if (null === $this->valueExporter) {
$this->valueExporter = new ValueExporter();
}
return $this->valueExporter->exportValue($var);
}
private function &recursiveBuildPreliminaryFormTree(FormInterface $form, array &$outputByHash)
{
$hash = spl_object_hash($form);
$output = &$outputByHash[$hash];
$output = isset($this->dataByForm[$hash])
? $this->dataByForm[$hash]
: array();
$output['children'] = array();
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 = isset($this->dataByView[$viewHash])
? $this->dataByView[$viewHash]
: array();
if (null !== $formHash) {
$output = array_replace(
$output,
isset($this->dataByForm[$formHash])
? $this->dataByForm[$formHash]
: array()
);
}
$output['children'] = array();
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,100 @@
<?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;
/**
* 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.
*
* @param FormInterface $form A root form
*/
public function collectConfiguration(FormInterface $form);
/**
* Stores the default data of the given form and its children.
*
* @param FormInterface $form A root form
*/
public function collectDefaultData(FormInterface $form);
/**
* Stores the submitted data of the given form and its children.
*
* @param FormInterface $form A root form
*/
public function collectSubmittedData(FormInterface $form);
/**
* Stores the view variables of the given form view and its children.
*
* @param FormView $view A root form view
*/
public function collectViewVariables(FormView $view);
/**
* Specifies that the given objects represent the same conceptual form.
*
* @param FormInterface $form A form object
* @param FormView $view A view object
*/
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()}.
*
* @param FormInterface $form A root form
*/
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.
*
* @param FormInterface $form A root form
* @param FormView $view A root view
*/
public function buildFinalFormTree(FormInterface $form, FormView $view);
/**
* Returns all collected data.
*
* @return array
*/
public function getData();
}

View File

@@ -0,0 +1,183 @@
<?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\Util\ValueExporter;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Default implementation of {@link FormDataExtractorInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormDataExtractor implements FormDataExtractorInterface
{
/**
* Constructs a new data extractor.
*/
public function __construct(ValueExporter $valueExporter = null, $triggerDeprecationNotice = true)
{
if (null !== $valueExporter && $triggerDeprecationNotice) {
@trigger_error('Passing a ValueExporter instance to '.__METHOD__.'() is deprecated in version 3.2 and will be removed in 4.0.', E_USER_DEPRECATED);
}
}
/**
* {@inheritdoc}
*/
public function extractConfiguration(FormInterface $form)
{
$data = array(
'id' => $this->buildId($form),
'name' => $form->getName(),
'type_class' => get_class($form->getConfig()->getType()->getInnerType()),
'synchronized' => $form->isSynchronized(),
'passed_options' => array(),
'resolved_options' => array(),
);
foreach ($form->getConfig()->getAttribute('data_collector/passed_options', array()) 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 = array(
'default_data' => array(
'norm' => $form->getNormData(),
),
'submitted_data' => array(),
);
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 = array(
'submitted_data' => array(
'norm' => $form->getNormData(),
),
'errors' => array(),
);
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 = array(
'message' => $error->getMessage(),
'origin' => is_object($error->getOrigin())
? spl_object_hash($error->getOrigin())
: null,
'trace' => array(),
);
$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 = array(
'id' => isset($view->vars['id']) ? $view->vars['id'] : null,
'name' => isset($view->vars['name']) ? $view->vars['name'] : null,
'view_vars' => array(),
);
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.
*
* @param FormInterface $form The form
*
* @return string The HTML ID
*/
private function buildId(FormInterface $form)
{
$id = $form->getName();
if (null !== $form->getParent()) {
$id = $this->buildId($form->getParent()).'_'.$id;
}
return $id;
}
}

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\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.
*
* @param FormInterface $form The form
*
* @return array Information about the form's configuration
*/
public function extractConfiguration(FormInterface $form);
/**
* Extracts the default data of a form.
*
* @param FormInterface $form The form
*
* @return array Information about the form's default data
*/
public function extractDefaultData(FormInterface $form);
/**
* Extracts the submitted data of a form.
*
* @param FormInterface $form The form
*
* @return array Information about the form's submitted data
*/
public function extractSubmittedData(FormInterface $form);
/**
* Extracts the view variables of a form.
*
* @param FormView $view The form view
*
* @return array Information about the view's variables
*/
public function extractViewVariables(FormView $view);
}

View File

@@ -0,0 +1,147 @@
<?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
{
/**
* @var ResolvedFormTypeInterface
*/
private $proxiedType;
/**
* @var FormDataCollectorInterface
*/
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, $name, array $options = array())
{
$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,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\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
{
/**
* @var ResolvedFormTypeFactoryInterface
*/
private $proxiedFactory;
/**
* @var FormDataCollectorInterface
*/
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,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\DataCollector\Type;
use Symfony\Component\Form\AbstractTypeExtension;
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 \Symfony\Component\EventDispatcher\EventSubscriberInterface
*/
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 function getExtendedType()
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
}
}

View File

@@ -0,0 +1,133 @@
<?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\FormExtensionInterface;
use Symfony\Component\Form\FormTypeGuesserChain;
use Symfony\Component\Form\Exception\InvalidArgumentException;
class DependencyInjectionExtension implements FormExtensionInterface
{
private $guesser;
private $guesserLoaded = false;
private $typeContainer;
private $typeExtensionServices;
private $guesserServices;
// @deprecated to be removed in Symfony 4.0
private $typeServiceIds;
private $guesserServiceIds;
/**
* Constructor.
*
* @param ContainerInterface $typeContainer
* @param iterable[] $typeExtensionServices
* @param iterable $guesserServices
*/
public function __construct(ContainerInterface $typeContainer, array $typeExtensionServices, $guesserServices, array $guesserServiceIds = null)
{
if (null !== $guesserServiceIds) {
@trigger_error(sprintf('Passing four arguments to the %s::__construct() method is deprecated since Symfony 3.3 and will be disallowed in Symfony 4.0. The new constructor only accepts three arguments.', __CLASS__), E_USER_DEPRECATED);
$this->guesserServiceIds = $guesserServiceIds;
$this->typeServiceIds = $typeExtensionServices;
$typeExtensionServices = $guesserServices;
$guesserServices = $guesserServiceIds;
}
$this->typeContainer = $typeContainer;
$this->typeExtensionServices = $typeExtensionServices;
$this->guesserServices = $guesserServices;
}
public function getType($name)
{
if (null !== $this->guesserServiceIds) {
if (!isset($this->typeServiceIds[$name])) {
throw new InvalidArgumentException(sprintf('The field type "%s" is not registered in the service container.', $name));
}
return $this->typeContainer->get($this->typeServiceIds[$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);
}
public function hasType($name)
{
if (null !== $this->guesserServiceIds) {
return isset($this->typeServiceIds[$name]);
}
return $this->typeContainer->has($name);
}
public function getTypeExtensions($name)
{
$extensions = array();
if (isset($this->typeExtensionServices[$name])) {
foreach ($this->typeExtensionServices[$name] as $serviceId => $extension) {
if (null !== $this->guesserServiceIds) {
$extension = $this->typeContainer->get($serviceId = $extension);
}
$extensions[] = $extension;
// validate result of getExtendedType() to ensure it is consistent with the service definition
if ($extension->getExtendedType() !== $name) {
throw new InvalidArgumentException(
sprintf('The extended type specified for the service "%s" does not match the actual extended type. Expected "%s", given "%s".',
$serviceId,
$name,
$extension->getExtendedType()
)
);
}
}
}
return $extensions;
}
public function hasTypeExtensions($name)
{
return isset($this->typeExtensionServices[$name]);
}
public function getTypeGuesser()
{
if (!$this->guesserLoaded) {
$this->guesserLoaded = true;
$guessers = array();
foreach ($this->guesserServices as $serviceId => $service) {
if (null !== $this->guesserServiceIds) {
$service = $this->typeContainer->get($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 array(
new Type\FormTypeHttpFoundationExtension(),
);
}
}

View File

@@ -0,0 +1,115 @@
<?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\Request;
/**
* A request processor using the {@link Request} class of the HttpFoundation
* component.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class HttpFoundationRequestHandler implements RequestHandlerInterface
{
/**
* @var ServerParams
*/
private $serverParams;
/**
* {@inheritdoc}
*/
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->get($name);
}
} else {
// Mark the form with an error if the uploaded size was too large
// This is done here and not in FormValidator because $_POST is
// empty when that error occurs. Hence the form is never submitted.
if ($this->serverParams->hasPostMaxSizeBeenExceeded()) {
// Submit the form, but don't clear the default values
$form->submit(null, false);
$form->addError(new FormError(
call_user_func($form->getConfig()->getOption('upload_max_size_message')),
null,
array('{{ 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() ? array() : null;
$params = $request->request->get($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);
}
}

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\HttpFoundation\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\RequestHandlerInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormTypeHttpFoundationExtension extends AbstractTypeExtension
{
/**
* @var RequestHandlerInterface
*/
private $requestHandler;
/**
* @param RequestHandlerInterface $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 function getExtendedType()
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Templating;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Templating\PhpEngine;
use Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper;
/**
* Integrates the Templating component with the Form library.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TemplatingExtension extends AbstractExtension
{
public function __construct(PhpEngine $engine, CsrfTokenManagerInterface $csrfTokenManager = null, array $defaultThemes = array())
{
$engine->addHelpers(array(
new FormHelper(new FormRenderer(new TemplatingRendererEngine($engine, $defaultThemes), $csrfTokenManager)),
));
}
}

View File

@@ -0,0 +1,125 @@
<?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\Templating;
use Symfony\Component\Form\AbstractRendererEngine;
use Symfony\Component\Form\FormView;
use Symfony\Component\Templating\EngineInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TemplatingRendererEngine extends AbstractRendererEngine
{
/**
* @var EngineInterface
*/
private $engine;
public function __construct(EngineInterface $engine, array $defaultThemes = array())
{
parent::__construct($defaultThemes);
$this->engine = $engine;
}
/**
* {@inheritdoc}
*/
public function renderBlock(FormView $view, $resource, $blockName, array $variables = array())
{
return trim($this->engine->render($resource, $variables));
}
/**
* Loads the cache with the resource for a given block name.
*
* This implementation tries to load as few blocks as possible, since each block
* is represented by a template on the file system.
*
* @see getResourceForBlock()
*
* @param string $cacheKey The cache key of the form view
* @param FormView $view The form view for finding the applying themes
* @param string $blockName The name of the block to load
*
* @return bool True if the resource could be loaded, false otherwise
*/
protected function loadResourceForBlockName($cacheKey, FormView $view, $blockName)
{
// Recursively try to find the block in the themes assigned to $view,
// then of its parent form, then of the parent form of the parent and so on.
// When the root form is reached in this recursion, also the default
// themes are taken into account.
// Check each theme whether it contains the searched block
if (isset($this->themes[$cacheKey])) {
for ($i = count($this->themes[$cacheKey]) - 1; $i >= 0; --$i) {
if ($this->loadResourceFromTheme($cacheKey, $blockName, $this->themes[$cacheKey][$i])) {
return true;
}
}
}
// Check the default themes once we reach the root form without success
if (!$view->parent) {
for ($i = count($this->defaultThemes) - 1; $i >= 0; --$i) {
if ($this->loadResourceFromTheme($cacheKey, $blockName, $this->defaultThemes[$i])) {
return true;
}
}
}
// If we did not find anything in the themes of the current view, proceed
// with the themes of the parent view
if ($view->parent) {
$parentCacheKey = $view->parent->vars[self::CACHE_KEY_VAR];
if (!isset($this->resources[$parentCacheKey][$blockName])) {
$this->loadResourceForBlockName($parentCacheKey, $view->parent, $blockName);
}
// If a template exists in the parent themes, cache that template
// for the current theme as well to speed up further accesses
if ($this->resources[$parentCacheKey][$blockName]) {
$this->resources[$cacheKey][$blockName] = $this->resources[$parentCacheKey][$blockName];
return true;
}
}
// Cache that we didn't find anything to speed up further accesses
$this->resources[$cacheKey][$blockName] = false;
return false;
}
/**
* Tries to load the resource for a block from a theme.
*
* @param string $cacheKey The cache key for storing the resource
* @param string $blockName The name of the block to load a resource for
* @param mixed $theme The theme to load the block from
*
* @return bool True if the resource could be loaded, false otherwise
*/
protected function loadResourceFromTheme($cacheKey, $blockName, $theme)
{
if ($this->engine->exists($templateName = $theme.':'.$blockName.'.html.php')) {
$this->resources[$cacheKey][$blockName] = $templateName;
return true;
}
return false;
}
}

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
{
const NOT_SYNCHRONIZED_ERROR = '1dafa156-89e1-4736-b832-419c2e501fca';
const NO_SUCH_FIELD_ERROR = '6e5212ed-a197-4339-99aa-5654798a4854';
protected static $errorNames = array(
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,176 @@
<?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\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
{
/**
* {@inheritdoc}
*/
public function validate($form, Constraint $constraint)
{
if (!$constraint instanceof Form) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Form');
}
if (!$form instanceof FormInterface) {
return;
}
/* @var FormInterface $form */
$config = $form->getConfig();
$validator = $this->context->getValidator()->inContext($this->context);
if ($form->isSynchronized()) {
// Validate the form data only if transformation succeeded
$groups = self::getValidationGroups($form);
$data = $form->getData();
// Validate the data against its own constraints
if ($form->isRoot() && (is_object($data) || is_array($data))) {
if (is_array($groups) && count($groups) > 0 || $groups instanceof GroupSequence && count($groups->groups) > 0) {
$validator->atPath('data')->validate($form->getData(), null, $groups);
}
}
// Validate the data against the constraints defined
// in the form
$constraints = $config->getOption('constraints', array());
foreach ($constraints as $constraint) {
// For the "Valid" constraint, validate the data in all groups
if ($constraint instanceof Valid) {
$validator->atPath('data')->validate($form->getData(), $constraint, $groups);
continue;
}
// Otherwise validate a constraint only once for the first
// matching group
foreach ($groups as $group) {
if (in_array($group, $constraint->groups)) {
$validator->atPath('data')->validate($form->getData(), $constraint, $group);
// Prevent duplicate validation
continue 2;
}
}
}
} else {
$childrenSynchronized = true;
foreach ($form as $child) {
if (!$child->isSynchronized()) {
$childrenSynchronized = false;
break;
}
}
// 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()
: gettype($form->getViewData());
$this->context->buildViolation($config->getOption('invalid_message'))
->setParameters(array_replace(array('{{ value }}' => $clientDataAsString), $config->getOption('invalid_message_parameters')))
->setInvalidValue($form->getViewData())
->setCode(Form::NOT_SYNCHRONIZED_ERROR)
->setCause($form->getTransformationFailure())
->addViolation();
}
}
// Mark the form with an error if it contains extra fields
if (!$config->getOption('allow_extra_fields') && count($form->getExtraData()) > 0) {
$this->context->buildViolation($config->getOption('extra_fields_message'))
->setParameter('{{ extra_fields }}', implode('", "', array_keys($form->getExtraData())))
->setInvalidValue($form->getExtraData())
->setCode(Form::NO_SUCH_FIELD_ERROR)
->addViolation();
}
}
/**
* Returns the validation groups of the given form.
*
* @param FormInterface $form The form
*
* @return array The validation groups
*/
private static 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);
}
$form = $form->getParent();
} while (null !== $form);
return array(Constraint::DEFAULT_GROUP);
}
/**
* Post-processes the validation groups option for a given form.
*
* @param array|callable $groups The validation groups
* @param FormInterface $form The validated form
*
* @return array The validation groups
*/
private static function resolveValidationGroups($groups, FormInterface $form)
{
if (!is_string($groups) && is_callable($groups)) {
$groups = call_user_func($groups, $form);
}
if ($groups instanceof GroupSequence) {
return $groups;
}
return (array) $groups;
}
}

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\Validator\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ValidationListener implements EventSubscriberInterface
{
private $validator;
private $violationMapper;
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(FormEvents::POST_SUBMIT => 'validateForm');
}
public function __construct(ValidatorInterface $validator, ViolationMapperInterface $violationMapper)
{
$this->validator = $validator;
$this->violationMapper = $violationMapper;
}
/**
* Validates the form and its domain object.
*
* @param FormEvent $event The event object
*/
public function validateForm(FormEvent $event)
{
$form = $event->getForm();
if ($form->isRoot()) {
// Validate the form in group "Default"
$violations = $this->validator->validate($form);
foreach ($violations as $violation) {
// Allow the "invalid" constraint to be put onto
// non-synchronized forms
// ConstraintViolation::getConstraint() must not expect to provide a constraint as long as Symfony\Component\Validator\ExecutionContext exists (before 3.0)
$allowNonSynchronized = (null === $violation->getConstraint() || $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 array();
}
if (empty($groups)) {
return;
}
if (is_callable($groups)) {
return $groups;
}
if ($groups instanceof GroupSequence) {
return $groups;
}
return (array) $groups;
};
$resolver->setDefaults(array(
'validation_groups' => null,
));
$resolver->setNormalizer('validation_groups', $validationGroupsNormalizer);
}
}

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\Validator\Type;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormTypeValidatorExtension extends BaseValidatorExtension
{
/**
* @var ValidatorInterface
*/
private $validator;
/**
* @var ViolationMapper
*/
private $violationMapper;
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
$this->violationMapper = new ViolationMapper();
}
/**
* {@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) ? array($constraints) : (array) $constraints;
};
$resolver->setDefaults(array(
'error_mapping' => array(),
'constraints' => array(),
'invalid_message' => 'This value is not valid.',
'invalid_message_parameters' => array(),
'allow_extra_fields' => false,
'extra_fields_message' => 'This form should not contain extra fields.',
));
$resolver->setNormalizer('constraints', $constraintsNormalizer);
}
/**
* {@inheritdoc}
*/
public function getExtendedType()
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
}
}

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\Validator\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RepeatedTypeValidatorExtension extends AbstractTypeExtension
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
// Map errors to the first field
$errorMapping = function (Options $options) {
return array('.' => $options['first_name']);
};
$resolver->setDefaults(array(
'error_mapping' => $errorMapping,
));
}
/**
* {@inheritdoc}
*/
public function getExtendedType()
{
return 'Symfony\Component\Form\Extension\Core\Type\RepeatedType';
}
}

View File

@@ -0,0 +1,26 @@
<?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;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class SubmitTypeValidatorExtension extends BaseValidatorExtension
{
/**
* {@inheritdoc}
*/
public function getExtendedType()
{
return 'Symfony\Component\Form\Extension\Core\Type\SubmitType';
}
}

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\Translation\TranslatorInterface;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
* @author David Badura <d.a.badura@gmail.com>
*/
class UploadValidatorExtension extends AbstractTypeExtension
{
private $translator;
private $translationDomain;
/**
* @param TranslatorInterface $translator The translator for translating error messages
* @param null|string $translationDomain The translation domain for translating
*/
public function __construct(TranslatorInterface $translator, $translationDomain = null)
{
$this->translator = $translator;
$this->translationDomain = $translationDomain;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$translator = $this->translator;
$translationDomain = $this->translationDomain;
$resolver->setNormalizer('upload_max_size_message', function (Options $options, $message) use ($translator, $translationDomain) {
return function () use ($translator, $translationDomain, $message) {
return $translator->trans(call_user_func($message), array(), $translationDomain);
};
});
}
/**
* {@inheritdoc}
*/
public function getExtendedType()
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Validator\Util;
use Symfony\Component\Form\Util\ServerParams as BaseServerParams;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ServerParams extends BaseServerParams
{
}

View File

@@ -0,0 +1,58 @@
<?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;
use Symfony\Component\Form\Extension\Validator\Constraints\Form;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* Extension supporting the Symfony Validator component in forms.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ValidatorExtension extends AbstractExtension
{
private $validator;
public function __construct(ValidatorInterface $validator)
{
$metadata = $validator->getMetadataFor('Symfony\Component\Form\Form');
// Register the form constraints in the validator programmatically.
// This functionality is required when using the Form component without
// the DIC, where the XML file is loaded automatically. Thus the following
// code must be kept synchronized with validation.xml
/* @var $metadata ClassMetadata */
$metadata->addConstraint(new Form());
$metadata->addPropertyConstraint('children', new Valid());
$this->validator = $validator;
}
public function loadTypeGuesser()
{
return new ValidatorTypeGuesser($this->validator);
}
protected function loadTypeExtensions()
{
return array(
new Type\FormTypeValidatorExtension($this->validator),
new Type\RepeatedTypeValidatorExtension(),
new Type\SubmitTypeValidatorExtension(),
);
}
}

View File

@@ -0,0 +1,286 @@
<?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;
use Symfony\Component\Form\FormTypeGuesserInterface;
use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\TypeGuess;
use Symfony\Component\Form\Guess\ValueGuess;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
class ValidatorTypeGuesser implements FormTypeGuesserInterface
{
private $metadataFactory;
public function __construct(MetadataFactoryInterface $metadataFactory)
{
$this->metadataFactory = $metadataFactory;
}
/**
* {@inheritdoc}
*/
public function guessType($class, $property)
{
return $this->guess($class, $property, function (Constraint $constraint) {
return $this->guessTypeForConstraint($constraint);
});
}
/**
* {@inheritdoc}
*/
public function guessRequired($class, $property)
{
return $this->guess($class, $property, function (Constraint $constraint) {
return $this->guessRequiredForConstraint($constraint);
// If we don't find any constraint telling otherwise, we can assume
// that a field is not required (with LOW_CONFIDENCE)
}, false);
}
/**
* {@inheritdoc}
*/
public function guessMaxLength($class, $property)
{
return $this->guess($class, $property, function (Constraint $constraint) {
return $this->guessMaxLengthForConstraint($constraint);
});
}
/**
* {@inheritdoc}
*/
public function guessPattern($class, $property)
{
return $this->guess($class, $property, function (Constraint $constraint) {
return $this->guessPatternForConstraint($constraint);
});
}
/**
* Guesses a field class name for a given constraint.
*
* @param Constraint $constraint The constraint to guess for
*
* @return TypeGuess|null The guessed field class and options
*/
public function guessTypeForConstraint(Constraint $constraint)
{
switch (get_class($constraint)) {
case 'Symfony\Component\Validator\Constraints\Type':
switch ($constraint->type) {
case 'array':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CollectionType', array(), Guess::MEDIUM_CONFIDENCE);
case 'boolean':
case 'bool':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CheckboxType', array(), Guess::MEDIUM_CONFIDENCE);
case 'double':
case 'float':
case 'numeric':
case 'real':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', array(), Guess::MEDIUM_CONFIDENCE);
case 'integer':
case 'int':
case 'long':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\IntegerType', array(), Guess::MEDIUM_CONFIDENCE);
case '\DateTime':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', array(), Guess::MEDIUM_CONFIDENCE);
case 'string':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', array(), Guess::LOW_CONFIDENCE);
}
break;
case 'Symfony\Component\Validator\Constraints\Country':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CountryType', array(), Guess::HIGH_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\Currency':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CurrencyType', array(), Guess::HIGH_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\Date':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', array('input' => 'string'), Guess::HIGH_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\DateTime':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', array('input' => 'string'), Guess::HIGH_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\Email':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\EmailType', array(), Guess::HIGH_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\File':
case 'Symfony\Component\Validator\Constraints\Image':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\FileType', array(), Guess::HIGH_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\Language':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\LanguageType', array(), Guess::HIGH_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\Locale':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\LocaleType', array(), Guess::HIGH_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\Time':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', array('input' => 'string'), Guess::HIGH_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\Url':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\UrlType', array(), Guess::HIGH_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\Ip':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', array(), Guess::MEDIUM_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\Length':
case 'Symfony\Component\Validator\Constraints\Regex':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', array(), Guess::LOW_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\Range':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', array(), Guess::LOW_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\Count':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CollectionType', array(), Guess::LOW_CONFIDENCE);
case 'Symfony\Component\Validator\Constraints\True':
case 'Symfony\Component\Validator\Constraints\False':
case 'Symfony\Component\Validator\Constraints\IsTrue':
case 'Symfony\Component\Validator\Constraints\IsFalse':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CheckboxType', array(), Guess::MEDIUM_CONFIDENCE);
}
}
/**
* Guesses whether a field is required based on the given constraint.
*
* @param Constraint $constraint The constraint to guess for
*
* @return ValueGuess|null The guess whether the field is required
*/
public function guessRequiredForConstraint(Constraint $constraint)
{
switch (get_class($constraint)) {
case 'Symfony\Component\Validator\Constraints\NotNull':
case 'Symfony\Component\Validator\Constraints\NotBlank':
case 'Symfony\Component\Validator\Constraints\True':
case 'Symfony\Component\Validator\Constraints\IsTrue':
return new ValueGuess(true, Guess::HIGH_CONFIDENCE);
}
}
/**
* Guesses a field's maximum length based on the given constraint.
*
* @param Constraint $constraint The constraint to guess for
*
* @return ValueGuess|null The guess for the maximum length
*/
public function guessMaxLengthForConstraint(Constraint $constraint)
{
switch (get_class($constraint)) {
case 'Symfony\Component\Validator\Constraints\Length':
if (is_numeric($constraint->max)) {
return new ValueGuess($constraint->max, Guess::HIGH_CONFIDENCE);
}
break;
case 'Symfony\Component\Validator\Constraints\Type':
if (in_array($constraint->type, array('double', 'float', 'numeric', 'real'))) {
return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
}
break;
case 'Symfony\Component\Validator\Constraints\Range':
if (is_numeric($constraint->max)) {
return new ValueGuess(strlen((string) $constraint->max), Guess::LOW_CONFIDENCE);
}
break;
}
}
/**
* Guesses a field's pattern based on the given constraint.
*
* @param Constraint $constraint The constraint to guess for
*
* @return ValueGuess|null The guess for the pattern
*/
public function guessPatternForConstraint(Constraint $constraint)
{
switch (get_class($constraint)) {
case 'Symfony\Component\Validator\Constraints\Length':
if (is_numeric($constraint->min)) {
return new ValueGuess(sprintf('.{%s,}', (string) $constraint->min), Guess::LOW_CONFIDENCE);
}
break;
case 'Symfony\Component\Validator\Constraints\Regex':
$htmlPattern = $constraint->getHtmlPattern();
if (null !== $htmlPattern) {
return new ValueGuess($htmlPattern, Guess::HIGH_CONFIDENCE);
}
break;
case 'Symfony\Component\Validator\Constraints\Range':
if (is_numeric($constraint->min)) {
return new ValueGuess(sprintf('.{%s,}', strlen((string) $constraint->min)), Guess::LOW_CONFIDENCE);
}
break;
case 'Symfony\Component\Validator\Constraints\Type':
if (in_array($constraint->type, array('double', 'float', 'numeric', 'real'))) {
return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
}
break;
}
}
/**
* Iterates over the constraints of a property, executes a constraints on
* them and returns the best guess.
*
* @param string $class The class to read the constraints from
* @param string $property The property for which to find constraints
* @param \Closure $closure The closure that returns a guess
* for a given constraint
* @param mixed $defaultValue The default value assumed if no other value
* can be guessed.
*
* @return Guess|null The guessed value with the highest confidence
*/
protected function guess($class, $property, \Closure $closure, $defaultValue = null)
{
$guesses = array();
$classMetadata = $this->metadataFactory->getMetadataFor($class);
if ($classMetadata instanceof ClassMetadataInterface && $classMetadata->hasPropertyMetadata($property)) {
$memberMetadatas = $classMetadata->getPropertyMetadata($property);
foreach ($memberMetadatas as $memberMetadata) {
$constraints = $memberMetadata->getConstraints();
foreach ($constraints as $constraint) {
if ($guess = $closure($constraint)) {
$guesses[] = $guess;
}
}
}
}
if (null !== $defaultValue) {
$guesses[] = new ValueGuess($defaultValue, Guess::LOW_CONFIDENCE);
}
return Guess::getBestGuess($guesses);
}
}

View File

@@ -0,0 +1,104 @@
<?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\ViolationMapper;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Exception\ErrorMappingException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class MappingRule
{
/**
* @var FormInterface
*/
private $origin;
/**
* @var string
*/
private $propertyPath;
/**
* @var string
*/
private $targetPath;
public function __construct(FormInterface $origin, $propertyPath, $targetPath)
{
$this->origin = $origin;
$this->propertyPath = $propertyPath;
$this->targetPath = $targetPath;
}
/**
* @return FormInterface
*/
public function getOrigin()
{
return $this->origin;
}
/**
* Matches a property path against the rule path.
*
* If the rule matches, the form mapped by the rule is returned.
* Otherwise this method returns false.
*
* @param string $propertyPath The property path to match against the rule
*
* @return null|FormInterface The mapped form or null
*/
public function match($propertyPath)
{
if ($propertyPath === (string) $this->propertyPath) {
return $this->getTarget();
}
}
/**
* Matches a property path against a prefix of the rule path.
*
* @param string $propertyPath The property path to match against the rule
*
* @return bool Whether the property path is a prefix of the rule or not
*/
public function isPrefix($propertyPath)
{
$length = strlen($propertyPath);
$prefix = substr($this->propertyPath, 0, $length);
$next = isset($this->propertyPath[$length]) ? $this->propertyPath[$length] : null;
return $prefix === $propertyPath && ('[' === $next || '.' === $next);
}
/**
* @return FormInterface
*
* @throws ErrorMappingException
*/
public function getTarget()
{
$childNames = explode('.', $this->targetPath);
$target = $this->origin;
foreach ($childNames as $childName) {
if (!$target->has($childName)) {
throw new ErrorMappingException(sprintf('The child "%s" of "%s" mapped by the rule "%s" in "%s" does not exist.', $childName, $target->getName(), $this->targetPath, $this->origin->getName()));
}
$target = $target->get($childName);
}
return $target;
}
}

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\Validator\ViolationMapper;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\PropertyAccess\PropertyPath;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RelativePath extends PropertyPath
{
/**
* @var FormInterface
*/
private $root;
/**
* @param FormInterface $root
* @param string $propertyPath
*/
public function __construct(FormInterface $root, $propertyPath)
{
parent::__construct($propertyPath);
$this->root = $root;
}
/**
* @return FormInterface
*/
public function getRoot()
{
return $this->root;
}
}

View File

@@ -0,0 +1,283 @@
<?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\ViolationMapper;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Util\InheritDataAwareIterator;
use Symfony\Component\PropertyAccess\PropertyPathIterator;
use Symfony\Component\PropertyAccess\PropertyPathBuilder;
use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Validator\ConstraintViolation;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ViolationMapper implements ViolationMapperInterface
{
/**
* @var bool
*/
private $allowNonSynchronized;
/**
* {@inheritdoc}
*/
public function mapViolation(ConstraintViolation $violation, FormInterface $form, $allowNonSynchronized = false)
{
$this->allowNonSynchronized = $allowNonSynchronized;
// The scope is the currently found most specific form that
// an error should be mapped to. After setting the scope, the
// mapper will try to continue to find more specific matches in
// the children of scope. If it cannot, the error will be
// mapped to this scope.
$scope = null;
$violationPath = null;
$relativePath = null;
$match = false;
// Don't create a ViolationPath instance for empty property paths
if (strlen($violation->getPropertyPath()) > 0) {
$violationPath = new ViolationPath($violation->getPropertyPath());
$relativePath = $this->reconstructPath($violationPath, $form);
}
// This case happens if the violation path is empty and thus
// the violation should be mapped to the root form
if (null === $violationPath) {
$scope = $form;
}
// In general, mapping happens from the root form to the leaf forms
// First, the rules of the root form are applied to determine
// the subsequent descendant. The rules of this descendant are then
// applied to find the next and so on, until we have found the
// most specific form that matches the violation.
// If any of the forms found in this process is not synchronized,
// mapping is aborted. Non-synchronized forms could not reverse
// transform the value entered by the user, thus any further violations
// caused by the (invalid) reverse transformed value should be
// ignored.
if (null !== $relativePath) {
// Set the scope to the root of the relative path
// This root will usually be $form. If the path contains
// an unmapped form though, the last unmapped form found
// will be the root of the path.
$scope = $relativePath->getRoot();
$it = new PropertyPathIterator($relativePath);
while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) {
$scope = $child;
$it->next();
$match = true;
}
}
// This case happens if an error happened in the data under a
// form inheriting its parent data that does not match any of the
// children of that form.
if (null !== $violationPath && !$match) {
// If we could not map the error to anything more specific
// than the root element, map it to the innermost directly
// mapped form of the violation path
// e.g. "children[foo].children[bar].data.baz"
// Here the innermost directly mapped child is "bar"
$scope = $form;
$it = new ViolationPathIterator($violationPath);
// Note: acceptsErrors() will always return true for forms inheriting
// their parent data, because these forms can never be non-synchronized
// (they don't do any data transformation on their own)
while ($this->acceptsErrors($scope) && $it->valid() && $it->mapsForm()) {
if (!$scope->has($it->current())) {
// Break if we find a reference to a non-existing child
break;
}
$scope = $scope->get($it->current());
$it->next();
}
}
// Follow dot rules until we have the final target
$mapping = $scope->getConfig()->getOption('error_mapping');
while ($this->acceptsErrors($scope) && isset($mapping['.'])) {
$dotRule = new MappingRule($scope, '.', $mapping['.']);
$scope = $dotRule->getTarget();
$mapping = $scope->getConfig()->getOption('error_mapping');
}
// Only add the error if the form is synchronized
if ($this->acceptsErrors($scope)) {
$scope->addError(new FormError(
$violation->getMessage(),
$violation->getMessageTemplate(),
$violation->getParameters(),
$violation->getPlural(),
$violation
));
}
}
/**
* Tries to match the beginning of the property path at the
* current position against the children of the scope.
*
* If a matching child is found, it is returned. Otherwise
* null is returned.
*
* @param FormInterface $form The form to search
* @param PropertyPathIteratorInterface $it The iterator at its current position
*
* @return null|FormInterface The found match or null
*/
private function matchChild(FormInterface $form, PropertyPathIteratorInterface $it)
{
$target = null;
$chunk = '';
$foundAtIndex = null;
// Construct mapping rules for the given form
$rules = array();
foreach ($form->getConfig()->getOption('error_mapping') as $propertyPath => $targetPath) {
// Dot rules are considered at the very end
if ('.' !== $propertyPath) {
$rules[] = new MappingRule($form, $propertyPath, $targetPath);
}
}
$children = iterator_to_array(new \RecursiveIteratorIterator(new InheritDataAwareIterator($form)), false);
while ($it->valid()) {
if ($it->isIndex()) {
$chunk .= '['.$it->current().']';
} else {
$chunk .= ('' === $chunk ? '' : '.').$it->current();
}
// Test mapping rules as long as we have any
foreach ($rules as $key => $rule) {
/* @var MappingRule $rule */
// Mapping rule matches completely, terminate.
if (null !== ($form = $rule->match($chunk))) {
return $form;
}
// Keep only rules that have $chunk as prefix
if (!$rule->isPrefix($chunk)) {
unset($rules[$key]);
}
}
/** @var FormInterface $child */
foreach ($children as $i => $child) {
$childPath = (string) $child->getPropertyPath();
if ($childPath === $chunk) {
$target = $child;
$foundAtIndex = $it->key();
} elseif (0 === strpos($childPath, $chunk)) {
continue;
}
unset($children[$i]);
}
$it->next();
}
if (null !== $foundAtIndex) {
$it->seek($foundAtIndex);
}
return $target;
}
/**
* Reconstructs a property path from a violation path and a form tree.
*
* @param ViolationPath $violationPath The violation path
* @param FormInterface $origin The root form of the tree
*
* @return RelativePath The reconstructed path
*/
private function reconstructPath(ViolationPath $violationPath, FormInterface $origin)
{
$propertyPathBuilder = new PropertyPathBuilder($violationPath);
$it = $violationPath->getIterator();
$scope = $origin;
// Remember the current index in the builder
$i = 0;
// Expand elements that map to a form (like "children[address]")
for ($it->rewind(); $it->valid() && $it->mapsForm(); $it->next()) {
if (!$scope->has($it->current())) {
// Scope relates to a form that does not exist
// Bail out
break;
}
// Process child form
$scope = $scope->get($it->current());
if ($scope->getConfig()->getInheritData()) {
// Form inherits its parent data
// Cut the piece out of the property path and proceed
$propertyPathBuilder->remove($i);
} elseif (!$scope->getConfig()->getMapped()) {
// Form is not mapped
// Set the form as new origin and strip everything
// we have so far in the path
$origin = $scope;
$propertyPathBuilder->remove(0, $i + 1);
$i = 0;
} else {
/* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */
$propertyPath = $scope->getPropertyPath();
if (null === $propertyPath) {
// Property path of a mapped form is null
// Should not happen, bail out
break;
}
$propertyPathBuilder->replace($i, 1, $propertyPath);
$i += $propertyPath->getLength();
}
}
$finalPath = $propertyPathBuilder->getPropertyPath();
return null !== $finalPath ? new RelativePath($origin, $finalPath) : null;
}
/**
* @param FormInterface $form
*
* @return bool
*/
private function acceptsErrors(FormInterface $form)
{
// Ignore non-submitted forms. This happens, for example, in PATCH
// requests.
// https://github.com/symfony/symfony/pull/10567
return $form->isSubmitted() && ($this->allowNonSynchronized || $form->isSynchronized());
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Validator\ConstraintViolation;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ViolationMapperInterface
{
/**
* Maps a constraint violation to a form in the form tree under
* the given form.
*
* @param ConstraintViolation $violation The violation to map
* @param FormInterface $form The root form of the tree
* to map it to.
* @param bool $allowNonSynchronized Whether to allow
* mapping to non-synchronized forms.
*/
public function mapViolation(ConstraintViolation $violation, FormInterface $form, $allowNonSynchronized = false);
}

View File

@@ -0,0 +1,258 @@
<?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\ViolationMapper;
use Symfony\Component\Form\Exception\OutOfBoundsException;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ViolationPath implements \IteratorAggregate, PropertyPathInterface
{
/**
* @var array
*/
private $elements = array();
/**
* @var array
*/
private $isIndex = array();
/**
* @var array
*/
private $mapsForm = array();
/**
* @var string
*/
private $pathAsString = '';
/**
* @var int
*/
private $length = 0;
/**
* Creates a new violation path from a string.
*
* @param string $violationPath The property path of a {@link \Symfony\Component\Validator\ConstraintViolation}
* object.
*/
public function __construct($violationPath)
{
$path = new PropertyPath($violationPath);
$elements = $path->getElements();
$data = false;
for ($i = 0, $l = count($elements); $i < $l; ++$i) {
if (!$data) {
// The element "data" has not yet been passed
if ('children' === $elements[$i] && $path->isProperty($i)) {
// Skip element "children"
++$i;
// Next element must exist and must be an index
// Otherwise consider this the end of the path
if ($i >= $l || !$path->isIndex($i)) {
break;
}
// All the following index items (regardless if .children is
// explicitly used) are children and grand-children
for (; $i < $l && $path->isIndex($i); ++$i) {
$this->elements[] = $elements[$i];
$this->isIndex[] = true;
$this->mapsForm[] = true;
}
// Rewind the pointer as the last element above didn't match
// (even if the pointer was moved forward)
--$i;
} elseif ('data' === $elements[$i] && $path->isProperty($i)) {
// Skip element "data"
++$i;
// End of path
if ($i >= $l) {
break;
}
$this->elements[] = $elements[$i];
$this->isIndex[] = $path->isIndex($i);
$this->mapsForm[] = false;
$data = true;
} else {
// Neither "children" nor "data" property found
// Consider this the end of the path
break;
}
} else {
// Already after the "data" element
// Pick everything as is
$this->elements[] = $elements[$i];
$this->isIndex[] = $path->isIndex($i);
$this->mapsForm[] = false;
}
}
$this->length = count($this->elements);
$this->buildString();
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return $this->pathAsString;
}
/**
* {@inheritdoc}
*/
public function getLength()
{
return $this->length;
}
/**
* {@inheritdoc}
*/
public function getParent()
{
if ($this->length <= 1) {
return;
}
$parent = clone $this;
--$parent->length;
array_pop($parent->elements);
array_pop($parent->isIndex);
array_pop($parent->mapsForm);
$parent->buildString();
return $parent;
}
/**
* {@inheritdoc}
*/
public function getElements()
{
return $this->elements;
}
/**
* {@inheritdoc}
*/
public function getElement($index)
{
if (!isset($this->elements[$index])) {
throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index));
}
return $this->elements[$index];
}
/**
* {@inheritdoc}
*/
public function isProperty($index)
{
if (!isset($this->isIndex[$index])) {
throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index));
}
return !$this->isIndex[$index];
}
/**
* {@inheritdoc}
*/
public function isIndex($index)
{
if (!isset($this->isIndex[$index])) {
throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index));
}
return $this->isIndex[$index];
}
/**
* Returns whether an element maps directly to a form.
*
* Consider the following violation path:
*
* <code>
* children[address].children[office].data.street
* </code>
*
* In this example, "address" and "office" map to forms, while
* "street does not.
*
* @param int $index The element index
*
* @return bool Whether the element maps to a form
*
* @throws OutOfBoundsException If the offset is invalid.
*/
public function mapsForm($index)
{
if (!isset($this->mapsForm[$index])) {
throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index));
}
return $this->mapsForm[$index];
}
/**
* Returns a new iterator for this path.
*
* @return ViolationPathIterator
*/
public function getIterator()
{
return new ViolationPathIterator($this);
}
/**
* Builds the string representation from the elements.
*/
private function buildString()
{
$this->pathAsString = '';
$data = false;
foreach ($this->elements as $index => $element) {
if ($this->mapsForm[$index]) {
$this->pathAsString .= ".children[$element]";
} elseif (!$data) {
$this->pathAsString .= '.data'.($this->isIndex[$index] ? "[$element]" : ".$element");
$data = true;
} else {
$this->pathAsString .= $this->isIndex[$index] ? "[$element]" : ".$element";
}
}
if ('' !== $this->pathAsString) {
// remove leading dot
$this->pathAsString = substr($this->pathAsString, 1);
}
}
}

View File

@@ -0,0 +1,30 @@
<?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\ViolationMapper;
use Symfony\Component\PropertyAccess\PropertyPathIterator;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ViolationPathIterator extends PropertyPathIterator
{
public function __construct(ViolationPath $violationPath)
{
parent::__construct($violationPath);
}
public function mapsForm()
{
return $this->path->mapsForm($this->key());
}
}