1
votes

Names changed due to NDA.

I'm trying to come up with a survey form. Each survey question can have multiple answers/scores, so there's a natural 1:* relationship to them. That said, for the public-facing form, I need to have a 1:1 relationship between the score and the question it relates to, which is what I'm working on now. Right now, the survey is open to the public, so each completed survey is not related to a user.

The interesting parts of my current setup are as follows...

Question:

namespace Acme\MyBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

class Question
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var string question
     *
     * @ORM\Column(name="question", type="string", length=255)
     */
    private $question;

    /**
     * @var ArrayCollection scores
     *
     * @ORM\OneToMany(targetEntity="Score", mappedBy="question")
     */
    private $scores;

    public function __construct()
    {
        $this->scores = new ArrayCollection();
    }

    // other getters and setters

    /**
     * @param $score
     */
    public function setScore($score)
    {
        $this->scores->add($score);
    }

    /**
     * @return mixed
     */
    public function getScore()
    {
        if (get_class($this->scores) === 'ArrayCollection') {
            return $this->scores->current();
        } else {
            return $this->scores;
        }
    }
}

Those last two are helper methods so I can add/retrieve individual scores. The type checking convolutions were due to an error I encountered here

Score:

namespace Acme\MyBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

class Score
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var integer $question
     *
     * @ORM\ManyToOne(targetEntity="Question", inversedBy="scores")
     * @ORM\JoinColumn(name="question_id", referencedColumnName="id")
     */
    private $question;

    /**
     * @var float score
     *
     * @ORM\Column(name="score", type="float")
     */
    private $score;

    // getters and setters
}

Controller method:

public function takeSurveyAction(Request $request)
{
    $em = $this->get('doctrine')->getManager();
    $questions = $em->getRepository('Acme\MyBundle\Entity\Question')->findAll();
    $viewQuestions = array();

    foreach ($questions as $question) {
        $viewQuestions[] = $question;
        $rating = new Score();
        $rating->setQuestion($question->getId());
        $question->setRatings($rating);
    }

    $form = $this->createForm(new SurveyType(), array('questions' => $questions));

    if ('POST' === $request->getMethod()) {
        $form->bind($request);

        if ($form->isValid()) {
            foreach ($questions as $q) {
                $em->persist($q);
            }

            $em->flush();
            $em->clear();

            $url = $this->get('router')->generate('_main');
            $response = new RedirectResponse($url);

            return $response;
        }
    }

    return $this->render('MyBundle:Survey:take.html.twig', array('form' => $form->createView(), 'questions' => $viewQuestions));
}

My form types....

SurveyType:

namespace Acme\MyBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class SurveyType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('questions', 'collection', array('type' => new SurveyListItemType()));
    }

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

SurveyListItemType:

namespace Acme\MyBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class SurveyListItemType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('rating', new SurveyScoreType());
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Question'));
    }

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

SurveyScoreType:

namespace Acme\MyBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class SurveyRatingType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('score', 'choice', array('choices' => array(
                             '0' => '',
                             '0.5' => '',
                             '1' => '',
                             '1.5' => '',
                             '2' => '',
                             '2.5' => '',
                             '3' => '',
                             '3.5' => '',
                             '4' => '',
                             '4.5' => '',
                             '5' => ''
                         ), 'expanded' => true, 'multiple' => false));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Score'));
    }

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

Okay, with all of that, I'm getting the following error when Doctrine's EntityManager attempts to flush() in my controller action:

Catchable Fatal Error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given, called in /home/kevin/www/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 547 and defined in /home/kevin/www/project/vendor/doctrine/collections/lib/Doctrine/Common/Collections/ArrayCollection.php line 47

I believe it has to do with the questions' related scores, as they're supposed to be an array(collection) in Question, but they're individual instances in this case. The only problem is I'm not sure how to fix it.

I'm thinking my form setup may be too complex. All I really need to do is attach each Question.id to each related Score. I'm just not sure the best way to build the form part of it so everything is persisted properly.

3

3 Answers

1
votes

I believe your error is here

    $rating = new Score();
    //...
    $question->setRatings($rating);

Usually if you have an ArrayCollection in your Entity, then you have addChildEntity and removeChildEntity methods that add and remove elements from the ArrayCollection.

setRatings() would take an array of entities, rather than a single entity.

Assuming that you do have this method, try

$question->addRating($rating);
0
votes

I think you have a mistake in your setRating method.

You have

 $this->score->add($score);

It should be:

  $this->scores->add($score);
0
votes

I was able to solve it by simply handling the Scores. So, with that approach, I was able to remove SurveyListItemType, and make the following changes:

SurveyType:

namespace Acme\MyBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class SurveyType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('scores', 'collection', array('type' => new SurveyRatingType()));
    }

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

Note how the collection type is now mapped to SurveyRatingType.

SurveyRatingType:

namespace Acme\MyBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class SurveyRatingType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('score', 'choice', array('choices' => array(
                             '0' => '',
                             '0.5' => '',
                             '1' => '',
                             '1.5' => '',
                             '2' => '',
                             '2.5' => '',
                             '3' => '',
                             '3.5' => '',
                             '4' => '',
                             '4.5' => '',
                             '5' => ''
                         ), 'expanded' => true, 'multiple' => false));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    { 
        $resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Score'));
    }

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

And my modified controller action:

public function takeSurveyAction(Request $request)
{
    $em = $this->get('doctrine')->getManager();
    $questions = $em->getRepository('Acme\MyBundle\Entity\Question')->findAll();
    $ratings = array();

    foreach ($questions as $question) {
        $rating = new SurveyRating();
        $rating->setQuestion($question);
        $ratings[] = $rating;
    }

    $form = $this->createForm(new SurveyType(), array('ratings' => $ratings));

    if ('POST' === $request->getMethod()) {
        $form->bind($request);

        if ($form->isValid()) {
            foreach ($ratings as $r) {
                $em->persist($r);
            }

            $em->flush();
            $em->clear();

            $url = $this->get('router')->generate('_main');
            $response = new RedirectResponse($url);

            return $response;
        }
    }

    return $this->render('MyBundle:Survey:take.html.twig', array('form' => $form->createView(), 'questions' => $questions));
}

I had a feeling I was doing it wrong due to the three form types. That really jumped out as a bad code smell. Thanks to everyone for their patience and attempts at helping. :)