【问题标题】:symfony2 form create new type combining collection and entitysymfony2 表单创建结合集合和实体的新类型
【发布时间】:2015-02-10 22:16:58
【问题描述】:

使用 symfony 2,我愿意创建一个新的字段类型,结合实体字段类型和集合字段类型的行为: - 如果用户选择现有实体,则新实体的集合为空 - 如果用户创建一个新实体,第一个字段不是必需的

您知道如何进行吗?我可以重用现有的 symfony 类型吗?我把逻辑放在哪里(如果是旧的,不需要收集,如果是新的,不需要实体)?

非常感谢

【问题讨论】:

    标签: forms symfony collections


    【解决方案1】:

    如果您需要使用新模板创建新的字段类型,您可以在此处查看如何执行此操作: http://symfony.com/doc/current/cookbook/form/create_custom_field_type.html

    【讨论】:

      【解决方案2】:

      对于这种行为,您实际上不需要一种全新的表单类型(尽管您当然可以根据需要创建一个)。

      查看Symfony dynamic form modification,它有一个根据实体是否为“新”来修改表单字段的示例。您可以以此为基础,然后根据需要进行修改。

      如果您在从控制器创建表单时已经知道想要什么,那么您可以传递选项来标记您想要显示的内容。例如,从您的控制器:

      $form = $this->createForm(
          new MyType(),
          $entity,
          array('show_my_entity_collection' => false)
      );
      

      然后在您的表单中输入:

      public function buildForm(FormBuilderInterface $builder, array $options)
      {
          if ($options['show_my_entity_collection'])
          {
              $builder->add('entity', 'entity', array(
                  'class' => 'MyBundle:MyEntity',
                  'required' => false,
                  'query_builder' => function(MyEntityRepository $repository) { 
                      return $repository->findAll();
                  }, 
              ));
          }
          // rest of form builder here
      }
      
      public function setDefaultOptions(OptionsResolverInterface $resolver)
      {
          $resolver->setDefaults(array(
              'data_class' => 'MyBundle\Entity\MyEntity',
              'show_my_entity_collection' => true,
          ));
      }
      

      【讨论】:

        【解决方案3】:

        我终于明白了!哇,这不是那么容易。

        所以基本上,当表单类型是实体类型时,使用 javascript 添加一个新条目会触发 Symfony\Component\Form\Exception\TransformationFailedException。

        此异常来自 ChoicesToValuesTransformer 的 reverseTransform 方法中的 ChoiceListInterface 上调用的 getChoicesForValues 方法。这个 DataTransformer 用在 ChoiceType 中,所以为了克服这个问题,我必须构建一个扩展 ChoiceType 的新类型并只替换它的一小部分。

        使其工作的步骤:

        创建一个新类型:

        <?php
        
        namespace AppBundle\Form\Type;
        
        use AppBundle\Form\DataTransformer\ChoicesToValuesTransformer;
        use Doctrine\Common\Persistence\ObjectManager;
        use Doctrine\ORM\EntityManager;
        use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
        use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
        use Symfony\Component\Form\FormBuilderInterface;
        use Doctrine\Common\Persistence\ManagerRegistry;
        use Symfony\Component\Form\Exception\RuntimeException;
        use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
        use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
        use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
        use Symfony\Component\OptionsResolver\Options;
        use Symfony\Component\PropertyAccess\PropertyAccess;
        use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
        use Symfony\Component\Form\Extension\Core\View\ChoiceView;
        use Symfony\Component\Form\Exception\LogicException;
        use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener;
        use Symfony\Component\Form\Extension\Core\EventListener\FixCheckboxInputListener;
        use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
        use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
        use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer;
        use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer;
        use Symfony\Component\OptionsResolver\OptionsResolverInterface;
        
        class TagType extends ChoiceType
        {
            /**
             * @var ManagerRegistry
             */
            protected $registry;
        
            /**
             * @var array
             */
            private $choiceListCache = array();
        
            /**
             * @var PropertyAccessorInterface
             */
            private $propertyAccessor;
        
            /**
             * @var EntityManager
             */
            private $entityManager;
        
            public function __construct(EntityManager $entityManager, ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null)
            {
                $this->registry = $registry;
                $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
                $this->entityManager = $entityManager;
                $this->propertyAccessor = $propertyAccessor;
            }
        
            public function buildForm(FormBuilderInterface $builder, array $options)
            {
        
                if (!$options['choice_list'] && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) {
                    throw new LogicException('Either the option "choices" or "choice_list" must be set.');
                }
        
                if ($options['expanded']) {
                    // 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,
                    // the index check would read in one SQL query and the initialization
                    // requires another SQL query. When the initialization is done first,
                    // one SQL query is sufficient.
                    $preferredViews = $options['choice_list']->getPreferredViews();
                    $remainingViews = $options['choice_list']->getRemainingViews();
        
                    // Check if the choices already contain the empty value
                    // Only add the empty value option if this is not the case
                    if (null !== $options['placeholder'] && 0 === count($options['choice_list']->getChoicesForValues(array('')))) {
                        $placeholderView = new ChoiceView(null, '', $options['placeholder']);
        
                        // "placeholder" is a reserved index
                        $this->addSubForms($builder, array('placeholder' => $placeholderView), $options);
                    }
        
                    $this->addSubForms($builder, $preferredViews, $options);
                    $this->addSubForms($builder, $remainingViews, $options);
        
                    if ($options['multiple']) {
                        $builder->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list']));
                        $builder->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10);
                    } else {
                        $builder->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'], $builder->has('placeholder')));
                        $builder->addEventSubscriber(new FixRadioInputListener($options['choice_list'], $builder->has('placeholder')), 10);
                    }
                } else {
                    if ($options['multiple']) {
                        $builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list']));
                    } else {
                        $builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list']));
                    }
                }
        
                if ($options['multiple'] && $options['by_reference']) {
                    // Make sure the collection created during the client->norm
                    // transformation is merged back into the original collection
                    $builder->addEventSubscriber(new MergeCollectionListener(true, true));
                }
        
                if ($options['multiple']) {
                    $builder
                        ->addEventSubscriber(new MergeDoctrineCollectionListener())
                        ->addViewTransformer(new CollectionToArrayTransformer(), true)
                    ;
                }
            }
        
            public function setDefaultOptions(OptionsResolverInterface $resolver)
            {
                $choiceListCache = & $this->choiceListCache;
        
                $choiceList = function (Options $options) use (&$choiceListCache) {
                    // Harden against NULL values (like in EntityType and ModelType)
                    $choices = null !== $options['choices'] ? $options['choices'] : array();
        
                    // Reuse existing choice lists in order to increase performance
                    $hash = hash('sha256', serialize(array($choices, $options['preferred_choices'])));
        
                    if (!isset($choiceListCache[$hash])) {
                        $choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']);
                    }
        
                    return $choiceListCache[$hash];
                };
        
                $emptyData = function (Options $options) {
                    if ($options['multiple'] || $options['expanded']) {
                        return array();
                    }
        
                    return '';
                };
        
                $emptyValue = function (Options $options) {
                    return $options['required'] ? null : '';
                };
        
                // for BC with the "empty_value" option
                $placeholder = function (Options $options) {
                    return $options['empty_value'];
                };
        
                $placeholderNormalizer = function (Options $options, $placeholder) {
                    if ($options['multiple']) {
                        // never use an empty value for this case
                        return;
                    } elseif (false === $placeholder) {
                        // an empty value should be added but the user decided otherwise
                        return;
                    } elseif ($options['expanded'] && '' === $placeholder) {
                        // never use an empty label for radio buttons
                        return 'None';
                    }
        
                    // empty value has been set explicitly
                    return $placeholder;
                };
        
                $compound = function (Options $options) {
                    return $options['expanded'];
                };
        
                $resolver->setDefaults(array(
                        'multiple' => false,
                        'expanded' => false,
                        'choice_list' => $choiceList,
                        'choices' => array(),
                        'preferred_choices' => array(),
                        'empty_data' => $emptyData,
                        'empty_value' => $emptyValue, // deprecated
                        'placeholder' => $placeholder,
                        'error_bubbling' => false,
                        'compound' => $compound,
                        // The view data is always a string, even if the "data" option
                        // is manually set to an object.
                        // See https://github.com/symfony/symfony/pull/5582
                        'data_class' => null,
                    ));
        
                $resolver->setNormalizers(array(
                        'empty_value' => $placeholderNormalizer,
                        'placeholder' => $placeholderNormalizer,
                    ));
        
                $resolver->setAllowedTypes(array(
                        'choice_list' => array('null', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface'),
                    ));
        
                $choiceListCache = & $this->choiceListCache;
                $registry = $this->registry;
                $propertyAccessor = $this->propertyAccessor;
                $type = $this;
        
                $loader = function (Options $options) use ($type) {
                    if (null !== $options['query_builder']) {
                        return $type->getLoader($options['em'], $options['query_builder'], $options['class']);
                    }
                };
        
                $choiceList = function (Options $options) use (&$choiceListCache, $propertyAccessor) {
                    // Support for closures
                    $propertyHash = is_object($options['property'])
                        ? spl_object_hash($options['property'])
                        : $options['property'];
        
                    $choiceHashes = $options['choices'];
        
                    // Support for recursive arrays
                    if (is_array($choiceHashes)) {
                        // A second parameter ($key) is passed, so we cannot use
                        // spl_object_hash() directly (which strictly requires
                        // one parameter)
                        array_walk_recursive($choiceHashes, function (&$value) {
                                $value = spl_object_hash($value);
                            });
                    } elseif ($choiceHashes instanceof \Traversable) {
                        $hashes = array();
                        foreach ($choiceHashes as $value) {
                            $hashes[] = spl_object_hash($value);
                        }
        
                        $choiceHashes = $hashes;
                    }
        
                    $preferredChoiceHashes = $options['preferred_choices'];
        
                    if (is_array($preferredChoiceHashes)) {
                        array_walk_recursive($preferredChoiceHashes, function (&$value) {
                                $value = spl_object_hash($value);
                            });
                    }
        
                    // Support for custom loaders (with query builders)
                    $loaderHash = is_object($options['loader'])
                        ? spl_object_hash($options['loader'])
                        : $options['loader'];
        
                    // Support for closures
                    $groupByHash = is_object($options['group_by'])
                        ? spl_object_hash($options['group_by'])
                        : $options['group_by'];
        
                    $hash = hash('sha256', json_encode(array(
                                spl_object_hash($options['em']),
                                $options['class'],
                                $propertyHash,
                                $loaderHash,
                                $choiceHashes,
                                $preferredChoiceHashes,
                                $groupByHash,
                            )));
        
                    if (!isset($choiceListCache[$hash])) {
                        $choiceListCache[$hash] = new EntityChoiceList(
                            $options['em'],
                            $options['class'],
                            $options['property'],
                            $options['loader'],
                            $options['choices'],
                            $options['preferred_choices'],
                            $options['group_by'],
                            $propertyAccessor
                        );
                    }
        
                    return $choiceListCache[$hash];
                };
        
                $emNormalizer = function (Options $options, $em) use ($registry) {
                    /* @var ManagerRegistry $registry */
                    if (null !== $em) {
                        if ($em instanceof ObjectManager) {
                            return $em;
                        }
        
                        return $registry->getManager($em);
                    }
        
                    $em = $registry->getManagerForClass($options['class']);
        
                    if (null === $em) {
                        throw new RuntimeException(sprintf(
                                'Class "%s" seems not to be a managed Doctrine entity. '.
                                'Did you forget to map it?',
                                $options['class']
                            ));
                    }
        
                    return $em;
                };
        
                $resolver->setDefaults(array(
                        'em' => null,
                        'property' => null,
                        'query_builder' => null,
                        'loader' => $loader,
                        'choices' => null,
                        'choice_list' => $choiceList,
                        'group_by' => null,
                    ));
        
                $resolver->setRequired(array('class'));
        
                $resolver->setNormalizers(array(
                        'em' => $emNormalizer,
                    ));
        
                $resolver->setAllowedTypes(array(
                        'em' => array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager'),
                        'loader' => array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface'),
                    ));
            }
        
            /**
             * @return string
             */
            public function getName()
            {
                return 'fmu_tag';
            }
        
            /**
             * Return the default loader object.
             *
             * @param ObjectManager $manager
             * @param mixed         $queryBuilder
             * @param string        $class
             *
             * @return ORMQueryBuilderLoader
             */
            public function getLoader(ObjectManager $manager, $queryBuilder, $class)
            {
                return new ORMQueryBuilderLoader(
                    $queryBuilder,
                    $manager,
                    $class
                );
            }
        
            /**
             * Adds the sub fields for an expanded choice field.
             *
             * @param FormBuilderInterface $builder     The form builder.
             * @param array                $choiceViews The choice view objects.
             * @param array                $options     The build options.
             */
            private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
            {
                foreach ($choiceViews as $i => $choiceView) {
                    if (is_array($choiceView)) {
                        // Flatten groups
                        $this->addSubForms($builder, $choiceView, $options);
                    } else {
                        $choiceOpts = array(
                            'value' => $choiceView->value,
                            'label' => $choiceView->label,
                            'translation_domain' => $options['translation_domain'],
                            'block_name' => 'entry',
                        );
        
                        if ($options['multiple']) {
                            $choiceType = 'checkbox';
                            // 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);
                    }
                }
            }
        }
        

        在您的服务中注册类型:

        tag.type:
            class: %tag.type.class%
            arguments: [@doctrine.orm.entity_manager, @doctrine ,@property_accessor]
            tags:
                - { name: form.type, alias: fmu_tag }
        

        为复制选项的类型创建一个新视图:

        {#app/Resources/views/Form/fmu_tag.html.twig#}
        
        {% block fmu_tag_widget %}
            {% if expanded %}
                {{- block('choice_widget_expanded') -}}
            {% else %}
                {{- block('choice_widget_collapsed') -}}
            {% endif %}
        {% endblock %}
        

        在你的 twig config.yml 中注册视图:

        # Twig Configuration
        twig:
            form:
                resources:
                    - 'Form/fmu_tag.html.twig'
        

        创建一个新的 ChoiceToValueDataTransformer 替换choiceType中使用的默认类

        <?php
        
        /*
         * This file is part of the Symfony package.
         *
         * (c) Fabien Potencier <fabien@symfony.com>
         *
         * For the full copyright and license information, please view the LICENSE
         * file that was distributed with this source code.
         */
        
        namespace AppBundle\Form\DataTransformer;
        
        use AppBundle\Entity\Core\Tag;
        use Symfony\Component\Form\Exception\TransformationFailedException;
        use Symfony\Component\Form\DataTransformerInterface;
        use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
        
        /**
         * @author Bernhard Schussek <bschussek@gmail.com>
         */
        class ChoicesToValuesTransformer implements DataTransformerInterface
        {
            private $choiceList;
        
            /**
             * Constructor.
             *
             * @param ChoiceListInterface $choiceList
             */
            public function __construct(ChoiceListInterface $choiceList)
            {
                $this->choiceList = $choiceList;
            }
        
            /**
             * @param array $array
             *
             * @return array
             *
             * @throws TransformationFailedException If the given value is not an array.
             */
            public function transform($array)
            {
                if (null === $array) {
                    return array();
                }
        
                if (!is_array($array)) {
                    throw new TransformationFailedException('Expected an array.');
                }
        
                return $this->choiceList->getValuesForChoices($array);
            }
        
            /**
             * @param array $array
             *
             * @return array
             *
             * @throws TransformationFailedException If the given value is not an array
             *                                       or if no matching choice could be
             *                                       found for some given value.
             */
            public function reverseTransform($array)
            {
                if (null === $array) {
                    return array();
                }
        
                if (!is_array($array)) {
                    throw new TransformationFailedException('Expected an array.');
                }
        
                $choices = $this->choiceList->getChoicesForValues($array);
        
                if (count($choices) !== count($array)) {
                    $missingChoices = array_diff($array, $this->choiceList->getValues());
                    $choices = array_merge($choices, $this->transformMissingChoicesToEntities($missingChoices));
                }
        
        
                return $choices;
            }
        
            public function transformMissingChoicesToEntities(Array $missingChoices)
            {
                $newChoices = array_map(function($choice){
                        return new Tag($choice);
                    }, $missingChoices);
        
                return $newChoices;
            }
        
        }
        

        在这个文件的最后一个方法中掠夺:transformMissingChoicesToEntities 在这里,当我失踪时,我创建了一个新实体。所以如果你想使用这一切,你需要适应新的 Tag($choice) 即。用您自己的新实体替换它。

        因此,您添加集合的表单现在使用您的新类型:

        $builder
                    ->add('tags', 'fmu_tag', array(
                            'by_reference' => false,
                            'required' => false,
                            'class' => 'AppBundle\Entity\Core\Tag',
                            'multiple' => true,
                            'label'=>'Tags',
                        ));
        

        为了创建新的选择,我使用了 select2 控件。 在您的 javascripts 中添加文件:http://select2.github.io 在您的视图中添加以下代码:

        <script>
        
            $(function() {
        
                $('#appbundle_marketplace_product_ingredient_tags').select2({
                    closeOnSelect: false,
                    multiple: true,
                    placeholder: 'Tapez quelques lettres',
                    tags: true,
                    tokenSeparators: [',', ' ']
                });
        
            });
        
        </script>
        

        就是这样,您可以选择现有实体或从 select2 生成的新条目中创建新实体。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-02-13
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多