diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig index ef2035ab43f0..8eba307890d9 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig @@ -87,6 +87,25 @@ {% endif %} {%- endblock time_widget %} +{% block dateinterval_widget %} + {% if widget == 'single_text' %} + {{- block('form_widget_simple') -}} + {% else %} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) %} +
+ {{ form_errors(form) }} + {% if with_years %}{{ form_widget(form.years) }}{% endif %} + {% if with_months %}{{ form_widget(form.months) }}{% endif %} + {% if with_weeks %}{{ form_widget(form.weeks) }}{% endif %} + {% if with_days %}{{ form_widget(form.days) }}{% endif %} + {% if with_hours %}{{ form_widget(form.hours) }}{% endif %} + {% if with_minutes %}{{ form_widget(form.minutes) }}{% endif %} + {% if with_seconds %}{{ form_widget(form.seconds) }}{% endif %} + {% if with_invert %}{{ form_widget(form.invert) }}{% endif %} +
+ {% endif %} +{% endblock dateinterval_widget %} + {% block choice_widget_collapsed -%} {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %} {{- parent() -}} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 63104def7d3e..cac4077a4368 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -131,6 +131,24 @@ {%- endif -%} {%- endblock time_widget -%} +{% block dateinterval_widget %} + {% if widget == 'single_text' %} + {{- block('form_widget_simple') -}} + {% else %} +
+ {{ form_errors(form) }} + {% if with_years %}{{ form_widget(form.years) }}{% endif %} + {% if with_months %}{{ form_widget(form.months) }}{% endif %} + {% if with_weeks %}{{ form_widget(form.weeks) }}{% endif %} + {% if with_days %}{{ form_widget(form.days) }}{% endif %} + {% if with_hours %}{{ form_widget(form.hours) }}{% endif %} + {% if with_minutes %}{{ form_widget(form.minutes) }}{% endif %} + {% if with_seconds %}{{ form_widget(form.seconds) }}{% endif %} + {% if with_invert %}{{ form_widget(form.invert) }}{% endif %} +
+ {% endif %} +{% endblock dateinterval_widget %} + {%- block number_widget -%} {# type="number" doesn't work with floats #} {%- set type = type|default('text') -%} diff --git a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php index 96e6c1961a80..156d5568801a 100644 --- a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php +++ b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php @@ -51,6 +51,7 @@ protected function loadTypes() new Type\ChoiceType($this->choiceListFactory), new Type\CollectionType(), new Type\CountryType(), + new Type\DateIntervalType(), new Type\DateType(), new Type\DateTimeType(), new Type\EmailType(), diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php new file mode 100644 index 000000000000..c7a4f692533f --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateIntervalToArrayTransformer.php @@ -0,0 +1,173 @@ + + * + * For 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 + */ +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; + + /** + * @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; + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php new file mode 100644 index 000000000000..7b9cca0fbd15 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateIntervalToStringTransformer.php @@ -0,0 +1,104 @@ + + * + * For 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateIntervalType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateIntervalType.php new file mode 100644 index 000000000000..fb2b7d7ff3ec --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateIntervalType.php @@ -0,0 +1,273 @@ + + * + * For 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 + */ +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(); + $childOptions[$part]['error_bubbling'] = true; + if ('choice' === $options['widget']) { + $childOptions[$part]['choices'] = $options[$part]; + $childOptions[$part]['placeholder'] = $options['placeholder'][$part]; + } + } + } + $invertOptions = array( + 'error_bubbling' => true, + ); + // 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]; + } + } + if ($options['with_invert']) { + $invertOptions[$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', $invertOptions); + } + $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); + }; + + $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, + ) + ); + $resolver->setNormalizer('placeholder', $placeholderNormalizer); + + $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'); + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix() + { + return 'dateinterval'; + } +} diff --git a/src/Symfony/Component/Form/Test/TypeTestCase.php b/src/Symfony/Component/Form/Test/TypeTestCase.php index 87797757ff3d..9e0b2cf8677c 100644 --- a/src/Symfony/Component/Form/Test/TypeTestCase.php +++ b/src/Symfony/Component/Form/Test/TypeTestCase.php @@ -38,4 +38,9 @@ public static function assertDateTimeEquals(\DateTime $expected, \DateTime $actu { self::assertEquals($expected->format('c'), $actual->format('c')); } + + public static function assertDateIntervalEquals(\DateInterval $expected, \DateInterval $actual) + { + self::assertEquals($expected->format('%RP%yY%mM%dDT%hH%iM%sS'), $actual->format('%RP%yY%mM%dDT%hH%iM%sS')); + } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateIntervalTestCase.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateIntervalTestCase.php new file mode 100644 index 000000000000..f65e79f262ae --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateIntervalTestCase.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer; + +abstract class DateIntervalTestCase extends \PHPUnit_Framework_TestCase +{ + public static function assertDateIntervalEquals(\DateInterval $expected, \DateInterval $actual) + { + self::assertEquals($expected->format('%RP%yY%mM%dDT%hH%iM%sS'), $actual->format('%RP%yY%mM%dDT%hH%iM%sS')); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateIntervalToArrayTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateIntervalToArrayTransformerTest.php new file mode 100644 index 000000000000..488ea3c06bec --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateIntervalToArrayTransformerTest.php @@ -0,0 +1,263 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer; + +use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToArrayTransformer; + +/** + * @author Steffen Roßkamp + */ +class DateIntervalToArrayTransformerTest extends DateIntervalTestCase +{ + public function testTransform() + { + $transformer = new DateIntervalToArrayTransformer(); + $input = new \DateInterval('P1Y2M3DT4H5M6S'); + $output = array( + 'years' => '1', + 'months' => '2', + 'days' => '3', + 'hours' => '4', + 'minutes' => '5', + 'seconds' => '6', + 'invert' => false, + ); + $this->assertSame($output, $transformer->transform($input)); + } + + public function testTransformEmpty() + { + $transformer = new DateIntervalToArrayTransformer(); + $output = array( + 'years' => '', + 'months' => '', + 'days' => '', + 'hours' => '', + 'minutes' => '', + 'seconds' => '', + 'invert' => false, + ); + $this->assertSame($output, $transformer->transform(null)); + } + + public function testTransformEmptyWithFields() + { + $transformer = new DateIntervalToArrayTransformer(array('years', 'weeks', 'minutes', 'seconds')); + $output = array( + 'years' => '', + 'weeks' => '', + 'minutes' => '', + 'seconds' => '', + ); + $this->assertSame($output, $transformer->transform(null)); + } + + public function testTransformWithFields() + { + $transformer = new DateIntervalToArrayTransformer(array('years', 'minutes', 'seconds')); + $input = new \DateInterval('P1Y2M3DT4H5M6S'); + $output = array( + 'years' => '1', + 'minutes' => '5', + 'seconds' => '6', + ); + $this->assertSame($output, $transformer->transform($input)); + } + + public function testTransformWithWeek() + { + $transformer = new DateIntervalToArrayTransformer(array('weeks', 'minutes', 'seconds')); + $input = new \DateInterval('P1Y2M3WT4H5M6S'); + $output = array( + 'weeks' => '3', + 'minutes' => '5', + 'seconds' => '6', + ); + $input = $transformer->transform($input); + ksort($input); + ksort($output); + $this->assertSame($output, $input); + } + + public function testTransformDaysToWeeks() + { + $transformer = new DateIntervalToArrayTransformer(array('weeks', 'minutes', 'seconds')); + $input = new \DateInterval('P1Y2M23DT4H5M6S'); + $output = array( + 'weeks' => '3', + 'minutes' => '5', + 'seconds' => '6', + ); + $input = $transformer->transform($input); + ksort($input); + ksort($output); + $this->assertSame($output, $input); + } + + public function testTransformDaysNotOverflowingToWeeks() + { + $transformer = new DateIntervalToArrayTransformer(array('days', 'minutes', 'seconds')); + $input = new \DateInterval('P1Y2M23DT4H5M6S'); + $output = array( + 'days' => '23', + 'minutes' => '5', + 'seconds' => '6', + ); + $this->assertSame($output, $transformer->transform($input)); + } + + public function testTransformWithInvert() + { + $transformer = new DateIntervalToArrayTransformer(array('years', 'invert')); + $input = new \DateInterval('P1Y'); + $input->invert = 1; + $output = array( + 'years' => '1', + 'invert' => true, + ); + $this->assertSame($output, $transformer->transform($input)); + } + + public function testTransformWithPadding() + { + $transformer = new DateIntervalToArrayTransformer(null, true); + $input = new \DateInterval('P1Y2M3DT4H5M6S'); + $output = array( + 'years' => '01', + 'months' => '02', + 'days' => '03', + 'hours' => '04', + 'minutes' => '05', + 'seconds' => '06', + 'invert' => false, + ); + $this->assertSame($output, $transformer->transform($input)); + } + + public function testTransformWithFieldsAndPadding() + { + $transformer = new DateIntervalToArrayTransformer(array('years', 'minutes', 'seconds'), true); + $input = new \DateInterval('P1Y2M3DT4H5M6S'); + $output = array( + 'years' => '01', + 'minutes' => '05', + 'seconds' => '06', + ); + $this->assertSame($output, $transformer->transform($input)); + } + + public function testReverseTransformRequiresDateTime() + { + $transformer = new DateIntervalToArrayTransformer(); + $this->assertNull($transformer->reverseTransform(null)); + $this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException'); + $transformer->reverseTransform('12345'); + } + + public function testReverseTransformWithUnsetFields() + { + $transformer = new DateIntervalToArrayTransformer(); + $input = array('years' => '1'); + $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException'); + $transformer->reverseTransform($input); + } + + public function testReverseTransformWithEmptyFields() + { + $transformer = new DateIntervalToArrayTransformer(array('years', 'minutes', 'seconds')); + $input = array( + 'years' => '1', + 'minutes' => '', + 'seconds' => '6', + ); + $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException', 'This amount of "minutes" is invalid'); + $transformer->reverseTransform($input); + } + + public function testReverseTransformWithWrongInvertType() + { + $transformer = new DateIntervalToArrayTransformer(array('invert')); + $input = array( + 'invert' => '1', + ); + $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException', 'The value of "invert" must be boolean'); + $transformer->reverseTransform($input); + } + + public function testReverseTransform() + { + $transformer = new DateIntervalToArrayTransformer(); + $input = array( + 'years' => '1', + 'months' => '2', + 'days' => '3', + 'hours' => '4', + 'minutes' => '5', + 'seconds' => '6', + 'invert' => false, + ); + $output = new \DateInterval('P01Y02M03DT04H05M06S'); + $this->assertDateIntervalEquals($output, $transformer->reverseTransform($input)); + } + + public function testReverseTransformWithWeek() + { + $transformer = new DateIntervalToArrayTransformer( + array('years', 'months', 'weeks', 'hours', 'minutes', 'seconds') + ); + $input = array( + 'years' => '1', + 'months' => '2', + 'weeks' => '3', + 'hours' => '4', + 'minutes' => '5', + 'seconds' => '6', + ); + $output = new \DateInterval('P1Y2M21DT4H5M6S'); + $this->assertDateIntervalEquals($output, $transformer->reverseTransform($input)); + } + + public function testReverseTransformWithFields() + { + $transformer = new DateIntervalToArrayTransformer(array('years', 'minutes', 'seconds')); + $input = array( + 'years' => '1', + 'minutes' => '5', + 'seconds' => '6', + ); + $output = new \DateInterval('P1Y0M0DT0H5M6S'); + $this->assertDateIntervalEquals($output, $transformer->reverseTransform($input)); + } + + public function testBothTransformsWithWeek() + { + $transformer = new DateIntervalToArrayTransformer( + array('years', 'months', 'weeks', 'hours', 'minutes', 'seconds') + ); + $interval = new \DateInterval('P1Y2M21DT4H5M6S'); + $array = array( + 'years' => '1', + 'months' => '2', + 'weeks' => '3', + 'hours' => '4', + 'minutes' => '5', + 'seconds' => '6', + ); + $input = $transformer->transform($interval); + ksort($input); + ksort($array); + $this->assertSame($array, $input); + $interval = new \DateInterval('P1Y2M0DT4H5M6S'); + $input['weeks'] = '0'; + $this->assertDateIntervalEquals($interval, $transformer->reverseTransform($input)); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateIntervalToStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateIntervalToStringTransformerTest.php new file mode 100644 index 000000000000..9815b70bff8f --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateIntervalToStringTransformerTest.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer; + +use Symfony\Component\Form\Extension\Core\DataTransformer\DateIntervalToStringTransformer; + +/** + * @author Steffen Roßkamp + */ +class DateIntervalToStringTransformerTest extends DateIntervalTestCase +{ + public function dataProviderISO() + { + $data = array( + array('P%YY%MM%DDT%HH%IM%SS', 'P00Y00M00DT00H00M00S', 'PT0S'), + array('P%yY%mM%dDT%hH%iM%sS', 'P0Y0M0DT0H0M0S', 'PT0S'), + array('P%yY%mM%dDT%hH%iM%sS', 'P10Y2M3DT16H5M6S', 'P10Y2M3DT16H5M6S'), + array('P%yY%mM%dDT%hH%iM', 'P10Y2M3DT16H5M', 'P10Y2M3DT16H5M'), + array('P%yY%mM%dDT%hH', 'P10Y2M3DT16H', 'P10Y2M3DT16H'), + array('P%yY%mM%dD', 'P10Y2M3D', 'P10Y2M3DT0H'), + ); + + return $data; + } + + public function dataProviderDate() + { + $data = array( + array( + '%y years %m months %d days %h hours %i minutes %s seconds', + '10 years 2 months 3 days 16 hours 5 minutes 6 seconds', + 'P10Y2M3DT16H5M6S', + ), + array( + '%y years %m months %d days %h hours %i minutes', + '10 years 2 months 3 days 16 hours 5 minutes', + 'P10Y2M3DT16H5M', + ), + array('%y years %m months %d days %h hours', '10 years 2 months 3 days 16 hours', 'P10Y2M3DT16H'), + array('%y years %m months %d days', '10 years 2 months 3 days', 'P10Y2M3D'), + array('%y years %m months', '10 years 2 months', 'P10Y2M'), + array('%y year', '1 year', 'P1Y'), + ); + + return $data; + } + + /** + * @dataProvider dataProviderISO + */ + public function testTransform($format, $output, $input) + { + $transformer = new DateIntervalToStringTransformer($format); + $input = new \DateInterval($input); + $this->assertEquals($output, $transformer->transform($input)); + } + + public function testTransformEmpty() + { + $transformer = new DateIntervalToStringTransformer(); + $this->assertSame('', $transformer->transform(null)); + } + + public function testTransformExpectsDateTime() + { + $transformer = new DateIntervalToStringTransformer(); + $this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException'); + $transformer->transform('1234'); + } + + /** + * @dataProvider dataProviderISO + */ + public function testReverseTransform($format, $input, $output) + { + $reverseTransformer = new DateIntervalToStringTransformer($format, true); + $interval = new \DateInterval($output); + $this->assertDateIntervalEquals($interval, $reverseTransformer->reverseTransform($input)); + } + + /** + * @dataProvider dataProviderDate + */ + public function testReverseTransformDateString($format, $input, $output) + { + $reverseTransformer = new DateIntervalToStringTransformer($format, true); + $interval = new \DateInterval($output); + $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException'); + $this->assertDateIntervalEquals($interval, $reverseTransformer->reverseTransform($input)); + } + + public function testReverseTransformEmpty() + { + $reverseTransformer = new DateIntervalToStringTransformer(); + $this->assertNull($reverseTransformer->reverseTransform('')); + } + + public function testReverseTransformExpectsString() + { + $reverseTransformer = new DateIntervalToStringTransformer(); + $this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException'); + $reverseTransformer->reverseTransform(1234); + } + + public function testReverseTransformExpectsValidIntervalString() + { + $reverseTransformer = new DateIntervalToStringTransformer(); + $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException'); + $reverseTransformer->reverseTransform('10Y'); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateIntervalTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateIntervalTypeTest.php new file mode 100644 index 000000000000..e40c0949f696 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateIntervalTypeTest.php @@ -0,0 +1,367 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Core\Type; + +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\Test\TypeTestCase as TestCase; + +class DateIntervalTypeTest extends TestCase +{ + public function testSubmitDateInterval() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'input' => 'dateinterval', + ) + ); + $form->submit( + array( + 'years' => '7', + 'months' => '6', + 'days' => '5', + ) + ); + $dateInterval = new \DateInterval('P7Y6M5D'); + $this->assertDateIntervalEquals($dateInterval, $form->getData()); + } + + public function testSubmitString() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'input' => 'string', + ) + ); + $form->submit( + array( + 'years' => '7', + 'months' => '6', + 'days' => '5', + ) + ); + $this->assertEquals('P7Y6M5D', $form->getData()); + } + + public function testSubmitArray() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'input' => 'array', + ) + ); + $form->submit( + array( + 'years' => '7', + 'months' => '6', + 'days' => '5', + ) + ); + $this->assertEquals(array('years' => '7', 'months' => '6', 'days' => '5'), $form->getData()); + } + + public function testSubmitWithoutMonths() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'input' => 'dateinterval', + 'with_months' => false, + ) + ); + $form->setData(new \DateInterval('P7Y5D')); + $input = array( + 'years' => '7', + 'months' => '6', + 'days' => '5', + ); + $form->submit($input); + $this->assertDateIntervalEquals(new \DateInterval('P7Y5D'), $form->getData()); + } + + public function testSubmitWithTime() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'input' => 'dateinterval', + 'with_hours' => true, + 'with_minutes' => true, + 'with_seconds' => true, + ) + ); + $form->setData(new \DateInterval('P7Y6M5DT4H3M2S')); + $input = array( + 'years' => '7', + 'months' => '6', + 'days' => '5', + 'hours' => '4', + 'minutes' => '3', + 'seconds' => '2', + ); + $form->submit($input); + $this->assertDateIntervalEquals(new \DateInterval('P7Y6M5DT4H3M2S'), $form->getData()); + } + + public function testSubmitWithWeeks() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'input' => 'dateinterval', + 'with_years' => false, + 'with_months' => false, + 'with_weeks' => true, + 'with_days' => false, + ) + ); + $form->setData(new \DateInterval('P0Y')); + $input = array( + 'weeks' => '30', + ); + $form->submit($input); + $this->assertDateIntervalEquals(new \DateInterval('P30W'), $form->getData()); + } + + public function testSubmitWithInvert() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'input' => 'dateinterval', + 'with_invert' => true, + ) + ); + $input = array( + 'years' => '7', + 'months' => '6', + 'days' => '5', + 'invert' => true, + ); + $form->submit($input); + $interval = new \DateInterval('P7Y6M5D'); + $interval->invert = 1; + $this->assertDateIntervalEquals($interval, $form->getData()); + } + + public function testSubmitStringSingleText() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'input' => 'string', + 'widget' => 'single_text', + ) + ); + $form->submit('P7Y6M5D'); + $this->assertEquals('P7Y6M5D', $form->getData()); + $this->assertEquals('P7Y6M5D', $form->getViewData()); + } + + public function testSubmitStringSingleTextWithSeconds() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'input' => 'string', + 'widget' => 'single_text', + 'with_hours' => true, + 'with_minutes' => true, + 'with_seconds' => true, + ) + ); + $form->submit('P7Y6M5DT4H3M2S'); + $this->assertEquals('P7Y6M5DT4H3M2S', $form->getData()); + $this->assertEquals('P7Y6M5DT4H3M2S', $form->getViewData()); + } + + public function testSubmitArrayInteger() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'widget' => 'integer', + 'with_invert' => true, + ) + ); + $input = array( + 'years' => '7', + 'months' => '6', + 'days' => '5', + 'invert' => true, + ); + $form->submit($input); + $this->assertSame('7', $form['years']->getData()); + $this->assertSame('7', $form['years']->getViewData()); + } + + public function testInitializeWithDateInterval() + { + // Throws an exception if "data_class" option is not explicitly set + // to null in the type + $this->factory->create('Symfony\Component\Form\Extension\Core\Type\DateIntervalType', new \DateInterval('P0Y')); + } + + public function testPassDefaultPlaceholderToViewIfNotRequired() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'required' => false, + 'with_seconds' => true, + ) + ); + $view = $form->createView(); + $this->assertSame('', $view['years']->vars['placeholder']); + $this->assertSame('', $view['months']->vars['placeholder']); + $this->assertSame('', $view['days']->vars['placeholder']); + $this->assertSame('', $view['seconds']->vars['placeholder']); + } + + public function testPassNoPlaceholderToViewIfRequired() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'required' => true, + 'with_seconds' => true, + ) + ); + $view = $form->createView(); + $this->assertNull($view['years']->vars['placeholder']); + $this->assertNull($view['months']->vars['placeholder']); + $this->assertNull($view['days']->vars['placeholder']); + $this->assertNull($view['seconds']->vars['placeholder']); + } + + public function testPassPlaceholderAsString() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'placeholder' => 'Empty', + 'with_seconds' => true, + ) + ); + $view = $form->createView(); + $this->assertSame('Empty', $view['years']->vars['placeholder']); + $this->assertSame('Empty', $view['months']->vars['placeholder']); + $this->assertSame('Empty', $view['days']->vars['placeholder']); + $this->assertSame('Empty', $view['seconds']->vars['placeholder']); + } + + public function testPassPlaceholderAsArray() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'placeholder' => array( + 'years' => 'Empty years', + 'months' => 'Empty months', + 'days' => 'Empty days', + 'hours' => 'Empty hours', + 'minutes' => 'Empty minutes', + 'seconds' => 'Empty seconds', + ), + 'with_hours' => true, + 'with_minutes' => true, + 'with_seconds' => true, + ) + ); + $view = $form->createView(); + $this->assertSame('Empty years', $view['years']->vars['placeholder']); + $this->assertSame('Empty months', $view['months']->vars['placeholder']); + $this->assertSame('Empty days', $view['days']->vars['placeholder']); + $this->assertSame('Empty hours', $view['hours']->vars['placeholder']); + $this->assertSame('Empty minutes', $view['minutes']->vars['placeholder']); + $this->assertSame('Empty seconds', $view['seconds']->vars['placeholder']); + } + + public function testPassPlaceholderAsPartialArrayAddEmptyIfNotRequired() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'required' => false, + 'placeholder' => array( + 'years' => 'Empty years', + 'days' => 'Empty days', + 'hours' => 'Empty hours', + 'seconds' => 'Empty seconds', + ), + 'with_hours' => true, + 'with_minutes' => true, + 'with_seconds' => true, + ) + ); + $view = $form->createView(); + $this->assertSame('Empty years', $view['years']->vars['placeholder']); + $this->assertSame('', $view['months']->vars['placeholder']); + $this->assertSame('Empty days', $view['days']->vars['placeholder']); + $this->assertSame('Empty hours', $view['hours']->vars['placeholder']); + $this->assertSame('', $view['minutes']->vars['placeholder']); + $this->assertSame('Empty seconds', $view['seconds']->vars['placeholder']); + } + + public function testPassPlaceholderAsPartialArrayAddNullIfRequired() + { + $form = $this->factory->create( + 'Symfony\Component\Form\Extension\Core\Type\DateIntervalType', + null, + array( + 'required' => true, + 'placeholder' => array( + 'years' => 'Empty years', + 'days' => 'Empty days', + 'hours' => 'Empty hours', + 'seconds' => 'Empty seconds', + ), + 'with_hours' => true, + 'with_minutes' => true, + 'with_seconds' => true, + ) + ); + $view = $form->createView(); + $this->assertSame('Empty years', $view['years']->vars['placeholder']); + $this->assertNull($view['months']->vars['placeholder']); + $this->assertSame('Empty days', $view['days']->vars['placeholder']); + $this->assertSame('Empty hours', $view['hours']->vars['placeholder']); + $this->assertNull($view['minutes']->vars['placeholder']); + $this->assertSame('Empty seconds', $view['seconds']->vars['placeholder']); + } + + public function testDateTypeChoiceErrorsBubbleUp() + { + $error = new FormError('Invalid!'); + $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\DateIntervalType', null); + $form['years']->addError($error); + $this->assertSame(array(), iterator_to_array($form['years']->getErrors())); + $this->assertSame(array($error), iterator_to_array($form->getErrors())); + } +}