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