Symfony 2: Many-to-Many Relationships and Form Elements


One of the things I’m developing a love for with Symfony 2 is how Doctrine, forms and Twig work together in a fairly elegant manner. With a little bit of code and annotations, you can allow Symfony 2 to perform a lot of the heavy lifting and boiler plate code that normally is a pain-in-the-rear to handle. One such aspect in development is the many-to-many relationship mapping and subsequent introduction of that code into forms. Symfony 2 will remove all the painful setup but nailing down the right technique might make you do a little research online. What I would like to do is bring together my way of dealing with this problem into a single article.

The many-to-many relationship issue is a very common problem in development. Without going into database normalization theory, I want to say that various ORMs have their own methods for handling mappings and retrieval. Sometimes you end up dealing with complex retrievals and persistence. In Symfony 2’s case with Doctrine, most of the work can be handled using configuration. I mostly just use the annotation aspects to have my code located into one spot. So let’s get into an example.

For my example, I will be having two types of entities: races and classes (the example will be for a role playing type of game). In this case, many races can be a type of class and you can assign multiple classes to a race. For myself, part of the way this works in the system I’m building is defining ahead of time the allowable races for a class, acting as a type of validation. The main thing is that in a many-to-many relationship situation, you will have a link/join table.

In Symfony 2/Doctrine, you can employ the many-to-many bi-directional relationship to manage this case. Here’s the code for the Wclass class:

<?php

namespace Kwpro\VguildBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * 
 * @ORM\Entity(repositoryClass="Kwpro\VguildBundle\Entity\WclassRepository")
 * @ORM\Table(name="wclasses")
 */
class Wclass extends Base
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @Assert\NotBlank()
     * @Assert\Length(
     *     min = "2",
     *  max = "150"
     * )
     * @ORM\Column(type="string", length=150)
     * 
     */    
    protected $name;

    /**
     * @ORM\ManyToMany(targetEntity="Race", inversedBy="wclasses")
     * @ORM\JoinTable(name="wclass_races")
     */
    protected $races;

    public function __construct()
    {
        $this->races = new ArrayCollection();
    }
... // additional getter/setter code ignored
}

The most important thing to look at here is the annotations above the $races data member. $races will contain the related Race objects. We can see the relationship defined as a many-to-many type with the first annotation (important thing is to also include the @ORM\ part since you probably will run into an error when you run the console command to generate the entities). The inversedBy attribute tells you which data member will be used when traversing in the other direction of the relationship. Here, it will be $wclasses which will we see in a moment. The target entity is just the entity/model class that will be used (Race).

Lastly, you have JoinTable which is the table that will be used and/or autogenerated as the link/join intermediary table between these two relationships. Since I do not have a pre-existing database scheme that contains the join table, I use the diff and migrate commands to create the tables. What you’ll see in your database after that table is generated is a simple table named by the “name” attribute in the JoinTable annotation containing two columns, which should be the corresponding id’s from the two entities. If you let the system manage this aspect, it seems that the column names will be based on the class name + the primary key (for me it was wclass_id and race_id). I’d have to dig around a little more to see how you can configure this for pre-existing tables or when you have primary keys with other names (but I’m just assuming for now how this operates).

Now, that we’ve looked at the Wclass entity, let’s show the Race entity’s relevant parts:

<?php

namespace Kwpro\VguildBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * 
 * @ORM\Entity(repositoryClass="Kwpro\VguildBundle\Entity\RaceRepository")
 * @ORM\Table(name="races")
 */
class Race extends Base
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @Assert\NotBlank()
     * @Assert\Length(
     *     min = "2",
     *  max = "150"
     * )
     * @ORM\Column(type="string", length=150)
     * 
     */
    protected $name;

    /**
     * 
     * @Assert\NotBlank()
     * @Assert\Length(
     *     min = "1",
     *  max = "1"
     * )
     * @Assert\Choice(choices = {"a", "h"}, message = "Choose a valid faction." )
     * @ORM\Column(type="string", length=1)
     */
    protected $faction;

    /**
     * @ORM\ManyToMany(targetEntity="Wclass", mappedBy="races")
     */
    protected $wclasses;

    public function __construct()
    {
        $this->wclasses = new ArrayCollection();
    }
...// getter setters ignored.
}

Here, we’ll focus again on the relationship aspect. $wclasses will contain the collect of Wclass entities and is the attribute that we mentioned previously in the “inversedBy” section of the ManyToMany annotation.  Here, again we use the ManyToMany annotation but we use the mappedBy attribute that is linked to the “races” field in the Wclass entity. The JoinTable is not defined here neither.

After running the entities generator and diff/migrate commands, you’ll see the class decorated by various setters/getters for these fields as well as a way to add/remove entities from the collections. At this stage, I did not feel it was necessary to deal with the notion of the owner entity. So I will skip that part for now (or write about it in a future post).

So now, the great thing is that the vast bulk of the relationship mapping and database table structure setup is complete. That’s how easy the Doctrine part is and why it’s just a really awesome system! From a coding point of view, the only things you really needed to do was add a field ($races or $wclasses), some annotations describing the relationship, a constructor that instantiates these fields and import any related classes. That’s really not a lot of work for the amount of pain most other systems put you through! More than that, everything is very logically and elegantly described.

