2
votes

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 ?

4

4 Answers

2
votes

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.

0
votes

You can setup you form type to not to use PostCategory by reference (set by_reference option to false)

This will force symfony forms to use addPostCategory and removePostCategory instead of setPostCategories.

UPD

1) You are mixing working with plain array and ArrayCollection. Choose one strategy. Getter will always output an ArrayCollection, because it should do so. If you want to force it to be plain array add ->toArray() method to getter

2) Also I understand that choice with multiple=false return an entity, while multiple=true return array independend of mapped relation (*toMany, or *toOne). So just try to remove setter from class and use only adder and remover if you want similar behavior on different cases.

/** @var ArrayCollection|PostCategory[] */
private $postCategories;

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

public function addPostCategory(PostCategory $postCategory)
{
   if (!$this->postCategories->contains($postCategory) {
      $postCategory->addPost($this);
      $this->postCategories->add($postCategory);
   }
}

public function removePostCategory(PostCategory $postCategory)
{
   if ($this->postCategories->contains($postCategory) {
      $postCategory->removePost($this);
      $this->postCategories->add($postCategory);
   }
}

/**
 * @return ArrayCollection|PostCategory[]
 */
public function getPostCategories()
{
    return $this->postCategories;
}
0
votes

In my case, the reason was that Doctrine does not have relation One-To-Many, Unidirectional with Join Table. In Documentations example is show haw we can do this caind of relation by ManyToMany (adding flag unique=true on second column).

This way is ok but Form component mixes himself.

Solution is to change geters and seters in entity class... even those generated automatically.

Here is my case (I hope someone will need it). Assumption: classic One-To-Many relation, Unidirectional with Join Table

Entity class:

/**
 * @ORM\ManyToMany(targetEntity="B2B\AdminBundle\Entity\DictionaryValues")
 * @ORM\JoinTable(
 *      name="users_responsibility",
 *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")},
 *      inverseJoinColumns={@ORM\JoinColumn(name="responsibility_id", referencedColumnName="id", unique=true, onDelete="CASCADE")}
 * )
 */
private $responsibility;

/**
 * Constructor
 */
public function __construct()
{
    $this->responsibility = new \Doctrine\Common\Collections\ArrayCollection();
}

/**
 * Add responsibility
 *
 * @param \B2B\AdminBundle\Entity\DictionaryValues $responsibility
 *
 * @return User
 */
public function setResponsibility(\B2B\AdminBundle\Entity\DictionaryValues $responsibility = null)
{

    if(count($this->responsibility) > 0){
        foreach($this->responsibility as $item){
            $this->removeResponsibility($item);
        }
    }

    $this->responsibility[] = $responsibility;

    return $this;
}

/**
 * Remove responsibility
 *
 * @param \B2B\AdminBundle\Entity\DictionaryValues $responsibility
 */
public function removeResponsibility(\B2B\AdminBundle\Entity\DictionaryValues $responsibility)
{
    $this->responsibility->removeElement($responsibility);
}

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

Form:

->add('responsibility', EntityType::class, 
    array(
        'required' => false,
        'label'    => 'Obszar odpowiedzialności:',
        'class'    => DictionaryValues::class,
        'query_builder' => function (EntityRepository $er) {
            return $er->createQueryBuilder('n')
                ->where('n.parent = 2')
                ->orderBy('n.id', 'ASC');
        },
        'choice_label' => 'value',
        'placeholder'  => 'Wybierz',
        'multiple' => false,
        'constraints' => array(
            new NotBlank()
        )
    )
)
0
votes

I know its a pretty old question, but the problem is still valid today. Using a simple inline data transformer did the trick for me.

public function buildForm(FormBuilderInterface $builder, array $options): void
{
  $builder->add('profileTypes', EntityType::class, [
      'multiple' => false,
      'expanded' => true,
      'class' => ProfileType::class,
  ]);

  // data transformer so profileTypes does work with multiple => false
  $builder->get('profileTypes')
      ->addModelTransformer(new CallbackTransformer(
          // return first item from collection
          fn ($data) => $data instanceof Collection && $data->count() ? $data->first() : $data,
          // convert single ProfileType into collection
          fn ($data) => $data && $data instanceof ProfileType ? new ArrayCollection([$data]) : $data
      ));
}

PS: Array functions are available in PHP 7.4 and above.