vendor/symfony/form/Extension/Core/Type/ChoiceType.php line 277

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\ChoiceList\ChoiceListInterface;
  13. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
  14. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
  15. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFilter;
  16. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
  17. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
  18. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceTranslationParameters;
  19. use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
  20. use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy;
  21. use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice;
  22. use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
  23. use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
  24. use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
  25. use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
  26. use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
  27. use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
  28. use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
  29. use Symfony\Component\Form\ChoiceList\View\ChoiceView;
  30. use Symfony\Component\Form\Exception\TransformationFailedException;
  31. use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper;
  32. use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper;
  33. use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
  34. use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
  35. use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
  36. use Symfony\Component\Form\FormBuilderInterface;
  37. use Symfony\Component\Form\FormError;
  38. use Symfony\Component\Form\FormEvent;
  39. use Symfony\Component\Form\FormEvents;
  40. use Symfony\Component\Form\FormInterface;
  41. use Symfony\Component\Form\FormView;
  42. use Symfony\Component\OptionsResolver\Options;
  43. use Symfony\Component\OptionsResolver\OptionsResolver;
  44. use Symfony\Component\PropertyAccess\PropertyPath;
  45. use Symfony\Contracts\Translation\TranslatorInterface;
  46. class ChoiceType extends AbstractType
  47. {
  48.     private $choiceListFactory;
  49.     private $translator;
  50.     /**
  51.      * @param TranslatorInterface $translator
  52.      */
  53.     public function __construct(ChoiceListFactoryInterface $choiceListFactory null$translator null)
  54.     {
  55.         $this->choiceListFactory $choiceListFactory ?? new CachingFactoryDecorator(
  56.             new PropertyAccessDecorator(
  57.                 new DefaultChoiceListFactory()
  58.             )
  59.         );
  60.         if (null !== $translator && !$translator instanceof TranslatorInterface) {
  61.             throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be han instance of "%s", "%s" given.'__METHOD__TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
  62.         }
  63.         $this->translator $translator;
  64.         // BC, to be removed in 6.0
  65.         if ($this->choiceListFactory instanceof CachingFactoryDecorator) {
  66.             return;
  67.         }
  68.         $ref = new \ReflectionMethod($this->choiceListFactory'createListFromChoices');
  69.         if ($ref->getNumberOfParameters() < 3) {
  70.             trigger_deprecation('symfony/form''5.1''Not defining a third parameter "callable|null $filter" in "%s::%s()" is deprecated.'$ref->class$ref->name);
  71.         }
  72.     }
  73.     /**
  74.      * {@inheritdoc}
  75.      */
  76.     public function buildForm(FormBuilderInterface $builder, array $options)
  77.     {
  78.         $unknownValues = [];
  79.         $choiceList $this->createChoiceList($options);
  80.         $builder->setAttribute('choice_list'$choiceList);
  81.         if ($options['expanded']) {
  82.             $builder->setDataMapper($options['multiple'] ? new CheckboxListMapper() : new RadioListMapper());
  83.             // Initialize all choices before doing the index check below.
  84.             // This helps in cases where index checks are optimized for non
  85.             // initialized choice lists. For example, when using an SQL driver,
  86.             // the index check would read in one SQL query and the initialization
  87.             // requires another SQL query. When the initialization is done first,
  88.             // one SQL query is sufficient.
  89.             $choiceListView $this->createChoiceListView($choiceList$options);
  90.             $builder->setAttribute('choice_list_view'$choiceListView);
  91.             // Check if the choices already contain the empty value
  92.             // Only add the placeholder option if this is not the case
  93.             if (null !== $options['placeholder'] && === \count($choiceList->getChoicesForValues(['']))) {
  94.                 $placeholderView = new ChoiceView(null''$options['placeholder']);
  95.                 // "placeholder" is a reserved name
  96.                 $this->addSubForm($builder'placeholder'$placeholderView$options);
  97.             }
  98.             $this->addSubForms($builder$choiceListView->preferredChoices$options);
  99.             $this->addSubForms($builder$choiceListView->choices$options);
  100.         }
  101.         if ($options['expanded'] || $options['multiple']) {
  102.             // Make sure that scalar, submitted values are converted to arrays
  103.             // which can be submitted to the checkboxes/radio buttons
  104.             $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($choiceList$options, &$unknownValues) {
  105.                 $form $event->getForm();
  106.                 $data $event->getData();
  107.                 // Since the type always use mapper an empty array will not be
  108.                 // considered as empty in Form::submit(), we need to evaluate
  109.                 // empty data here so its value is submitted to sub forms
  110.                 if (null === $data) {
  111.                     $emptyData $form->getConfig()->getEmptyData();
  112.                     $data $emptyData instanceof \Closure $emptyData($form$data) : $emptyData;
  113.                 }
  114.                 // Convert the submitted data to a string, if scalar, before
  115.                 // casting it to an array
  116.                 if (!\is_array($data)) {
  117.                     if ($options['multiple']) {
  118.                         throw new TransformationFailedException('Expected an array.');
  119.                     }
  120.                     $data = (array) (string) $data;
  121.                 }
  122.                 // A map from submitted values to integers
  123.                 $valueMap array_flip($data);
  124.                 // Make a copy of the value map to determine whether any unknown
  125.                 // values were submitted
  126.                 $unknownValues $valueMap;
  127.                 // Reconstruct the data as mapping from child names to values
  128.                 $knownValues = [];
  129.                 if ($options['expanded']) {
  130.                     /** @var FormInterface $child */
  131.                     foreach ($form as $child) {
  132.                         $value $child->getConfig()->getOption('value');
  133.                         // Add the value to $data with the child's name as key
  134.                         if (isset($valueMap[$value])) {
  135.                             $knownValues[$child->getName()] = $value;
  136.                             unset($unknownValues[$value]);
  137.                             continue;
  138.                         } else {
  139.                             $knownValues[$child->getName()] = null;
  140.                         }
  141.                     }
  142.                 } else {
  143.                     foreach ($data as $value) {
  144.                         if ($choiceList->getChoicesForValues([$value])) {
  145.                             $knownValues[] = $value;
  146.                             unset($unknownValues[$value]);
  147.                         }
  148.                     }
  149.                 }
  150.                 // The empty value is always known, independent of whether a
  151.                 // field exists for it or not
  152.                 unset($unknownValues['']);
  153.                 // Throw exception if unknown values were submitted (multiple choices will be handled in a different event listener below)
  154.                 if (\count($unknownValues) > && !$options['multiple']) {
  155.                     throw new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.'implode('", "'array_keys($unknownValues))));
  156.                 }
  157.                 $event->setData($knownValues);
  158.             });
  159.         }
  160.         if ($options['multiple']) {
  161.             $messageTemplate $options['invalid_message'] ?? 'The value {{ value }} is not valid.';
  162.             $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use (&$unknownValues$messageTemplate) {
  163.                 // Throw exception if unknown values were submitted
  164.                 if (\count($unknownValues) > 0) {
  165.                     $form $event->getForm();
  166.                     $clientDataAsString is_scalar($form->getViewData()) ? (string) $form->getViewData() : (\is_array($form->getViewData()) ? implode('", "'array_keys($unknownValues)) : \gettype($form->getViewData()));
  167.                     if (null !== $this->translator) {
  168.                         $message $this->translator->trans($messageTemplate, ['{{ value }}' => $clientDataAsString], 'validators');
  169.                     } else {
  170.                         $message strtr($messageTemplate, ['{{ value }}' => $clientDataAsString]);
  171.                     }
  172.                     $form->addError(new FormError($message$messageTemplate, ['{{ value }}' => $clientDataAsString], null, new TransformationFailedException(sprintf('The choices "%s" do not exist in the choice list.'$clientDataAsString))));
  173.                 }
  174.             });
  175.             // <select> tag with "multiple" option or list of checkbox inputs
  176.             $builder->addViewTransformer(new ChoicesToValuesTransformer($choiceList));
  177.         } else {
  178.             // <select> tag without "multiple" option or list of radio inputs
  179.             $builder->addViewTransformer(new ChoiceToValueTransformer($choiceList));
  180.         }
  181.         if ($options['multiple'] && $options['by_reference']) {
  182.             // Make sure the collection created during the client->norm
  183.             // transformation is merged back into the original collection
  184.             $builder->addEventSubscriber(new MergeCollectionListener(truetrue));
  185.         }
  186.         // To avoid issues when the submitted choices are arrays (i.e. array to string conversions),
  187.         // we have to ensure that all elements of the submitted choice data are NULL, strings or ints.
  188.         $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
  189.             $data $event->getData();
  190.             if (!\is_array($data)) {
  191.                 return;
  192.             }
  193.             foreach ($data as $v) {
  194.                 if (null !== $v && !\is_string($v) && !\is_int($v)) {
  195.                     throw new TransformationFailedException('All choices submitted must be NULL, strings or ints.');
  196.                 }
  197.             }
  198.         }, 256);
  199.     }
  200.     /**
  201.      * {@inheritdoc}
  202.      */
  203.     public function buildView(FormView $viewFormInterface $form, array $options)
  204.     {
  205.         $choiceTranslationDomain $options['choice_translation_domain'];
  206.         if ($view->parent && null === $choiceTranslationDomain) {
  207.             $choiceTranslationDomain $view->vars['translation_domain'];
  208.         }
  209.         /** @var ChoiceListInterface $choiceList */
  210.         $choiceList $form->getConfig()->getAttribute('choice_list');
  211.         /** @var ChoiceListView $choiceListView */
  212.         $choiceListView $form->getConfig()->hasAttribute('choice_list_view')
  213.             ? $form->getConfig()->getAttribute('choice_list_view')
  214.             : $this->createChoiceListView($choiceList$options);
  215.         $view->vars array_replace($view->vars, [
  216.             'multiple' => $options['multiple'],
  217.             'expanded' => $options['expanded'],
  218.             'preferred_choices' => $choiceListView->preferredChoices,
  219.             'choices' => $choiceListView->choices,
  220.             'separator' => '-------------------',
  221.             'placeholder' => null,
  222.             'choice_translation_domain' => $choiceTranslationDomain,
  223.             'choice_translation_parameters' => $options['choice_translation_parameters'],
  224.         ]);
  225.         // The decision, whether a choice is selected, is potentially done
  226.         // thousand of times during the rendering of a template. Provide a
  227.         // closure here that is optimized for the value of the form, to
  228.         // avoid making the type check inside the closure.
  229.         if ($options['multiple']) {
  230.             $view->vars['is_selected'] = function ($choice, array $values) {
  231.                 return \in_array($choice$valuestrue);
  232.             };
  233.         } else {
  234.             $view->vars['is_selected'] = function ($choice$value) {
  235.                 return $choice === $value;
  236.             };
  237.         }
  238.         // Check if the choices already contain the empty value
  239.         $view->vars['placeholder_in_choices'] = $choiceListView->hasPlaceholder();
  240.         // Only add the empty value option if this is not the case
  241.         if (null !== $options['placeholder'] && !$view->vars['placeholder_in_choices']) {
  242.             $view->vars['placeholder'] = $options['placeholder'];
  243.         }
  244.         if ($options['multiple'] && !$options['expanded']) {
  245.             // Add "[]" to the name in case a select tag with multiple options is
  246.             // displayed. Otherwise only one of the selected options is sent in the
  247.             // POST request.
  248.             $view->vars['full_name'] .= '[]';
  249.         }
  250.     }
  251.     /**
  252.      * {@inheritdoc}
  253.      */
  254.     public function finishView(FormView $viewFormInterface $form, array $options)
  255.     {
  256.         if ($options['expanded']) {
  257.             // Radio buttons should have the same name as the parent
  258.             $childName $view->vars['full_name'];
  259.             // Checkboxes should append "[]" to allow multiple selection
  260.             if ($options['multiple']) {
  261.                 $childName .= '[]';
  262.             }
  263.             foreach ($view as $childView) {
  264.                 $childView->vars['full_name'] = $childName;
  265.             }
  266.         }
  267.     }
  268.     /**
  269.      * {@inheritdoc}
  270.      */
  271.     public function configureOptions(OptionsResolver $resolver)
  272.     {
  273.         $emptyData = function (Options $options) {
  274.             if ($options['expanded'] && !$options['multiple']) {
  275.                 return null;
  276.             }
  277.             if ($options['multiple']) {
  278.                 return [];
  279.             }
  280.             return '';
  281.         };
  282.         $placeholderDefault = function (Options $options) {
  283.             return $options['required'] ? null '';
  284.         };
  285.         $placeholderNormalizer = function (Options $options$placeholder) {
  286.             if ($options['multiple']) {
  287.                 // never use an empty value for this case
  288.                 return null;
  289.             } elseif ($options['required'] && ($options['expanded'] || isset($options['attr']['size']) && $options['attr']['size'] > 1)) {
  290.                 // placeholder for required radio buttons or a select with size > 1 does not make sense
  291.                 return null;
  292.             } elseif (false === $placeholder) {
  293.                 // an empty value should be added but the user decided otherwise
  294.                 return null;
  295.             } elseif ($options['expanded'] && '' === $placeholder) {
  296.                 // never use an empty label for radio buttons
  297.                 return 'None';
  298.             }
  299.             // empty value has been set explicitly
  300.             return $placeholder;
  301.         };
  302.         $compound = function (Options $options) {
  303.             return $options['expanded'];
  304.         };
  305.         $choiceTranslationDomainNormalizer = function (Options $options$choiceTranslationDomain) {
  306.             if (true === $choiceTranslationDomain) {
  307.                 return $options['translation_domain'];
  308.             }
  309.             return $choiceTranslationDomain;
  310.         };
  311.         $resolver->setDefaults([
  312.             'multiple' => false,
  313.             'expanded' => false,
  314.             'choices' => [],
  315.             'choice_filter' => null,
  316.             'choice_loader' => null,
  317.             'choice_label' => null,
  318.             'choice_name' => null,
  319.             'choice_value' => null,
  320.             'choice_attr' => null,
  321.             'choice_translation_parameters' => [],
  322.             'preferred_choices' => [],
  323.             'group_by' => null,
  324.             'empty_data' => $emptyData,
  325.             'placeholder' => $placeholderDefault,
  326.             'error_bubbling' => false,
  327.             'compound' => $compound,
  328.             // The view data is always a string or an array of strings,
  329.             // even if the "data" option is manually set to an object.
  330.             // See https://github.com/symfony/symfony/pull/5582
  331.             'data_class' => null,
  332.             'choice_translation_domain' => true,
  333.             'trim' => false,
  334.             'invalid_message' => function (Options $options$previousValue) {
  335.                 return ($options['legacy_error_messages'] ?? true)
  336.                     ? $previousValue
  337.                     'The selected choice is invalid.';
  338.             },
  339.         ]);
  340.         $resolver->setNormalizer('placeholder'$placeholderNormalizer);
  341.         $resolver->setNormalizer('choice_translation_domain'$choiceTranslationDomainNormalizer);
  342.         $resolver->setAllowedTypes('choices', ['null''array'\Traversable::class]);
  343.         $resolver->setAllowedTypes('choice_translation_domain', ['null''bool''string']);
  344.         $resolver->setAllowedTypes('choice_loader', ['null'ChoiceLoaderInterface::class, ChoiceLoader::class]);
  345.         $resolver->setAllowedTypes('choice_filter', ['null''callable''string'PropertyPath::class, ChoiceFilter::class]);
  346.         $resolver->setAllowedTypes('choice_label', ['null''bool''callable''string'PropertyPath::class, ChoiceLabel::class]);
  347.         $resolver->setAllowedTypes('choice_name', ['null''callable''string'PropertyPath::class, ChoiceFieldName::class]);
  348.         $resolver->setAllowedTypes('choice_value', ['null''callable''string'PropertyPath::class, ChoiceValue::class]);
  349.         $resolver->setAllowedTypes('choice_attr', ['null''array''callable''string'PropertyPath::class, ChoiceAttr::class]);
  350.         $resolver->setAllowedTypes('choice_translation_parameters', ['null''array''callable'ChoiceTranslationParameters::class]);
  351.         $resolver->setAllowedTypes('preferred_choices', ['array'\Traversable::class, 'callable''string'PropertyPath::class, PreferredChoice::class]);
  352.         $resolver->setAllowedTypes('group_by', ['null''callable''string'PropertyPath::class, GroupBy::class]);
  353.     }
  354.     /**
  355.      * {@inheritdoc}
  356.      */
  357.     public function getBlockPrefix()
  358.     {
  359.         return 'choice';
  360.     }
  361.     /**
  362.      * Adds the sub fields for an expanded choice field.
  363.      */
  364.     private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
  365.     {
  366.         foreach ($choiceViews as $name => $choiceView) {
  367.             // Flatten groups
  368.             if (\is_array($choiceView)) {
  369.                 $this->addSubForms($builder$choiceView$options);
  370.                 continue;
  371.             }
  372.             if ($choiceView instanceof ChoiceGroupView) {
  373.                 $this->addSubForms($builder$choiceView->choices$options);
  374.                 continue;
  375.             }
  376.             $this->addSubForm($builder$name$choiceView$options);
  377.         }
  378.     }
  379.     private function addSubForm(FormBuilderInterface $builderstring $nameChoiceView $choiceView, array $options)
  380.     {
  381.         $choiceOpts = [
  382.             'value' => $choiceView->value,
  383.             'label' => $choiceView->label,
  384.             'label_html' => $options['label_html'],
  385.             'attr' => $choiceView->attr,
  386.             'label_translation_parameters' => $choiceView->labelTranslationParameters,
  387.             'translation_domain' => $options['choice_translation_domain'],
  388.             'block_name' => 'entry',
  389.         ];
  390.         if ($options['multiple']) {
  391.             $choiceType CheckboxType::class;
  392.             // The user can check 0 or more checkboxes. If required
  393.             // is true, they are required to check all of them.
  394.             $choiceOpts['required'] = false;
  395.         } else {
  396.             $choiceType RadioType::class;
  397.         }
  398.         $builder->add($name$choiceType$choiceOpts);
  399.     }
  400.     private function createChoiceList(array $options)
  401.     {
  402.         if (null !== $options['choice_loader']) {
  403.             return $this->choiceListFactory->createListFromLoader(
  404.                 $options['choice_loader'],
  405.                 $options['choice_value'],
  406.                 $options['choice_filter']
  407.             );
  408.         }
  409.         // Harden against NULL values (like in EntityType and ModelType)
  410.         $choices null !== $options['choices'] ? $options['choices'] : [];
  411.         return $this->choiceListFactory->createListFromChoices(
  412.             $choices,
  413.             $options['choice_value'],
  414.             $options['choice_filter']
  415.         );
  416.     }
  417.     private function createChoiceListView(ChoiceListInterface $choiceList, array $options)
  418.     {
  419.         return $this->choiceListFactory->createView(
  420.             $choiceList,
  421.             $options['preferred_choices'],
  422.             $options['choice_label'],
  423.             $options['choice_name'],
  424.             $options['group_by'],
  425.             $options['choice_attr'],
  426.             $options['choice_translation_parameters']
  427.         );
  428.     }
  429. }