symfony2表单创建组合集合和实体的新类型

时间:2014-12-12 08:33:54

标签: forms symfony collections

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

你知道如何继续吗?我可以重用现有的symfony类型吗?我在哪里放置逻辑(如果旧的,不需要集合,如果不需要新的实体)?

Thansk很多

3 个答案:

答案 0 :(得分:6)

我终于明白了!哇,这并不容易。

所以基本上,当表单类型是实体类型时,使用javascript向select中添加一个新条目会触发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 这是我失踪的地方,我创建了一个新的实体。因此,如果您想要使用所有这些,您需要调整新标签($ 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生成的新条目中创建新实体。

答案 1 :(得分:1)

对于这种行为,您不应该真正需要一种全新的表单类型(尽管如果您愿意,您当然可以创建一种)。

查看Symfony dynamic form modification,其中包含根据实体是否为“新”修改表单字段的示例。您可以从此开始,并根据您的需要进行修改。

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

$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,
    ));
}

答案 2 :(得分:0)

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