Symfony 2 - Multiple form fields for one database row

I'm using Symfony 2.1 and Doctrine 2.

I'm dealing with 2 main entities : Place and Feature, with a ManyToMany relationship between them. There's many features in the database, and to group them by theme the Feature is also related to a FeatureCategory entity with a ManyToOne relationship.

Here's the code of the different entities :

The Place entity

namespace Mv\PlaceBundle\Entity;
…

/**
 * Mv\PlaceBundle\Entity\Place
 *
 * @ORM\Table(name="place")
 * @ORM\Entity(repositoryClass="Mv\PlaceBundle\Entity\Repository\PlaceRepository")
 * @ORM\HasLifecycleCallbacks
 */
class Place
{
  /**
   * @var integer $id
   *
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

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

  /**
   * @ORM\ManyToMany(targetEntity="\Mv\MainBundle\Entity\Feature")
   * @ORM\JoinTable(name="places_features",
   *    joinColumns={@ORM\JoinColumn(name="place_id", referencedColumnName="id")},
   *    inverseJoinColumns={@ORM\JoinColumn(name="feature_id", referencedColumnName="id")}
   * )
   */
  private $features;

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

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

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

  /**
   * Add features
   *
   * @param \Mv\MainBundle\Entity\Feature $features
   * @return Place
   */
  public function addFeature(\Mv\MainBundle\Entity\Feature $features)
  {
    $this->features[] = $features;
    echo 'Add "'.$features.'" - Total '.count($this->features).'<br />';
    return $this;
  }

  /**
   * Remove features
   *
   * @param \Mv\MainBundle\Entity\Feature $features
   */
  public function removeFeature(\Mv\MainBundle\Entity\Feature $features)
  {
    $this->features->removeElement($features);
  }

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

  public function __construct()
  {
    $this->features = new \Doctrine\Common\Collections\ArrayCollection();
  }

The Feature Entity :

namespace Mv\MainBundle\Entity;
…

/**
 * @ORM\Entity
 * @ORM\Table(name="feature")
 * @ORM\HasLifecycleCallbacks
 */
class Feature 
{
    use KrToolsTraits\PictureTrait;

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

    /**
     * @ORM\Column(name="label", type="string", length=255)
     * @Assert\NotBlank()
     */
    protected $label;

    /**
     * @ORM\ManyToOne(targetEntity="\Mv\MainBundle\Entity\FeatureCategory", inversedBy="features", cascade={"persist"})
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;

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

    /**
     * Set label
     *
     * @param string $label
     * @return Feature
     */
    public function setLabel($label)
    {
        $this->label = $label;
        return $this;
    }

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

    /**
     * Set category
     *
     * @param Mv\MainBundle\Entity\FeatureCategory $category
     * @return Feature
     */
    public function setCategory(\Mv\MainBundle\Entity\FeatureCategory $category = null)
    {
        $this->category = $category;
        return $this;
    }

    /**
     * Get category
     *
     * @return Mv\MainBundle\Entity\FeatureCategory 
     */
    public function getCategory()
    {
        return $this->category;
    }
}

The FeatureCategory entity :

namespace Mv\MainBundle\Entity;
...

/**
 * @ORM\Entity
 * @ORM\Table(name="feature_category")
 */
class FeatureCategory 
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(name="code", type="string", length=255)
     * @Assert\NotBlank()
     */
    protected $code;

    /**
     * @ORM\Column(name="label", type="string", length=255)
     * @Assert\NotBlank()
     */
    protected $label;

    /**
     * @ORM\OneToMany(targetEntity="\Mv\MainBundle\Entity\Feature", mappedBy="category", cascade={"persist", "remove"}, orphanRemoval=true)
     * @Assert\Valid()
     */
    private $features;

    public function __construct()
    {
       $this->features = new \Doctrine\Common\Collections\ArrayCollection();
    }

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

    /**
     * Set code
     *
     * @param string $code
     * @return Feature
     */
    public function setCode($code)
    {
        $this->code = $code;
        return $this;
    }

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

    /**
     * Set label
     *
     * @param string $label
     * @return Feature
     */
    public function setLabel($label)
    {
        $this->label = $label;
        return $this;
    }

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

    /**
    * Add features
    *
    * @param \Mv\MainBundle\Entity\Feature $features
    */
    public function addFeatures(\Mv\MainBundle\Entity\Feature $features){
      $features->setCategory($this);
      $this->features[] = $features;
    }

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

