vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php line 38

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