vendor/symfony/form/Extension/Core/Type/TimeType.php line 30

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\InvalidConfigurationException;
  13. use Symfony\Component\Form\Exception\LogicException;
  14. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
  15. use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
  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\FormEvent;
  20. use Symfony\Component\Form\FormEvents;
  21. use Symfony\Component\Form\FormInterface;
  22. use Symfony\Component\Form\FormView;
  23. use Symfony\Component\Form\ReversedTransformer;
  24. use Symfony\Component\OptionsResolver\Options;
  25. use Symfony\Component\OptionsResolver\OptionsResolver;
  26. class TimeType extends AbstractType
  27. {
  28.     private const WIDGETS = [
  29.         'text' => TextType::class,
  30.         'choice' => ChoiceType::class,
  31.     ];
  32.     /**
  33.      * {@inheritdoc}
  34.      */
  35.     public function buildForm(FormBuilderInterface $builder, array $options)
  36.     {
  37.         $parts = ['hour'];
  38.         $format 'H';
  39.         if ($options['with_seconds'] && !$options['with_minutes']) {
  40.             throw new InvalidConfigurationException('You cannot disable minutes if you have enabled seconds.');
  41.         }
  42.         if (null !== $options['reference_date'] && $options['reference_date']->getTimezone()->getName() !== $options['model_timezone']) {
  43.             throw new InvalidConfigurationException(sprintf('The configured "model_timezone" (%s) must match the timezone of the "reference_date" (%s).'$options['model_timezone'], $options['reference_date']->getTimezone()->getName()));
  44.         }
  45.         if ($options['with_minutes']) {
  46.             $format .= ':i';
  47.             $parts[] = 'minute';
  48.         }
  49.         if ($options['with_seconds']) {
  50.             $format .= ':s';
  51.             $parts[] = 'second';
  52.         }
  53.         if ('single_text' === $options['widget']) {
  54.             $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $e) use ($options) {
  55.                 $data $e->getData();
  56.                 if ($data && preg_match('/^(?P<hours>\d{2}):(?P<minutes>\d{2})(?::(?P<seconds>\d{2})(?:\.\d+)?)?$/'$data$matches)) {
  57.                     if ($options['with_seconds']) {
  58.                         // handle seconds ignored by user's browser when with_seconds enabled
  59.                         // https://codereview.chromium.org/450533009/
  60.                         $e->setData(sprintf('%s:%s:%s'$matches['hours'], $matches['minutes'], $matches['seconds'] ?? '00'));
  61.                     } else {
  62.                         $e->setData(sprintf('%s:%s'$matches['hours'], $matches['minutes']));
  63.                     }
  64.                 }
  65.             });
  66.             $parseFormat null;
  67.             if (null !== $options['reference_date']) {
  68.                 $parseFormat 'Y-m-d '.$format;
  69.                 $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
  70.                     $data $event->getData();
  71.                     if (preg_match('/^\d{2}:\d{2}(:\d{2})?$/'$data)) {
  72.                         $event->setData($options['reference_date']->format('Y-m-d ').$data);
  73.                     }
  74.                 });
  75.             }
  76.             $builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format$parseFormat));
  77.         } else {
  78.             $hourOptions $minuteOptions $secondOptions = [
  79.                 'error_bubbling' => true,
  80.                 'empty_data' => '',
  81.             ];
  82.             // when the form is compound the entries of the array are ignored in favor of children data
  83.             // so we need to handle the cascade setting here
  84.             $emptyData $builder->getEmptyData() ?: [];
  85.             if ($emptyData instanceof \Closure) {
  86.                 $lazyEmptyData = static function ($option) use ($emptyData) {
  87.                     return static function (FormInterface $form) use ($emptyData$option) {
  88.                         $emptyData $emptyData($form->getParent());
  89.                         return $emptyData[$option] ?? '';
  90.                     };
  91.                 };
  92.                 $hourOptions['empty_data'] = $lazyEmptyData('hour');
  93.             } elseif (isset($emptyData['hour'])) {
  94.                 $hourOptions['empty_data'] = $emptyData['hour'];
  95.             }
  96.             if (isset($options['invalid_message'])) {
  97.                 $hourOptions['invalid_message'] = $options['invalid_message'];
  98.                 $minuteOptions['invalid_message'] = $options['invalid_message'];
  99.                 $secondOptions['invalid_message'] = $options['invalid_message'];
  100.             }
  101.             if (isset($options['invalid_message_parameters'])) {
  102.                 $hourOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
  103.                 $minuteOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
  104.                 $secondOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
  105.             }
  106.             if ('choice' === $options['widget']) {
  107.                 $hours $minutes = [];
  108.                 foreach ($options['hours'] as $hour) {
  109.                     $hours[str_pad($hour2'0'\STR_PAD_LEFT)] = $hour;
  110.                 }
  111.                 // Only pass a subset of the options to children
  112.                 $hourOptions['choices'] = $hours;
  113.                 $hourOptions['placeholder'] = $options['placeholder']['hour'];
  114.                 $hourOptions['choice_translation_domain'] = $options['choice_translation_domain']['hour'];
  115.                 if ($options['with_minutes']) {
  116.                     foreach ($options['minutes'] as $minute) {
  117.                         $minutes[str_pad($minute2'0'\STR_PAD_LEFT)] = $minute;
  118.                     }
  119.                     $minuteOptions['choices'] = $minutes;
  120.                     $minuteOptions['placeholder'] = $options['placeholder']['minute'];
  121.                     $minuteOptions['choice_translation_domain'] = $options['choice_translation_domain']['minute'];
  122.                 }
  123.                 if ($options['with_seconds']) {
  124.                     $seconds = [];
  125.                     foreach ($options['seconds'] as $second) {
  126.                         $seconds[str_pad($second2'0'\STR_PAD_LEFT)] = $second;
  127.                     }
  128.                     $secondOptions['choices'] = $seconds;
  129.                     $secondOptions['placeholder'] = $options['placeholder']['second'];
  130.                     $secondOptions['choice_translation_domain'] = $options['choice_translation_domain']['second'];
  131.                 }
  132.                 // Append generic carry-along options
  133.                 foreach (['required''translation_domain'] as $passOpt) {
  134.                     $hourOptions[$passOpt] = $options[$passOpt];
  135.                     if ($options['with_minutes']) {
  136.                         $minuteOptions[$passOpt] = $options[$passOpt];
  137.                     }
  138.                     if ($options['with_seconds']) {
  139.                         $secondOptions[$passOpt] = $options[$passOpt];
  140.                     }
  141.                 }
  142.             }
  143.             $builder->add('hour'self::WIDGETS[$options['widget']], $hourOptions);
  144.             if ($options['with_minutes']) {
  145.                 if ($emptyData instanceof \Closure) {
  146.                     $minuteOptions['empty_data'] = $lazyEmptyData('minute');
  147.                 } elseif (isset($emptyData['minute'])) {
  148.                     $minuteOptions['empty_data'] = $emptyData['minute'];
  149.                 }
  150.                 $builder->add('minute'self::WIDGETS[$options['widget']], $minuteOptions);
  151.             }
  152.             if ($options['with_seconds']) {
  153.                 if ($emptyData instanceof \Closure) {
  154.                     $secondOptions['empty_data'] = $lazyEmptyData('second');
  155.                 } elseif (isset($emptyData['second'])) {
  156.                     $secondOptions['empty_data'] = $emptyData['second'];
  157.                 }
  158.                 $builder->add('second'self::WIDGETS[$options['widget']], $secondOptions);
  159.             }
  160.             $builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts'text' === $options['widget'], $options['reference_date']));
  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'text' === $options['widget'], $options['reference_date'])
  175.             ));
  176.         }
  177.     }
  178.     /**
  179.      * {@inheritdoc}
  180.      */
  181.     public function buildView(FormView $viewFormInterface $form, array $options)
  182.     {
  183.         $view->vars array_replace($view->vars, [
  184.             'widget' => $options['widget'],
  185.             'with_minutes' => $options['with_minutes'],
  186.             'with_seconds' => $options['with_seconds'],
  187.         ]);
  188.         // Change the input to an HTML5 time input if
  189.         //  * the widget is set to "single_text"
  190.         //  * the html5 is set to true
  191.         if ($options['html5'] && 'single_text' === $options['widget']) {
  192.             $view->vars['type'] = 'time';
  193.             // we need to force the browser to display the seconds by
  194.             // adding the HTML attribute step if not already defined.
  195.             // Otherwise the browser will not display and so not send the seconds
  196.             // therefore the value will always be considered as invalid.
  197.             if ($options['with_seconds'] && !isset($view->vars['attr']['step'])) {
  198.                 $view->vars['attr']['step'] = 1;
  199.             }
  200.         }
  201.     }
  202.     /**
  203.      * {@inheritdoc}
  204.      */
  205.     public function configureOptions(OptionsResolver $resolver)
  206.     {
  207.         $compound = function (Options $options) {
  208.             return 'single_text' !== $options['widget'];
  209.         };
  210.         $placeholderDefault = function (Options $options) {
  211.             return $options['required'] ? null '';
  212.         };
  213.         $placeholderNormalizer = function (Options $options$placeholder) use ($placeholderDefault) {
  214.             if (\is_array($placeholder)) {
  215.                 $default $placeholderDefault($options);
  216.                 return array_merge(
  217.                     ['hour' => $default'minute' => $default'second' => $default],
  218.                     $placeholder
  219.                 );
  220.             }
  221.             return [
  222.                 'hour' => $placeholder,
  223.                 'minute' => $placeholder,
  224.                 'second' => $placeholder,
  225.             ];
  226.         };
  227.         $choiceTranslationDomainNormalizer = function (Options $options$choiceTranslationDomain) {
  228.             if (\is_array($choiceTranslationDomain)) {
  229.                 $default false;
  230.                 return array_replace(
  231.                     ['hour' => $default'minute' => $default'second' => $default],
  232.                     $choiceTranslationDomain
  233.                 );
  234.             }
  235.             return [
  236.                 'hour' => $choiceTranslationDomain,
  237.                 'minute' => $choiceTranslationDomain,
  238.                 'second' => $choiceTranslationDomain,
  239.             ];
  240.         };
  241.         $modelTimezone = static function (Options $options$value): ?string {
  242.             if (null !== $value) {
  243.                 return $value;
  244.             }
  245.             if (null !== $options['reference_date']) {
  246.                 return $options['reference_date']->getTimezone()->getName();
  247.             }
  248.             return null;
  249.         };
  250.         $viewTimezone = static function (Options $options$value): ?string {
  251.             if (null !== $value) {
  252.                 return $value;
  253.             }
  254.             if (null !== $options['model_timezone'] && null === $options['reference_date']) {
  255.                 return $options['model_timezone'];
  256.             }
  257.             return null;
  258.         };
  259.         $resolver->setDefaults([
  260.             'hours' => range(023),
  261.             'minutes' => range(059),
  262.             'seconds' => range(059),
  263.             'widget' => 'choice',
  264.             'input' => 'datetime',
  265.             'input_format' => 'H:i:s',
  266.             'with_minutes' => true,
  267.             'with_seconds' => false,
  268.             'model_timezone' => $modelTimezone,
  269.             'view_timezone' => $viewTimezone,
  270.             'reference_date' => null,
  271.             'placeholder' => $placeholderDefault,
  272.             'html5' => true,
  273.             // Don't modify \DateTime classes by reference, we treat
  274.             // them like immutable value objects
  275.             'by_reference' => false,
  276.             'error_bubbling' => false,
  277.             // If initialized with a \DateTime object, FormType initializes
  278.             // this option to "\DateTime". Since the internal, normalized
  279.             // representation is not \DateTime, but an array, we need to unset
  280.             // this option.
  281.             'data_class' => null,
  282.             'empty_data' => function (Options $options) {
  283.                 return $options['compound'] ? [] : '';
  284.             },
  285.             'compound' => $compound,
  286.             'choice_translation_domain' => false,
  287.             'invalid_message' => function (Options $options$previousValue) {
  288.                 return ($options['legacy_error_messages'] ?? true)
  289.                     ? $previousValue
  290.                     'Please enter a valid time.';
  291.             },
  292.         ]);
  293.         $resolver->setNormalizer('view_timezone', function (Options $options$viewTimezone): ?string {
  294.             if (null !== $options['model_timezone'] && $viewTimezone !== $options['model_timezone'] && null === $options['reference_date']) {
  295.                 throw new LogicException('Using different values for the "model_timezone" and "view_timezone" options without configuring a reference date is not supported.');
  296.             }
  297.             return $viewTimezone;
  298.         });
  299.         $resolver->setNormalizer('placeholder'$placeholderNormalizer);
  300.         $resolver->setNormalizer('choice_translation_domain'$choiceTranslationDomainNormalizer);
  301.         $resolver->setAllowedValues('input', [
  302.             'datetime',
  303.             'datetime_immutable',
  304.             'string',
  305.             'timestamp',
  306.             'array',
  307.         ]);
  308.         $resolver->setAllowedValues('widget', [
  309.             'single_text',
  310.             'text',
  311.             'choice',
  312.         ]);
  313.         $resolver->setAllowedTypes('hours''array');
  314.         $resolver->setAllowedTypes('minutes''array');
  315.         $resolver->setAllowedTypes('seconds''array');
  316.         $resolver->setAllowedTypes('input_format''string');
  317.         $resolver->setAllowedTypes('model_timezone', ['null''string']);
  318.         $resolver->setAllowedTypes('view_timezone', ['null''string']);
  319.         $resolver->setAllowedTypes('reference_date', ['null'\DateTimeInterface::class]);
  320.     }
  321.     /**
  322.      * {@inheritdoc}
  323.      */
  324.     public function getBlockPrefix()
  325.     {
  326.         return 'time';
  327.     }
  328. }