vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/DateType.php line 27

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Form\Extension\Core\Type;
  11. use Symfony\Component\Form\AbstractType;
  12. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
  13. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
  14. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
  15. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
  16. use Symfony\Component\Form\FormBuilderInterface;
  17. use Symfony\Component\Form\FormInterface;
  18. use Symfony\Component\Form\FormView;
  19. use Symfony\Component\Form\ReversedTransformer;
  20. use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
  21. use Symfony\Component\OptionsResolver\Options;
  22. use Symfony\Component\OptionsResolver\OptionsResolver;
  23. class DateType extends AbstractType
  24. {
  25.     const DEFAULT_FORMAT = \IntlDateFormatter::MEDIUM;
  26.     const HTML5_FORMAT 'yyyy-MM-dd';
  27.     private static $acceptedFormats = [
  28.         \IntlDateFormatter::FULL,
  29.         \IntlDateFormatter::LONG,
  30.         \IntlDateFormatter::MEDIUM,
  31.         \IntlDateFormatter::SHORT,
  32.     ];
  33.     private static $widgets = [
  34.         'text' => 'Symfony\Component\Form\Extension\Core\Type\TextType',
  35.         'choice' => 'Symfony\Component\Form\Extension\Core\Type\ChoiceType',
  36.     ];
  37.     /**
  38.      * {@inheritdoc}
  39.      */
  40.     public function buildForm(FormBuilderInterface $builder, array $options)
  41.     {
  42.         $dateFormat = \is_int($options['format']) ? $options['format'] : self::DEFAULT_FORMAT;
  43.         $timeFormat = \IntlDateFormatter::NONE;
  44.         $calendar = \IntlDateFormatter::GREGORIAN;
  45.         $pattern = \is_string($options['format']) ? $options['format'] : null;
  46.         if (!\in_array($dateFormatself::$acceptedFormatstrue)) {
  47.             throw new InvalidOptionsException('The "format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
  48.         }
  49.         if ('single_text' === $options['widget']) {
  50.             if (null !== $pattern && false === strpos($pattern'y') && false === strpos($pattern'M') && false === strpos($pattern'd')) {
  51.                 throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" or "d". Its current value is "%s".'$pattern));
  52.             }
  53.             $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
  54.                 $options['model_timezone'],
  55.                 $options['view_timezone'],
  56.                 $dateFormat,
  57.                 $timeFormat,
  58.                 $calendar,
  59.                 $pattern
  60.             ));
  61.         } else {
  62.             if (null !== $pattern && (false === strpos($pattern'y') || false === strpos($pattern'M') || false === strpos($pattern'd'))) {
  63.                 throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".'$pattern));
  64.             }
  65.             $yearOptions $monthOptions $dayOptions = [
  66.                 'error_bubbling' => true,
  67.                 'empty_data' => '',
  68.             ];
  69.             // when the form is compound the entries of the array are ignored in favor of children data
  70.             // so we need to handle the cascade setting here
  71.             $emptyData $builder->getEmptyData() ?: [];
  72.             if ($emptyData instanceof \Closure) {
  73.                 $lazyEmptyData = static function ($option) use ($emptyData) {
  74.                     return static function (FormInterface $form) use ($emptyData$option) {
  75.                         $emptyData $emptyData($form->getParent());
  76.                         return isset($emptyData[$option]) ? $emptyData[$option] : '';
  77.                     };
  78.                 };
  79.                 $yearOptions['empty_data'] = $lazyEmptyData('year');
  80.                 $monthOptions['empty_data'] = $lazyEmptyData('month');
  81.                 $dayOptions['empty_data'] = $lazyEmptyData('day');
  82.             } else {
  83.                 if (isset($emptyData['year'])) {
  84.                     $yearOptions['empty_data'] = $emptyData['year'];
  85.                 }
  86.                 if (isset($emptyData['month'])) {
  87.                     $monthOptions['empty_data'] = $emptyData['month'];
  88.                 }
  89.                 if (isset($emptyData['day'])) {
  90.                     $dayOptions['empty_data'] = $emptyData['day'];
  91.                 }
  92.             }
  93.             if (isset($options['invalid_message'])) {
  94.                 $dayOptions['invalid_message'] = $options['invalid_message'];
  95.                 $monthOptions['invalid_message'] = $options['invalid_message'];
  96.                 $yearOptions['invalid_message'] = $options['invalid_message'];
  97.             }
  98.             if (isset($options['invalid_message_parameters'])) {
  99.                 $dayOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
  100.                 $monthOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
  101.                 $yearOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
  102.             }
  103.             $formatter = new \IntlDateFormatter(
  104.                 \Locale::getDefault(),
  105.                 $dateFormat,
  106.                 $timeFormat,
  107.                 // see https://bugs.php.net/66323
  108.                 class_exists('IntlTimeZone'false) ? \IntlTimeZone::createDefault() : null,
  109.                 $calendar,
  110.                 $pattern
  111.             );
  112.             // new \IntlDateFormatter may return null instead of false in case of failure, see https://bugs.php.net/66323
  113.             if (!$formatter) {
  114.                 throw new InvalidOptionsException(intl_get_error_message(), intl_get_error_code());
  115.             }
  116.             $formatter->setLenient(false);
  117.             if ('choice' === $options['widget']) {
  118.                 // Only pass a subset of the options to children
  119.                 $yearOptions['choices'] = $this->formatTimestamps($formatter'/y+/'$this->listYears($options['years']));
  120.                 $yearOptions['placeholder'] = $options['placeholder']['year'];
  121.                 $yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year'];
  122.                 $monthOptions['choices'] = $this->formatTimestamps($formatter'/[M|L]+/'$this->listMonths($options['months']));
  123.                 $monthOptions['placeholder'] = $options['placeholder']['month'];
  124.                 $monthOptions['choice_translation_domain'] = $options['choice_translation_domain']['month'];
  125.                 $dayOptions['choices'] = $this->formatTimestamps($formatter'/d+/'$this->listDays($options['days']));
  126.                 $dayOptions['placeholder'] = $options['placeholder']['day'];
  127.                 $dayOptions['choice_translation_domain'] = $options['choice_translation_domain']['day'];
  128.             }
  129.             // Append generic carry-along options
  130.             foreach (['required''translation_domain'] as $passOpt) {
  131.                 $yearOptions[$passOpt] = $monthOptions[$passOpt] = $dayOptions[$passOpt] = $options[$passOpt];
  132.             }
  133.             $builder
  134.                 ->add('year'self::$widgets[$options['widget']], $yearOptions)
  135.                 ->add('month'self::$widgets[$options['widget']], $monthOptions)
  136.                 ->add('day'self::$widgets[$options['widget']], $dayOptions)
  137.                 ->addViewTransformer(new DateTimeToArrayTransformer(
  138.                     $options['model_timezone'], $options['view_timezone'], ['year''month''day']
  139.                 ))
  140.                 ->setAttribute('formatter'$formatter)
  141.             ;
  142.         }
  143.         if ('string' === $options['input']) {
  144.             $builder->addModelTransformer(new ReversedTransformer(
  145.                 new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], 'Y-m-d')
  146.             ));
  147.         } elseif ('timestamp' === $options['input']) {
  148.             $builder->addModelTransformer(new ReversedTransformer(
  149.                 new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
  150.             ));
  151.         } elseif ('array' === $options['input']) {
  152.             $builder->addModelTransformer(new ReversedTransformer(
  153.                 new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], ['year''month''day'])
  154.             ));
  155.         }
  156.     }
  157.     /**
  158.      * {@inheritdoc}
  159.      */
  160.     public function finishView(FormView $viewFormInterface $form, array $options)
  161.     {
  162.         $view->vars['widget'] = $options['widget'];
  163.         // Change the input to a HTML5 date input if
  164.         //  * the widget is set to "single_text"
  165.         //  * the format matches the one expected by HTML5
  166.         //  * the html5 is set to true
  167.         if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
  168.             $view->vars['type'] = 'date';
  169.         }
  170.         if ($form->getConfig()->hasAttribute('formatter')) {
  171.             $pattern $form->getConfig()->getAttribute('formatter')->getPattern();
  172.             // remove special characters unless the format was explicitly specified
  173.             if (!\is_string($options['format'])) {
  174.                 // remove quoted strings first
  175.                 $pattern preg_replace('/\'[^\']+\'/'''$pattern);
  176.                 // remove remaining special chars
  177.                 $pattern preg_replace('/[^yMd]+/'''$pattern);
  178.             }
  179.             // set right order with respect to locale (e.g.: de_DE=dd.MM.yy; en_US=M/d/yy)
  180.             // lookup various formats at http://userguide.icu-project.org/formatparse/datetime
  181.             if (preg_match('/^([yMd]+)[^yMd]*([yMd]+)[^yMd]*([yMd]+)$/'$pattern)) {
  182.                 $pattern preg_replace(['/y+/''/M+/''/d+/'], ['{{ year }}''{{ month }}''{{ day }}'], $pattern);
  183.             } else {
  184.                 // default fallback
  185.                 $pattern '{{ year }}{{ month }}{{ day }}';
  186.             }
  187.             $view->vars['date_pattern'] = $pattern;
  188.         }
  189.     }
  190.     /**
  191.      * {@inheritdoc}
  192.      */
  193.     public function configureOptions(OptionsResolver $resolver)
  194.     {
  195.         $compound = function (Options $options) {
  196.             return 'single_text' !== $options['widget'];
  197.         };
  198.         $placeholderDefault = function (Options $options) {
  199.             return $options['required'] ? null '';
  200.         };
  201.         $placeholderNormalizer = function (Options $options$placeholder) use ($placeholderDefault) {
  202.             if (\is_array($placeholder)) {
  203.                 $default $placeholderDefault($options);
  204.                 return array_merge(
  205.                     ['year' => $default'month' => $default'day' => $default],
  206.                     $placeholder
  207.                 );
  208.             }
  209.             return [
  210.                 'year' => $placeholder,
  211.                 'month' => $placeholder,
  212.                 'day' => $placeholder,
  213.             ];
  214.         };
  215.         $choiceTranslationDomainNormalizer = function (Options $options$choiceTranslationDomain) {
  216.             if (\is_array($choiceTranslationDomain)) {
  217.                 $default false;
  218.                 return array_replace(
  219.                     ['year' => $default'month' => $default'day' => $default],
  220.                     $choiceTranslationDomain
  221.                 );
  222.             }
  223.             return [
  224.                 'year' => $choiceTranslationDomain,
  225.                 'month' => $choiceTranslationDomain,
  226.                 'day' => $choiceTranslationDomain,
  227.             ];
  228.         };
  229.         $format = function (Options $options) {
  230.             return 'single_text' === $options['widget'] ? self::HTML5_FORMAT self::DEFAULT_FORMAT;
  231.         };
  232.         $resolver->setDefaults([
  233.             'years' => range((int) date('Y') - 5, (int) date('Y') + 5),
  234.             'months' => range(112),
  235.             'days' => range(131),
  236.             'widget' => 'choice',
  237.             'input' => 'datetime',
  238.             'format' => $format,
  239.             'model_timezone' => null,
  240.             'view_timezone' => null,
  241.             'placeholder' => $placeholderDefault,
  242.             'html5' => true,
  243.             // Don't modify \DateTime classes by reference, we treat
  244.             // them like immutable value objects
  245.             'by_reference' => false,
  246.             'error_bubbling' => false,
  247.             // If initialized with a \DateTime object, FormType initializes
  248.             // this option to "\DateTime". Since the internal, normalized
  249.             // representation is not \DateTime, but an array, we need to unset
  250.             // this option.
  251.             'data_class' => null,
  252.             'compound' => $compound,
  253.             'empty_data' => function (Options $options) {
  254.                 return $options['compound'] ? [] : '';
  255.             },
  256.             'choice_translation_domain' => false,
  257.         ]);
  258.         $resolver->setNormalizer('placeholder'$placeholderNormalizer);
  259.         $resolver->setNormalizer('choice_translation_domain'$choiceTranslationDomainNormalizer);
  260.         $resolver->setAllowedValues('input', [
  261.             'datetime',
  262.             'string',
  263.             'timestamp',
  264.             'array',
  265.         ]);
  266.         $resolver->setAllowedValues('widget', [
  267.             'single_text',
  268.             'text',
  269.             'choice',
  270.         ]);
  271.         $resolver->setAllowedTypes('format', ['int''string']);
  272.         $resolver->setAllowedTypes('years''array');
  273.         $resolver->setAllowedTypes('months''array');
  274.         $resolver->setAllowedTypes('days''array');
  275.     }
  276.     /**
  277.      * {@inheritdoc}
  278.      */
  279.     public function getBlockPrefix()
  280.     {
  281.         return 'date';
  282.     }
  283.     private function formatTimestamps(\IntlDateFormatter $formatter$regex, array $timestamps)
  284.     {
  285.         $pattern $formatter->getPattern();
  286.         $timezone $formatter->getTimeZoneId();
  287.         $formattedTimestamps = [];
  288.         $formatter->setTimeZone('UTC');
  289.         if (preg_match($regex$pattern$matches)) {
  290.             $formatter->setPattern($matches[0]);
  291.             foreach ($timestamps as $timestamp => $choice) {
  292.                 $formattedTimestamps[$formatter->format($timestamp)] = $choice;
  293.             }
  294.             // I'd like to clone the formatter above, but then we get a
  295.             // segmentation fault, so let's restore the old state instead
  296.             $formatter->setPattern($pattern);
  297.         }
  298.         $formatter->setTimeZone($timezone);
  299.         return $formattedTimestamps;
  300.     }
  301.     private function listYears(array $years)
  302.     {
  303.         $result = [];
  304.         foreach ($years as $year) {
  305.             if (false !== $y gmmktime(000615$year)) {
  306.                 $result[$y] = $year;
  307.             }
  308.         }
  309.         return $result;
  310.     }
  311.     private function listMonths(array $months)
  312.     {
  313.         $result = [];
  314.         foreach ($months as $month) {
  315.             $result[gmmktime(000$month15)] = $month;
  316.         }
  317.         return $result;
  318.     }
  319.     private function listDays(array $days)
  320.     {
  321.         $result = [];
  322.         foreach ($days as $day) {
  323.             $result[gmmktime(0005$day)] = $day;
  324.         }
  325.         return $result;
  326.     }
  327. }