    /*
     * Set features
     */
    public function setFeatures(\Doctrine\Common\Collections\Collection $features)
    {
      foreach ($features as $feature)
      {
        $feature->setCategory($this);
      }
      $this->features = $features;
    }

    /**
     * Remove features
     *
     * @param Mv\MainBundle\Entity\Feature $features
     */
    public function removeFeature(\Mv\MainBundle\Entity\Feature $features)
    {
        $this->features->removeElement($features);
    }

    /**
     * Add features
     *
     * @param Mv\MainBundle\Entity\Feature $features
     * @return FeatureCategory
     */
    public function addFeature(\Mv\MainBundle\Entity\Feature $features)
    {
        $features->setCategory($this);
        $this->features[] = $features;
    }
}

Feature table is already populated, and users won't be able to add features but only to select them in a form collection to link them to the Place. (The Feature entity is for the moment only linked to Places but will be later related to others entities from my application, and will contain all the features available for all entities)

In the Place form I need to display checkboxes of the features available for a Place, but I need to display them grouped by category. Example :

Visits (FeatureCategory - code VIS) :

  • Free (Feature)
  • Paying (Feature)

Languages spoken (FeatureCategory - code LAN) :

  • English (Feature)
  • French (Feature)
  • Spanish (Feature)

My idea

Use virtual forms in my PlaceType form, like this :

$builder
    ->add('name')
    ->add('visit', new FeatureType('VIS'), array(
        'data_class' => 'Mv\PlaceBundle\Entity\Place'
    ))
    ->add('language', new FeatureType('LAN'), array(
        'data_class' => 'Mv\PlaceBundle\Entity\Place'
    ));

And create a FeatureType virtual form, like this :

    class FeatureType extends AbstractType
    {
        protected $codeCat;

        public function __construct($codeCat)
        {
          $this->codeCat = $codeCat;
        }

        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                ->add('features', 'entity', array(
                    'class' => 'MvMainBundle:Feature',
                    'query_builder' => function(EntityRepository $er)
                    {
                      return $er->createQueryBuilder('f')
                              ->leftJoin('f.category', 'c')
                              ->andWhere('c.code = :codeCat')
                              ->setParameter('codeCat', $this->codeCat)
                              ->orderBy('f.position', 'ASC');
                    },
                    'expanded' => true,
                    'multiple' => true
                ));
        }

        public function setDefaultOptions(OptionsResolverInterface $resolver)
        {
            $resolver->setDefaults(array(
                'virtual' => true
            ));
        }

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

With this solution I get what I want but the bind process doesn't persist all the features. Instead of grouping them, it only keeps me and persist the last group "language", and erases all the previouses features datas. To see it in action, if I check the 5 checkboxes, it gets well into the Place->addFeature() function 5 times, but the length of the features arrayCollection is successively : 1, 2, 1, 2, 3.

Any idea on how to do it another way ? If I need to change the model I'm still able to do it. What is the best way, reusable on my future other entities also related to Feature, to handle this ?

Thank you guys.

Answers


I think your original need is only about templating.

So you should not tweak the form and entity persistence logic to get the desired autogenerated form.

You should go back to a basic form

$builder
    ->add('name')
    ->add('features', 'entity', array(
        'class' => 'MvMainBundle:Feature',
        'query_builder' => function(EntityRepository $er) {
              return $er->createQueryBuilder('f')
              //order by category.xxx, f.position
            },
                'expanded' => true,
                'multiple' => true
            ));

And tweak your form.html.twig


Need Your Help

Hide a <div> with jQuery without using ID or class

jquery wordpress html

I want to hide the following &lt;div&gt; in my site pages, but this &lt;div&gt; is changing its position dynamically, so I cannot use the code $("div:eq(0)").hide();

Heap dumps and memory use discrepancies in android?

java android memory heap memory-leaks

Alright, I am not typically one to ask for help as I usually prefer to find answers on my own, but unfortunately I seem unable to do that.

About UNIX Resources Network

Original, collect and organize Developers related documents, information and materials, contains jQuery, Html, CSS, MySQL, .NET, ASP.NET, SQL, objective-c, iPhone, Ruby on Rails, C, SQL Server, Ruby, Arrays, Regex, ASP.NET MVC, WPF, XML, Ajax, DataBase, and so on.