diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index 74042a7b41a31..d50d4cad2e268 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -84,6 +84,13 @@ class EntityChoiceList extends ObjectChoiceList */ private $preferredEntities = array(); + /** + * Whether or not the choice list shoud be loaded. + * + * @var Boolean + */ + private $lazy; + /** * Creates a new entity choice list. * @@ -97,22 +104,27 @@ class EntityChoiceList extends ObjectChoiceList * to group the choices. Only allowed if * the choices are given as flat array. * @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths. + * @param Boolean $lazy Whether or not the choice list shoud be loaded. */ - public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, array $preferredEntities = array(), $groupPath = null, PropertyAccessorInterface $propertyAccessor = null) + public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, array $preferredEntities = array(), $groupPath = null, PropertyAccessorInterface $propertyAccessor = null, $lazy = false) { $this->em = $manager; $this->entityLoader = $entityLoader; $this->classMetadata = $manager->getClassMetadata($class); $this->class = $this->classMetadata->getName(); $this->loaded = is_array($entities) || $entities instanceof \Traversable; + $this->lazy = $lazy; $this->preferredEntities = $preferredEntities; $identifier = $this->classMetadata->getIdentifierFieldNames(); if (1 === count($identifier)) { - $this->idField = $identifier[0]; - $this->idAsValue = true; - + if (!$lazy) { + $this->idField = $identifier[0]; + $this->idAsValue = true; + } else { + $this->idField = (string) $labelPath; + } if (in_array($this->classMetadata->getTypeOfField($this->idField), array('integer', 'smallint', 'bigint'))) { $this->idAsIndex = true; } @@ -124,7 +136,7 @@ public function __construct(ObjectManager $manager, $class, $labelPath = null, E $entities = array(); } - parent::__construct($entities, $labelPath, $preferredEntities, $groupPath, null, $propertyAccessor); + parent::__construct($entities, $labelPath, $preferredEntities, $groupPath, $labelPath, $propertyAccessor); } /** @@ -215,6 +227,10 @@ public function getChoicesForValues(array $values) return $this->entityLoader->getEntitiesByIds($this->idField, $values); } + if ($this->lazy) { + return $this->em->getRepository($this->class)->findBy(array($this->idField => $values)); + } + $this->load(); } @@ -237,13 +253,13 @@ public function getValuesForChoices(array $entities) // know that the IDs are used as values // Attention: This optimization does not check choices for existence - if ($this->idAsValue) { + if ($this->idAsValue || $this->lazy) { $values = array(); foreach ($entities as $entity) { if ($entity instanceof $this->class) { // Make sure to convert to the right format - $values[] = $this->fixValue(current($this->getIdentifierValues($entity))); + $values[] = $this->fixValue($this->createValue($entity)); } } @@ -379,6 +395,10 @@ protected function fixIndex($index) */ private function load() { + if ($this->lazy) { + return; + } + if ($this->entityLoader) { $entities = $this->entityLoader->getEntities(); } else { diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 81623966c1087..f86005e998ada 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Doctrine\Form\Type; use Doctrine\Common\Persistence\ManagerRegistry; +use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Exception\RuntimeException; use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\FormBuilderInterface; @@ -50,6 +51,9 @@ public function __construct(ManagerRegistry $registry, PropertyAccessorInterface public function buildForm(FormBuilderInterface $builder, array $options) { + if ('text' === $options['widget'] && !$options['property']) { + throw new LogicException('When using a text widget, the "property" option must be defined.'); + } if ($options['multiple']) { $builder ->addEventSubscriber(new MergeDoctrineCollectionListener()) @@ -116,7 +120,8 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) $loaderHash, $choiceHashes, $preferredChoiceHashes, - $groupByHash + $groupByHash, + 'text' === $options['widget'] ))); if (!isset($choiceListCache[$hash])) { @@ -128,7 +133,8 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) $options['choices'], $options['preferred_choices'], $options['group_by'], - $propertyAccessor + $propertyAccessor, + 'text' === $options['widget'] ); } diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 453c29c65b218..e6be2a21f8e01 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -46,10 +46,12 @@ {% block choice_widget %} {% spaceless %} - {% if expanded %} + {% if widget == 'checkbox' or widget == 'radio' %} {{ block('choice_widget_expanded') }} + {% elseif widget == 'select' %} + {{ block('choice_widget_select') }} {% else %} - {{ block('choice_widget_collapsed') }} + {{ block('form_widget_simple') }} {% endif %} {% endspaceless %} {% endblock choice_widget %} @@ -65,7 +67,7 @@ {% endspaceless %} {% endblock choice_widget_expanded %} -{% block choice_widget_collapsed %} +{% block choice_widget_select %} {% spaceless %} {% endspaceless %} -{% endblock choice_widget_collapsed %} +{% endblock choice_widget_select %} {% block choice_widget_options %} {% spaceless %} @@ -388,3 +390,7 @@ {% for attrname, attrvalue in attr %}{{ attrname }}="{{ attrvalue }}" {% endfor %} {% endspaceless %} {% endblock button_attributes %} + +{# Deprecated in Symfony 2.4, to be removed in 3.0 #} + +{% block choice_widget_collapsed %}{{ block('choice_widget_select') }}{% endblock %} diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/ValuesToStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ValuesToStringTransformer.php new file mode 100644 index 0000000000000..4c67cbb628200 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/ValuesToStringTransformer.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core\DataTransformer; + +use Symfony\Component\Form\Exception\TransformationFailedException; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * Converts an array of values to a string with multiple values separated by a delimiter. + * + * @author Bilal Amarni + */ +class ValuesToStringTransformer implements DataTransformerInterface +{ + private $delimiter; + private $trim; + + public function __construct($delimiter, $trim) + { + $this->delimiter = $delimiter; + $this->trim = $trim; + } + + /** + * @param array $array + * + * @return string + * + * @throws UnexpectedTypeException if the given value is not an array + */ + public function transform($array) + { + if (null === $array) { + return array(); + } + + if (!is_array($array)) { + throw new UnexpectedTypeException($array, 'array'); + } + + return implode($this->delimiter, $array); + } + + /** + * @param string $string + * + * @return array + * + * @throws UnexpectedTypeException if the given value is not a string + */ + public function reverseTransform($string) + { + if (empty($string)) { + return array(); + } + + if (!is_string($string)) { + throw new UnexpectedTypeException($string, 'string'); + } + + $values = explode($this->delimiter, $string); + + if ($this->trim) { + $values = array_map('trim', $values); + } + + return $values; + } +} diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 9a3fdef12b81d..2c504fe61cfeb 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -25,6 +25,7 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\ValuesToStringTransformer; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolverInterface; @@ -45,7 +46,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) throw new LogicException('Either the option "choices" or "choice_list" must be set.'); } - if ($options['expanded']) { + if (in_array($options['widget'], array('radio', 'checkbox'))) { // Initialize all choices before doing the index check below. // This helps in cases where index checks are optimized for non // initialized choice lists. For example, when using an SQL driver, @@ -68,7 +69,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) $this->addSubForms($builder, $preferredViews, $options); $this->addSubForms($builder, $remainingViews, $options); - if ($options['multiple']) { + if ('checkbox' === $options['widget']) { $builder->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list'])); $builder->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10); } else { @@ -78,6 +79,9 @@ public function buildForm(FormBuilderInterface $builder, array $options) } else { if ($options['multiple']) { $builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list'])); + if ('text' === $options['widget']) { + $builder->addViewTransformer(new ValuesToStringTransformer($options['delimiter'], $options['trim'])); + } } else { $builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list'])); } @@ -96,6 +100,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) public function buildView(FormView $view, FormInterface $form, array $options) { $view->vars = array_replace($view->vars, array( + 'widget' => $options['widget'], 'multiple' => $options['multiple'], 'expanded' => $options['expanded'], 'preferred_choices' => $options['choice_list']->getPreferredViews(), @@ -124,7 +129,7 @@ public function buildView(FormView $view, FormInterface $form, array $options) $view->vars['empty_value'] = $options['empty_value']; } - if ($options['multiple'] && !$options['expanded']) { + if ($options['multiple'] && 'select' === $options['widget']) { // Add "[]" to the name in case a select tag with multiple options is // displayed. Otherwise only one of the selected options is sent in the // POST request. @@ -137,12 +142,12 @@ public function buildView(FormView $view, FormInterface $form, array $options) */ public function finishView(FormView $view, FormInterface $form, array $options) { - if ($options['expanded']) { + if (in_array($options['widget'], array('radio', 'checkbox'))) { // Radio buttons should have the same name as the parent $childName = $view->vars['full_name']; // Checkboxes should append "[]" to allow multiple selection - if ($options['multiple']) { + if ('checkbox' === $options['widget']) { $childName .= '[]'; } @@ -174,7 +179,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) }; $emptyData = function (Options $options) { - if ($options['multiple'] || $options['expanded']) { + if ($options['multiple'] || 'radio' === $options['widget']) { return array(); } @@ -192,7 +197,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) } elseif (false === $emptyValue) { // an empty value should be added but the user decided otherwise return null; - } elseif ($options['expanded'] && '' === $emptyValue) { + } elseif ('radio' === $options['widget'] && '' === $emptyValue) { // never use an empty label for radio buttons return 'None'; } @@ -201,13 +206,38 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) return $emptyValue; }; + $widgetDefault = function (Options $options) { + if ($options['expanded']) { + if ($options['multiple']) { + return 'checkbox'; + } + + return 'radio'; + } + + return 'select'; + }; + + $multipleNormalizer = function (Options $options, $multiple) { + if (in_array($options['widget'], array('radio', 'checkbox'))) { + return ('checkbox' === $options['widget']); + } + + return $multiple; + }; + + $expandedNormalizer = function (Options $options) { + return in_array($options['widget'], array('radio', 'checkbox')); + }; + $compound = function (Options $options) { - return $options['expanded']; + return in_array($options['widget'], array('radio', 'checkbox')); }; $resolver->setDefaults(array( + 'widget' => $widgetDefault, + 'delimiter' => ',', 'multiple' => false, - 'expanded' => false, 'choice_list' => $choiceList, 'choices' => array(), 'preferred_choices' => array(), @@ -219,15 +249,23 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) // is manually set to an object. // See https://github.com/symfony/symfony/pull/5582 'data_class' => null, + // Deprecated, to be removed in 3.0 + 'expanded' => false, )); $resolver->setNormalizers(array( 'empty_value' => $emptyValueNormalizer, + 'multiple' => $multipleNormalizer, + 'expanded' => $expandedNormalizer, // For BC )); $resolver->setAllowedTypes(array( 'choice_list' => array('null', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface'), )); + + $resolver->setAllowedValues(array( + 'widget' => array('select', 'checkbox', 'radio', 'text') + )); } /** @@ -258,16 +296,13 @@ private function addSubForms(FormBuilderInterface $builder, array $choiceViews, 'translation_domain' => $options['translation_domain'], ); - if ($options['multiple']) { - $choiceType = 'checkbox'; + if ('checkbox' === $options['widget']) { // The user can check 0 or more checkboxes. If required // is true, he is required to check all of them. $choiceOpts['required'] = false; - } else { - $choiceType = 'radio'; } - $builder->add($i, $choiceType, $choiceOpts); + $builder->add($i, $options['widget'], $choiceOpts); } } }