Search code examples
symfonysymfony-formssymfony-2.3

Multiple, dependent one-to-many relations in Symfony forms


I'm trying to get a multi-relation (multiple, dependent one-to-many relations) form working, but no success. I'm using Symfony 2.3 with FOSUserbundle.

Entity User

    use FOS\UserBundle\Entity\User as BaseUser;
    [...]

    /**
     * @ORM\Entity
     * @Gedmo\Loggable
     * @ORM\Table(name="ta_user", indexes={@ORM\Index(name="IDX_LOGIN_TOKEN", columns={"login_token"})})
     */
    class User extends BaseUser
    {
            [...]

        /**
         * @ORM\OneToMany(targetEntity="UserLifestyle", mappedBy="user", fetch="LAZY", cascade={"persist", "remove"})
         */
        protected $lifestyle;

UserManager

    use Doctrine\ORM\EntityManager;
    use FOS\UserBundle\Entity\UserManager as BaseUserManager;
    use Acme\UserBundle\Entity\LifestyleQuestion;
    use Acme\UserBundle\Entity\UserLifestyle;
    [...]

    class UserManager extends BaseUserManager {
        public function createUser() {
            $user = parent::createUser();
            $lifestyle = new UserLifestyle();
            $lifestyle->setQuestion($this->objectManager->getReference('Acme\UserBundle\Entity\LifestyleQuestion', 1));
            $user->addLifeStyle($lifestyle);
            $lifestyle = new UserLifestyle();
            $lifestyle->setQuestion($this->objectManager->getReference('Acme\UserBundle\Entity\LifestyleQuestion', 2));
            $user->addLifeStyle($lifestyle);
            $lifestyle = new UserLifestyle();
            $lifestyle->setQuestion($this->objectManager->getReference('Acme\UserBundle\Entity\LifestyleQuestion', 3));
            $user->addLifeStyle($lifestyle);
            return $user;
        }

Entity UserLifestyle

    /**
     * @ORM\Entity
     * @Gedmo\Loggable
     * @ORM\Table(name="ta_user_lifestyle")
     */
    class UserLifestyle
    {
        /**
         * @ORM\Id
         * @ORM\Column(type="smallint")
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        protected $id;

        /**
         * @ORM\ManyToOne(targetEntity="User", inversedBy="lifestyle")
         * @ORM\JoinColumn(name="user_id")
         */
        protected $user;

        /**
         * @ORM\ManyToOne(targetEntity="LifestyleQuestion", inversedBy="answeredByUser")
         * @ORM\JoinColumn(name="question_id")
         */
        protected $question;

        /**
         * @ORM\ManyToOne(targetEntity="LifestyleAnswer", inversedBy="userAnswers")
         * @ORM\JoinColumn(name="answer_id")
         * @Gedmo\Versioned
         */
        protected $answer;

Then, there's a form type

    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Component\OptionsResolver\OptionsResolverInterface;
    use Doctrine\ORM\EntityRepository;

    class RegistrationType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                ->add('email', NULL, array('label' => 'E-Mail'))
                            [...]
                ->add('lifestyle', 'collection', array(
                    'type' => new RegistrationLifestyleType(),
                    'allow_add' => false,
                    'allow_delete' => false,
                    'label' => false,
                ))

and now there should be a related RegistrationLifestyleType. But I've no idea how it should look like. I expect, that there are three choice fields in my registration form, showing a question (as label) and bunch of answers (as choice field) related to these questions. The UserManager assigns three questions to a newly created user, so one can get a question with:

    $lifestyles = $user->getLifestyles();
    foreach ($lifestyles as $lifestyle) {
        $question = $lifestyle->getQuestion(); // echo $question->getQuestion();
        $answers = $lifestyle->getQuestion()->getAnswers(); // loop through $answers and echo $answer->getAnswer();
    }

But how I can modify the form type, to get this working. Important: my intention is to use built-in functionality as most as possible and trying to avoid inflating form types and others by injecting service containers and entity managers.


Solution

  • Found a solution, perhaps someone can use it. The problem seems, that LifestyleQuestion and LifestyleAnswer are 1:n relations at the same object (UserLifestyle), so Symfony does not know how to deal with it, even if I set the LifestyleQuestion to a specific question in UserManager already. Regarding https://stackoverflow.com/a/9729888/672452 one has to use form listeners, so the parent object is available in sub form. So here is my "simple" RegistrationLifestyleType (without using any injected container or manager):

    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\OptionsResolver\OptionsResolverInterface;
    use Symfony\Component\Form\FormBuilderInterface;
    use Doctrine\ORM\EntityRepository;
    use Symfony\Component\Form\FormEvents;
    use Symfony\Component\Form\FormEvent;
    use Symfony\Component\Security\Core\SecurityContext;
    
    class RegistrationLifestyleType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options) {
            $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($builder) {
                $form = $event->getForm();
                $lifestyle = $event->getData();
                if (!($lifestyle instanceof \Acme\UserBundle\Entity\UserLifestyle) || !$lifestyle->getQuestion()) return;
                $label = $lifestyle->getQuestion()->getQuestion();
                $questionId = $lifestyle->getQuestion()->getId();
                $form->add('answer', 'entity', array(
                    'class' => 'AcmeUserBundle:LifestyleAnswer',
                    'empty_value' => '',
                    'property' => 'answer',
                    'query_builder' => function(EntityRepository $er) use ($questionId) {
                        return $er
                            ->createQueryBuilder('t1')
                            ->andWhere('t1.question = :question')
                            ->setParameter('question', $questionId)
                            ->orderBy('t1.answer', 'ASC')
                        ;
                    },
                    'label' => $label,
                ));
            });
        }
    
        public function setDefaultOptions(OptionsResolverInterface $resolver)
        {
            $resolver->setDefaults(array(
                'data_class' => 'Acme\UserBundle\Entity\UserLifestyle',
                'error_bubbling' => false,
            ));
        }
    }