Symfony 2 Embedded Form Collection Many to Many

2020-05-19 05:37发布

问题:

I have 2 Entities - User and Group. They have a many-to-many relationship and Group is used to store a users' roles.

I'm trying to make a User edit form by adding a collection, I want to be able to add a new role by selecting it from a dropdown (limited to what's already in the DB)

UserType.php:

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('username')
            ->add('email')
            ->add('forename')
            ->add('surname')
            ->add('isActive')
            ->add('joinDate', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'))
            ->add('lastActive', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'))
            ->add('groups', 'collection', array(
                    'type' => new GroupType(),
                    'allow_add' => true,
                    ))
        ;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Sfox\CoreBundle\Entity\User'
        ));
    }
}

and GroupType.php:

class GroupType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name')
            ->add('role');
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
                "data_class" => 'Sfox\CoreBundle\Entity\Group'
                ));
    }
}

This displays the roles in the form in basic text boxes, but if I add an entry to the form, it will cascade persist a new entry into Groups and if I were to edit an entry, it would change the underlying Group data.

I tried making a GroupSelectType.php:

class GroupSelectType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('role', 'entity', array('class'=>'SfoxCoreBundle:Group', 'property'=>'name'));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
                "data_class" => 'Sfox\CoreBundle\Entity\Group'
                ));
    }
}

Adding the field as an "entity" type, this displays the correct select box (but with the default values) I cant seem to bind it to the UserType form!

All I want the form to do is modify the underlying 'groups' ArrayCollection in the User entity.

Does anyone know how I can achieve this?

回答1:

Well I worked out a solution for anyone else struggling with similar problems...

I had to create a custom form type and declare it as a service so I could pass in the Entity Manager. I then needed to make a dataTransformer to change my group objects into an integer for the form

Custom GroupSelectType:

class GroupSelectType extends AbstractType

{
    /**
     * @var ObjectManager
     */
    private $om;

    private $choices;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;

        // Build our choices array from the database
        $groups = $om->getRepository('SfoxCoreBundle:Group')->findAll();
        foreach ($groups as $group)
        {
            // choices[key] = label
            $this->choices[$group->getId()] = $group->getName() . " [". $group->getRole() ."]";
        }
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $transformer = new GroupToNumberTransformer($this->om);
        $builder->addModelTransformer($transformer);
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
                "choices" => $this->choices,
                ));
    }

    public function getParent()
    {
        return 'choice';
    }

    public function getName()
    {
        return 'group_select';
    }
}

In the constructor I'm getting all available groups and putting them into a "choices" array which is passed to the select box as an option.

You'll also notice I'm using a custom data transformer, this is to change the groupId (which is used in the rendering of the form) to a Group entity. I made the GroupSelectType a service as well and passed in the [@doctrine.orm.entity_manager]

services.yml (bundle config):

services:
    sfox_core.type.group_select:
        class: Sfox\CoreBundle\Form\Type\GroupSelectType
        arguments: [@doctrine.orm.entity_manager]
        tags:
          - { name: form.type, alias: group_select }

GroupToNumberTranformer.php

class GroupToNumberTransformer implements DataTransformerInterface
{
    /**
     * @var ObjectManager
     */
    private $om;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }

    /**
     * Transforms an object (group) to a string (number).
     *
     * @param  Group|null $group
     * @return string
     */
    public function transform($group)
    {
        if (null === $group) {
            return "";
        }

        return $group->getId();
    }

    /**
     * Transforms a string (number) to an object (group).
     *
     * @param  string $number
     * @return Group|null
     * @throws TransformationFailedException if object (group) is not found.
     */
    public function reverseTransform($number)
    {
        if (!$number) {
            return null;
        }

        $group = $this->om
        ->getRepository('SfoxCoreBundle:Group')
        ->findOneBy(array('id' => $number))
        ;

        if (null === $group) {
            throw new TransformationFailedException(sprintf(
                    'Group with ID "%s" does not exist!',
                    $number
            ));
        }

        return $group;
    }
}

And my modified UserType.php - Notice I'm using my custom form type "group_select" now as it's running as a service:

class UserType extends AbstractType
{
    private $entityManager;

    public function __construct($entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $transformer = new GroupToNumberTransformer($this->entityManager);

        $builder
            ->add('username')
            ->add('email')
            ->add('forename')
            ->add('surname')
            ->add('isActive')
            ->add('joinDate', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'))
            ->add('lastActive', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'));
        $builder
            ->add(
                $builder->create('groups', 'collection', array(
                    'type' => 'group_select',
                    'allow_add' => true,
                    'options' => array(
                            'multiple' => false,
                            'expanded' => false,
                            )
                    ))
        );
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Sfox\CoreBundle\Entity\User'
        ));
    }

    public function getName()
    {
        return 'sfox_corebundle_usertype';
    }
}