vendor/symfony/form/Extension/Core/Type/DateType.php line 29

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