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

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