Having both mapped classes defined, we need to do something with them. For the sake of argument, let’s say that the data for races already exist. In the case of an RPG style game (in this case World of Warcraft), we’ll have data such as humans, orcs, gnomes, blood elves, etc. Next, we need to assign them to classes. The thing here is that you would need to have the race part setup before the class part, which is why I really did not want to deal with the notion of the owner entity. There might be future use cases where classes have nothing to do with a race.

For the next section, I want to be able to define which races belong to which class in the class creation form. The best way to go about doing this is through a form class. Here’s my WclassType.php object:

<?php

namespace Kwpro\VguildBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class WclassType extends AbstractType
{
    public function buildForm(FormBuilderInterface $b, array $options)
    {
            $b->add('name', 'text', array(
                'attr' => array('class' => 'form_input'),
                'label' => 'Class',
                'label_attr' => array('class' => 'form_label')    
            ))
            ->add('races', 'entity', array(
                'class' => 'Kwpro\VguildBundle\Entity\Race',
                'property' => 'name',
                'multiple' => true,
                'expanded' => true
              ))
            ->add('save', 'submit')
            ->add('saveAndAdd', 'submit');
    }

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

The juicy bit we want to look at is where I’m adding the Race class. The first thing to notice is that we’re using “races”, which is the attribute that gets pulled from the Wclass entity in displaying this section on the form. The type is an “entity” as we will be generating the list of races related to a class. The ‘class’ part I discovered needed to have the full namespace/path to avoid an error for finding the class. ‘property’ is the item that gets displayed from the entity class; in this case, I just need to show the race’s name (e.g. blood elf, dwarf, etc.). ‘multiple’ denotes that we can select more than one item in this list; in short, this is the way you’ll be able to save more than one item in a many-to-many relationship type of form. Lastly, we have ‘expanded’ set to true so that we have checkboxes generated as the form element (otherwise, it’ll create an ugly select dropdown box, but I think aesthetically it does not look good here).

If you use the default form, you’ll see just a name and a horizontal list of checkboxes for races show up. In my case, I customized my template a little more because I felt the horizontal checkboxes looked appalling and I wanted to go for a vertical appearance. Here’s the snippet from my twig template:

{% extends 'KwproVguildBundle:Admin:manage.html.twig' %}

{% block form %}
    {{ form_row(form.name) }}
    <div>
        <ul>
        {% for r in form.races %}
            <li>
                {{ form_widget(r) }}
                {{ form_label(r) }}
            </li>
        {% endfor %}    
        </ul>
    </div>
{% endblock %}

The key piece in this block of code is how I used the ul/li elements to create a vertical list. You can use CSS to further clean the look of this up but I found for my purpose, this method looked decent. form.races is generated from the form class we defined beforehand and accessed through the “races” attribute. One downside in using this vanilla form is that the list will be ordered by creation. So if you need a special sort, you might have to pass in additional options to make it look the way you want.

So how about persisting this data? Persisting related data from Symfony 2’s point of view is pretty mindless, which is another great reason to use Symfony 2. Rather than having to iterate through each element and saving them one by one as well as coming up with some messy form conventions to save your relations, Symfony 2 will handle everything behind the scenes (unless you need to do something specific). For myself in this example, I did not need anything special and Symfony 2 handled all the persistence for me. I’ll show my save method and explain it:

    public function saveAction(Request $request)
    {
        $this->_addHelper();
        $id = $request->get('id');
        if ($id){
            return $this->_update($id, $request);
        }
        $f = $this->createForm($this->_formService, $this->_model);
        $f->handleRequest($request);
        $success = false;
        $data = array('success' => 0);
        if ($f->isValid())
        {
            $manager = $this->getDoctrine()->getManager();
            $manager->persist($this->_model);
            $manager->flush();
            $data['success'] = 1;
            $data['html'] = $this->renderView($this->_editRowView, array('r' => $this->_model, 'editpath' => $this->_makePath(), 'editKey' => $this->_editLinkKey));
        }
        return $this->_json($data);
    }

There might be some odd notations and very oddly named variables (because I attempted to have a base class that would handle 99% of my CRUD operations) but even without anything being specifically named, you can still get the idea. If you have seen the example code posted on persisting forms/objects in Symfony 2, you’ll notice that most of this code is not that much different. For the most part, the createForm and handleRequest lines are where all the behind-the-scenes heavy lifting for extracting the data from the request and populating your model will occur. With regards to many-to-many relationships, that aspect is handled too. $this->_formService is just a way to point to my WclassType form I defined before, except using the mapping from the services.yml configuration file while $this->_model is just my Wclass entity object instance (I have the class instantiated in the _addHelper() method). If the form is valid, then I can go on to persist it with the $manager->persist($this->_model) line.

Not once in this code did I ever have to iterate through some oddly named form element, put it into an array and do other mumble jumbo. Everything is pretty much black boxed automagic, which is great. If everything works out, when you go to examine your tables, you’ll see that they’re now populated with the correct data. And when you re-load them for editing, the data is retrieved automagically once again.

What’s nice from this perspective is that if you have an entity and a form that did not have relationships beforehand but later added them, then you won’t have to change much on the persistence aspect. Most of your code will be done at the entity, form and template levels. You have to admit that it’s pretty slick.

(Visited 20,987 times, 1 visits today)

Comments

comments