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

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