vendor/symfony/form/Extension/Core/Type/DateTimeType.php line 32

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\Exception\LogicException;
  13. use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
  14. use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
  15. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
  16. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
  17. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToHtml5LocalDateTimeTransformer;
  18. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
  19. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
  20. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
  21. use Symfony\Component\Form\FormBuilderInterface;
  22. use Symfony\Component\Form\FormInterface;
  23. use Symfony\Component\Form\FormView;
  24. use Symfony\Component\Form\ReversedTransformer;
  25. use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
  26. use Symfony\Component\OptionsResolver\Options;
  27. use Symfony\Component\OptionsResolver\OptionsResolver;
  28. class DateTimeType extends AbstractType
  29. {
  30.     public const DEFAULT_DATE_FORMAT \IntlDateFormatter::MEDIUM;
  31.     public const DEFAULT_TIME_FORMAT \IntlDateFormatter::MEDIUM;
  32.     /**
  33.      * The HTML5 datetime-local format as defined in
  34.      * http://w3c.github.io/html-reference/datatypes.html#form.data.datetime-local.
  35.      */
  36.     public const HTML5_FORMAT "yyyy-MM-dd'T'HH:mm:ss";
  37.     private const ACCEPTED_FORMATS = [
  38.         \IntlDateFormatter::FULL,
  39.         \IntlDateFormatter::LONG,
  40.         \IntlDateFormatter::MEDIUM,
  41.         \IntlDateFormatter::SHORT,
  42.     ];
  43.     /**
  44.      * {@inheritdoc}
  45.      */
  46.     public function buildForm(FormBuilderInterface $builder, array $options)
  47.     {
  48.         $parts = ['year''month''day''hour'];
  49.         $dateParts = ['year''month''day'];
  50.         $timeParts = ['hour'];
  51.         if ($options['with_minutes']) {
  52.             $parts[] = 'minute';
  53.             $timeParts[] = 'minute';
  54.         }
  55.         if ($options['with_seconds']) {
  56.             $parts[] = 'second';
  57.             $timeParts[] = 'second';
  58.         }
  59.         $dateFormat \is_int($options['date_format']) ? $options['date_format'] : self::DEFAULT_DATE_FORMAT;
  60.         $timeFormat self::DEFAULT_TIME_FORMAT;
  61.         $calendar \IntlDateFormatter::GREGORIAN;
  62.         $pattern \is_string($options['format']) ? $options['format'] : null;
  63.         if (!\in_array($dateFormatself::ACCEPTED_FORMATStrue)) {
  64.             throw new InvalidOptionsException('The "date_format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
  65.         }
  66.         if ('single_text' === $options['widget']) {
  67.             if (self::HTML5_FORMAT === $pattern) {
  68.                 $builder->addViewTransformer(new DateTimeToHtml5LocalDateTimeTransformer(
  69.                     $options['model_timezone'],
  70.                     $options['view_timezone']
  71.                 ));
  72.             } else {
  73.                 $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
  74.                     $options['model_timezone'],
  75.                     $options['view_timezone'],
  76.                     $dateFormat,
  77.                     $timeFormat,
  78.                     $calendar,
  79.                     $pattern
  80.                 ));
  81.             }
  82.         } else {
  83.             // when the form is compound the entries of the array are ignored in favor of children data
  84.             // so we need to handle the cascade setting here
  85.             $emptyData $builder->getEmptyData() ?: [];
  86.             // Only pass a subset of the options to children
  87.             $dateOptions array_intersect_key($optionsarray_flip([
  88.                 'years',
  89.                 'months',
  90.                 'days',
  91.                 'placeholder',
  92.                 'choice_translation_domain',
  93.                 'required',
  94.                 'translation_domain',
  95.                 'html5',
  96.                 'invalid_message',
  97.                 'invalid_message_parameters',
  98.             ]));
  99.             if ($emptyData instanceof \Closure) {
  100.                 $lazyEmptyData = static function ($option) use ($emptyData) {
  101.                     return static function (FormInterface $form) use ($emptyData$option) {
  102.                         $emptyData $emptyData($form->getParent());
  103.                         return $emptyData[$option] ?? '';
  104.                     };
  105.                 };
  106.                 $dateOptions['empty_data'] = $lazyEmptyData('date');
  107.             } elseif (isset($emptyData['date'])) {
  108.                 $dateOptions['empty_data'] = $emptyData['date'];
  109.             }
  110.             $timeOptions array_intersect_key($optionsarray_flip([
  111.                 'hours',
  112.                 'minutes',
  113.                 'seconds',
  114.                 'with_minutes',
  115.                 'with_seconds',
  116.                 'placeholder',
  117.                 'choice_translation_domain',
  118.                 'required',
  119.                 'translation_domain',
  120.                 'html5',
  121.                 'invalid_message',
  122.                 'invalid_message_parameters',
  123.             ]));
  124.             if ($emptyData instanceof \Closure) {
  125.                 $timeOptions['empty_data'] = $lazyEmptyData('time');
  126.             } elseif (isset($emptyData['time'])) {
  127.                 $timeOptions['empty_data'] = $emptyData['time'];
  128.             }
  129.             if (false === $options['label']) {
  130.                 $dateOptions['label'] = false;
  131.                 $timeOptions['label'] = false;
  132.             }
  133.             if (null !== $options['date_widget']) {
  134.                 $dateOptions['widget'] = $options['date_widget'];
  135.             }
  136.             if (null !== $options['date_label']) {
  137.                 $dateOptions['label'] = $options['date_label'];
  138.             }
  139.             if (null !== $options['time_widget']) {
  140.                 $timeOptions['widget'] = $options['time_widget'];
  141.             }
  142.             if (null !== $options['time_label']) {
  143.                 $timeOptions['label'] = $options['time_label'];
  144.             }
  145.             if (null !== $options['date_format']) {
  146.                 $dateOptions['format'] = $options['date_format'];
  147.             }
  148.             $dateOptions['input'] = $timeOptions['input'] = 'array';
  149.             $dateOptions['error_bubbling'] = $timeOptions['error_bubbling'] = true;
  150.             $builder
  151.                 ->addViewTransformer(new DataTransformerChain([
  152.                     new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts),
  153.                     new ArrayToPartsTransformer([
  154.                         'date' => $dateParts,
  155.                         'time' => $timeParts,
  156.                     ]),
  157.                 ]))
  158.                 ->add('date'DateType::class, $dateOptions)
  159.                 ->add('time'TimeType::class, $timeOptions)
  160.             ;
  161.         }
  162.         if ('datetime_immutable' === $options['input']) {
  163.             $builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
  164.         } elseif ('string' === $options['input']) {
  165.             $builder->addModelTransformer(new ReversedTransformer(
  166.                 new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $options['input_format'])
  167.             ));
  168.         } elseif ('timestamp' === $options['input']) {
  169.             $builder->addModelTransformer(new ReversedTransformer(
  170.                 new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
  171.             ));
  172.         } elseif ('array' === $options['input']) {
  173.             $builder->addModelTransformer(new ReversedTransformer(
  174.                 new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts)
  175.             ));
  176.         }
  177.     }
  178.     /**
  179.      * {@inheritdoc}
  180.      */
  181.     public function buildView(FormView $viewFormInterface $form, array $options)
  182.     {
  183.         $view->vars['widget'] = $options['widget'];
  184.         // Change the input to an HTML5 datetime input if
  185.         //  * the widget is set to "single_text"
  186.         //  * the format matches the one expected by HTML5
  187.         //  * the html5 is set to true
  188.         if ($options['html5'] && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
  189.             $view->vars['type'] = 'datetime-local';
  190.             // we need to force the browser to display the seconds by
  191.             // adding the HTML attribute step if not already defined.
  192.             // Otherwise the browser will not display and so not send the seconds
  193.             // therefore the value will always be considered as invalid.
  194.             if ($options['with_seconds'] && !isset($view->vars['attr']['step'])) {
  195.                 $view->vars['attr']['step'] = 1;
  196.             }
  197.         }
  198.     }
  199.     /**
  200.      * {@inheritdoc}
  201.      */
  202.     public function configureOptions(OptionsResolver $resolver)
  203.     {
  204.         $compound = function (Options $options) {
  205.             return 'single_text' !== $options['widget'];
  206.         };
  207.         // Defaults to the value of "widget"
  208.         $dateWidget = function (Options $options) {
  209.             return 'single_text' === $options['widget'] ? null $options['widget'];
  210.         };
  211.         // Defaults to the value of "widget"
  212.         $timeWidget = function (Options $options) {
  213.             return 'single_text' === $options['widget'] ? null $options['widget'];
  214.         };
  215.         $resolver->setDefaults([
  216.             'input' => 'datetime',
  217.             'model_timezone' => null,
  218.             'view_timezone' => null,
  219.             'format' => self::HTML5_FORMAT,
  220.             'date_format' => null,
  221.             'widget' => null,
  222.             'date_widget' => $dateWidget,
  223.             'time_widget' => $timeWidget,
  224.             'with_minutes' => true,
  225.             'with_seconds' => false,
  226.             'html5' => true,
  227.             // Don't modify \DateTime classes by reference, we treat
  228.             // them like immutable value objects
  229.             'by_reference' => false,
  230.             'error_bubbling' => false,
  231.             // If initialized with a \DateTime object, FormType initializes
  232.             // this option to "\DateTime". Since the internal, normalized
  233.             // representation is not \DateTime, but an array, we need to unset
  234.             // this option.
  235.             'data_class' => null,
  236.             'compound' => $compound,
  237.             'date_label' => null,
  238.             'time_label' => null,
  239.             'empty_data' => function (Options $options) {
  240.                 return $options['compound'] ? [] : '';
  241.             },
  242.             'input_format' => 'Y-m-d H:i:s',
  243.             'invalid_message' => function (Options $options$previousValue) {
  244.                 return ($options['legacy_error_messages'] ?? true)
  245.                     ? $previousValue
  246.                     'Please enter a valid date and time.';
  247.             },
  248.         ]);
  249.         // Don't add some defaults in order to preserve the defaults
  250.         // set in DateType and TimeType
  251.         $resolver->setDefined([
  252.             'placeholder',
  253.             'choice_translation_domain',
  254.             'years',
  255.             'months',
  256.             'days',
  257.             'hours',
  258.             'minutes',
  259.             'seconds',
  260.         ]);
  261.         $resolver->setAllowedValues('input', [
  262.             'datetime',
  263.             'datetime_immutable',
  264.             'string',
  265.             'timestamp',
  266.             'array',
  267.         ]);
  268.         $resolver->setAllowedValues('date_widget', [
  269.             null// inherit default from DateType
  270.             'single_text',
  271.             'text',
  272.             'choice',
  273.         ]);
  274.         $resolver->setAllowedValues('time_widget', [
  275.             null// inherit default from TimeType
  276.             'single_text',
  277.             'text',
  278.             'choice',
  279.         ]);
  280.         // This option will overwrite "date_widget" and "time_widget" options
  281.         $resolver->setAllowedValues('widget', [
  282.             null// default, don't overwrite options
  283.             'single_text',
  284.             'text',
  285.             'choice',
  286.         ]);
  287.         $resolver->setAllowedTypes('input_format''string');
  288.         $resolver->setNormalizer('date_format', function (Options $options$dateFormat) {
  289.             if (null !== $dateFormat && 'single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
  290.                 throw new LogicException(sprintf('Cannot use the "date_format" option of the "%s" with an HTML5 date.'self::class));
  291.             }
  292.             return $dateFormat;
  293.         });
  294.         $resolver->setNormalizer('date_widget', function (Options $options$dateWidget) {
  295.             if (null !== $dateWidget && 'single_text' === $options['widget']) {
  296.                 throw new LogicException(sprintf('Cannot use the "date_widget" option of the "%s" when the "widget" option is set to "single_text".'self::class));
  297.             }
  298.             return $dateWidget;
  299.         });
  300.         $resolver->setNormalizer('time_widget', function (Options $options$timeWidget) {
  301.             if (null !== $timeWidget && 'single_text' === $options['widget']) {
  302.                 throw new LogicException(sprintf('Cannot use the "time_widget" option of the "%s" when the "widget" option is set to "single_text".'self::class));
  303.             }
  304.             return $timeWidget;
  305.         });
  306.         $resolver->setNormalizer('html5', function (Options $options$html5) {
  307.             if ($html5 && self::HTML5_FORMAT !== $options['format']) {
  308.                 throw new LogicException(sprintf('Cannot use the "format" option of "%s" when the "html5" option is enabled.'self::class));
  309.             }
  310.             return $html5;
  311.         });
  312.     }
  313.     /**
  314.      * {@inheritdoc}
  315.      */
  316.     public function getBlockPrefix()
  317.     {
  318.         return 'datetime';
  319.     }
  320. }