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

4

2

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 ?

Rubbish answered 20/2, 2015 at 9:46 Comment(0)
S
2

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.

Seventy answered 25/6, 2015 at 11:56 Comment(1)
Glad it's helpful. Thanks for accepting as answer!Seventy
U
0

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;
}
Unit answered 20/2, 2015 at 9:57 Comment(10)
unfortunately, it looks like using multiple => false on a many to many forces the use of the setPostCategories setter. I do have by_reference => false and this setter is still used. i'm posting my form details. Then, this does not solve the getter issue, which is the one I'm interested in.Furr
Yeah, as I see you are not using default choice field, but external. As I see it source, it requires additional service configuration, could you post it, please?Unit
aw, no need, I see your widget is configuring to entity, investigatingUnit
sorry, I could use the default entity widget, this would be just the same.Furr
try github.com/symfony/symfony-docs/issues/1057 (there is much docs on by_reference and accessors)Unit
"As of symfony/symfony#3239 the "collection" and "choice" types (with multiple=true) as well as subtypes (such as "entity") will try to discover an adder and a remover method in your model if "by_reference" is set to false." I need it to understand the getter is returning a single result. I'll try something with preset data event...Furr
Post the essentials of the class, please( adder, remover, getter and setter for this relation). also constructor, if used.Unit
udpated my post with new ideasUnit
well, again, my issue is not with setting, it is with getting. As I have a manyToMany, getPostCategories returns an arrayCollection, no matter what I want or which type hint I put. But I want to retrieve a single entity and not an array when setting multiple => false in the form.Furr
It's not possible to retrieve single entity because you are referring this relation to an array. If you need to work with single entity only - rewrite it to manyToOne association insteadUnit
W
0

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()
        )
    )
)
Waw answered 8/2, 2019 at 5:0 Comment(0)
B
0

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.

Berlioz answered 15/3, 2021 at 14:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.