Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to work correctly with Forms, FOS Rest Bundle and many to many relations in Symfony2

I'm working with Symfony2 Forms and FOSRestBundle.

I'm trying to save in the database, an entity with a many to many relationship.

I create a Form with a collection field (http://symfony.com/doc/master/cookbook/form/form_collections.html) like this:

class MainType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name');
        $builder->add('description');

        $builder->add('others', 'collection', array(
            'type' => new OtherType()
        ));
    }

    public function getName()
    {
        return '';
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\SearchBundle\Entity\Main',
            'csrf_protection' => false
        ));
    }
}

class OtherType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('id');
    }

    public function getName()
    {
        return '';
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\SearchBundle\Entity\Other',
            'csrf_protection' => false
        ));
    }
}

The collection of objects of type "Other" is stored in the database. And I don't want to store more objects of that type, only read and relate them to the main object.

When I process the form I use this function:

private function processForm(Main $main, $new = false)
{

    $new = true;

    $statusCode = $new ? 201 : 204;

    $form = $this->createForm(new MainType(), $main);
    $form->bind($this->getRequest());


    if ($form->isValid()) {

        $mainValidated = $form->getData();

        // I should store the collection of objects of type other
        // in the database

        $em = $this->getDoctrine()->getEntityManager();
        $em->persist($mainValidated);
        $em->flush();

        return $this->view($new ? $mainValidated : null, $statusCode);
    }

    return $this->view($form, 400);
}

The code json I send from a client Backbone.js is:

{"others":[{"id":1}, {"id":2}]}

Entities:

  • Main

Xml:

  <entity name="Acme\SearchBundle\Entity\Main" table="main">
    <id name="id type="integer" column="id">
      <generator strategy="IDENTITY"/>
    </id>
    <field name="name" type="integer" column="name" nullable="true"/>
    <field name="description" type="integer" column="description" nullable="true"/>

    <many-to-many field="others" target-entity="Other" inversed-by="mains">
      <cascade>
         <cascade-persist/>
      </cascade>
      <join-table name="main_has_other">
        <join-columns>
          <join-column name="main" referenced-column-name="id"/>
        </join-columns>
        <inverse-join-columns>
          <join-column name="other" referenced-column-name="id"/>
        </inverse-join-columns>
      </join-table>
    </many-to-many>

  </entity>

Entity:

<?php

namespace Acme\SearchBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

use JMS\Serializer\Annotation\Type;

use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\Expose;

class Main
{   
    /**
     * @Type("integer")
     * @Groups({"admin"})
     * 
     * @var integer
     * 
     */
    private $id;

    /**
     * 
     * @Type("string")
     * @Groups({"manage"})
     * 
     * @var string
     */
    private $name;


    /**
     * @Type("string")
     * @Groups({"manage"})
     * 
     * @var string
     */
    private $description;


    /**
     * @Type("ArrayCollection<Acme\SearchBundle\Entity\Other>")
     * @Groups({"manage"})
     * 
     * @var \Doctrine\Common\Collections\Collection
     */
    private $others;


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



    /**
     * Set name
     *
     * @param string $name
     * @return Main
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string 
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set description
     *
     * @param string $description
     * @return Main
     */
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Get description
     *
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Add others
     *
     * @param \Acme\SearchBundle\Entity\Other $other
     * @return Main
     */
    public function addOthers(\Acme\SearchBundle\Entity\Other $other)
    {
        $this->others[] = $other;

        return $this;
    }

    /**
     * Remove others
     *
     * @param \Acme\SearchBundle\Entity\Other $other
     */
    public function removeOthers(\Acme\SearchBundle\Entity\Other $other)
    {
        $this->others->removeElement($other);
    }

    /**
     * Get others
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getOthers()
    {
        return $this->others;
    }
}
  • Other

Xml:

<entity name="Acme\SearchBundle\Entity\Other" table="other">
  <id name="id" type="integer" column="id">
    <generator strategy="IDENTITY"/>
  </id>
  <field name="name" type="string" column="name" length="255" nullable="true"/>
  <field name="description" type="string" column="name" length="255" nullable="true"/>
  <many-to-many field="mains" target-entity="Main" mapped-by="others">
    <cascade>
        <cascade-persist/>
    </cascade>
  </many-to-many>
</entity>

Entity:

<?php

namespace Acme\SearchBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation\Groups;

class Other
{

   /**
     * @Type("integer")
     * @Groups({"manage"})
     * 
     * @var integer
     */
    private $id;

    /**
     * @Type("string")
     * @Groups({"manage"})
     * 
     * @var string
     */
    private $name;

    /**
     * @Type("string")
     * @Groups({"manage"})
     * 
     * @var string
     */
    private $description;

    /**
     * @Type("Acme\SearchBundle\Entity\Main")
     * @Groups({"admin"})
     * 
     * @var \Doctrine\Common\Collections\Collection
     */
    private $mains;

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

    /**
     * Set name
     *
     * @param string $name
     * @return Other
     */
    public function setName($name)
    {
        $this->name = $name
    }

    /**
     * Get name
     *
     * @return string 
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set description
     *
     * @param string $description
     * @return Other
     */
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Get description
     *
     * @return string 
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set id
     *
     * @param integer $id
     * @return Other
     */
    public function setId($id)
    {
        $this->id = $id;

        return $this;
    }

    /**
     * Add main
     *
     * @param \Acme\SearchBundle\Entity\Main $main
     * @return Other
     */
    public function addMains(\Acme\SearchBundle\Entity\Main $main)
    {
        $this->mains[] = $main;

        return $this;
    }

    /**
     * Remove main
     *
     * @param \Acme\SearchBundle\Entity\Main $main
     */
    public function removeMains(\AcmeSearchBundle\Entity\Main $main)
    {
        $this->mains->removeElement($main);
    }

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

When I persist the object of type "main" in the database, the collection is not persited in the table of many to many relationship. I have to save the collection manually when persist the "main" object.

I'm looking for a way to save the collection of objects automatically as easy as possible.

like image 780
escrichov Avatar asked Oct 05 '22 09:10

escrichov


1 Answers

I had a similiar issue, I think you just need to configure the form to expect extra items in your collection.

'allow_add' => true

This way "this form should not contain extra fields" error will not rise, as the form will be expecting these extra fields. So, the code should be

    $builder->add('others', 'collection', array(
        'type' => new OtherType(),
        'allow_add' => true
    ));
like image 139
jsampedro77 Avatar answered Oct 13 '22 11:10

jsampedro77