Search code examples
phpformssymfonydoctrine-ormentity

Symfony entity field : manyToMany with multiple = false - field not populated correctly


I am using symfony2 with doctrine 2. I have a many to many relationship between two entities :

/**
 * @ORM\ManyToMany(targetEntity="\AppBundle\Entity\Social\PostCategory", inversedBy="posts")
 * @ORM\JoinTable(
 *     name="post_postcategory",
 *     joinColumns={@ORM\JoinColumn(name="postId", referencedColumnName="id", onDelete="CASCADE")},
 *     inverseJoinColumns={@ORM\JoinColumn(name="postCategoryId", referencedColumnName="id", onDelete="CASCADE")}
 * )
 */
private $postCategories;

Now I want to let the user only select one category. For this I use the option 'multiple' => false in my form.

My form:

        ->add('postCategories', 'entity', array(
                'label'=> 'Catégorie',
                'required' => true,
                'empty_data' => false,
                'empty_value' => 'Sélectionnez une catégorie',
                'class' => 'AppBundle\Entity\Social\PostCategory',
                'multiple' => false,
                'by_reference' => false,
                'query_builder' => $queryBuilder,
                'position' => array('before' => 'name'),
                'attr' => array(
                    'data-toggle'=>"tooltip",
                    'data-placement'=>"top",
                    'title'=>"Choisissez la catégorie dans laquelle publier le feedback",
                )))

This first gave me errors when saving and I had to change the setter as following :

/**
 * @param \AppBundle\Entity\Social\PostCategory $postCategories
 *
 * @return Post
 */
public function setPostCategories($postCategories)
{
    if (is_array($postCategories) || $postCategories instanceof Collection)
    {
        /** @var PostCategory $postCategory */
        foreach ($postCategories as $postCategory)
        {
            $this->addPostCategory($postCategory);
        }
    }
    else
    {
        $this->addPostCategory($postCategories);
    }

    return $this;
}

/**
 * Add postCategory
 *
 * @param \AppBundle\Entity\Social\PostCategory $postCategory
 *
 * @return Post
 */
public function addPostCategory(\AppBundle\Entity\Social\PostCategory $postCategory)
{
    $postCategory->addPost($this);
    $this->postCategories[] = $postCategory;

    return $this;
}

/**
 * Remove postCategory
 *
 * @param \AppBundle\Entity\Social\PostCategory $postCategory
 */
public function removePostCategory(\AppBundle\Entity\Social\PostCategory $postCategory)
{
    $this->postCategories->removeElement($postCategory);
}

/**
 * Get postCategories
 *
 * @return \Doctrine\Common\Collections\Collection
 */
public function getPostCategories()
{
    return $this->postCategories;
}
/**
 * Constructor
 * @param null $user
 */
public function __construct($user = null)
{
    $this->postCategories = new \Doctrine\Common\Collections\ArrayCollection();
}

Now, when editing a post, I also have an issue because it uses a getter which ouputs a collection, not a single entity, and my category field is not filled correctly.

/**
 * Get postCategories
 *
 * @return \Doctrine\Common\Collections\Collection
 */
public function getPostCategories()
{
    return $this->postCategories;
}

It's working if I set 'multiple' => true but I don't want this, I want the user to only select one category and I don't want to only constraint this with asserts.

Of course there are cases when I want to let the user select many fields so I want to keep the manyToMany relationship.

What can I do ?


Solution

  • If you want to set the multiple option to false when adding to a ManyToMany collection, you can use a "fake" property on the entity by creating a couple of new getters and setters, and updating your form-building code.

    (Interestingly, I saw this problem on my project only after upgrading to Symfony 2.7, which is what forced me to devise this solution.)

    Here's an example using your entities. The example assumes you want validation (as that's slightly complicated, so makes this answer hopefully more useful to others!)

    Add the following to your Post class:

    public function setSingleCategory(PostCategory $category = null)
    {
        // When binding invalid data, this may be null
        // But it'll be caught later by the constraint set up in the form builder
        // So that's okay!
        if (!$category) {
            return;
        }
    
        $this->postCategories->add($category);
    }
    
    // Which one should it use for pre-filling the form's default data?
    // That's defined by this getter.  I think you probably just want the first?
    public function getSingleCategory()
    {
        return $this->postCategories->first();
    }
    

    And now change this line in your form:

    ->add('postCategories', 'entity', array(
    

    to be

    ->add('singleCategory', 'entity', array(
        'constraints' => [
            new NotNull(),
        ],
    

    i.e. we've changed the field it references, and also added some inline validation - you can't set up validation via annotations as there is no property called singleCategory on your class, only some methods using that phrase.