escape image

..RUN!

The Form Component  in Symfony is arguably the most complicated part of the framework (alongside with the Security Component).
This means there are plenty of concepts to learn, traps to be careful of, documentation to read/keep up-to-date and specially tough rules to stick with.

These rules sometimes go to the point of paralyzing you and throwing you into sleepless nights struggling to finish a very basic task you would have accomplished in minutes under a decoupled REST-API<->JavaScript implementation.

Surfing the internet, looking for hints on how Symfony veterans managed to escape  this jail, I came to an old yet valuable post of Mathias Verraes titled: Decoupling (Symfony2) Forms from Entities.

The idea behind this solution is to adopt the Command Pattern in order to decouple the targeted entity from the Symfony Form Component. This will give you much more freedom on how to read data by using “simpler” entities in the front-side (which will play the command role here) then use this simple entity to finally create your real Big Monster Entity that might have a forest of properties and Validation Constraints

How does Symfony Forms really act?

The form component could be used in various ways. However, one of its most common use cases is to couple it to one or many Entities. Entities are the building blocks of your backend, your Model, your M in the MVC pattern.

As you can see in the picturebelow, the role of the form is to provide the end-user with an interface allowing him to “build” an instance of an Entity, which we could later persist to the database.

symfony-form

So if you have an Entity coupled to a Form with  Validation Constraints on it,  you are actually automating a hell of  repetitive tasks and getting them ordered in a clean process. However: “Less/More Responsibilities means Less/More Rights”.  If you go for the “easier” solution, you will loose some (or many) authorities.

So the workaround shall deal with the Entity itself and not the Form.

The soccer clubs application.

Enough of theories? OK. Let’s get our hands dirty with some coding.

 

footbal-teams

We will apply this trick to a use case. We will consider a simple application called “Top Soccer Teams”

The application allows users to vote for Football Teams so that we can publish later the TOP 10 of the week.

We will start by creating a fresh Symfony application:

# php symfony new top-soccer-teams 3.3

We wait until the installation complete then enter the project folder.

# cd ./top-soccer-teams

We will start by generating our two entities: Country and Team.

# bin/console doctrine:generate:entity AppBundle:Country

Nothing more than a simple text property (string:255:Unique=true) to build the Country entity. Note that we don’t have to set the id property as it is managed by the generator.

For the Team Entity, it goes exactly the same way. Some might think of setting a country property or a numberOfVotes or even a rank property. All of these aren’t a pure properties of the Team entity.

So just Team the same way we created Country.

(a simple text property (string:255:Unique=true))

# bin/console doctrine:generate:entity AppBundle:Team

We should then obtain two classes with the following structures:

<?php
// src/Entity/Country.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Country
 *
 * @ORM\Table(name="country")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\CountryRepository")
 */
class Country
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="text", type="string", length=255, unique=true)
     */
    private $text;
    /**
     * @ORM\OneToMany(targetEntity="Team",mappedBy="country",cascade={"persist"})
     */
    private $teams;


    /**
     * Get id
     *
     * @return int
     */
    public function __construct()
    {
        $this->teams = new ArrayCollection();
    }
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set text
     *
     * @param string $text
     *
     * @return Country
     */
    public function setText($text)
    {
        $this->text = $text;

        return $this;
    }

    /**
     * Get text
     *
     * @return string
     */
    public function getText()
    {
        return $this->text;
    }
}
<?php
// src/Entity/Team.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Team
 *
 * @ORM\Table(name="team")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\TeamRepository")
 */
class Team
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

    /**
     * @ORM\ManyToOne(targetEntity="Country",inversedBy="teams",cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $country;


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

    /**
     * Set text
     *
     * @param string $text
     *
     * @return Team
     */
    public function setText($text)
    {
        $this->text = $text;

        return $this;
    }

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

 

Notice that we added the association annotations to tell the ORM (Doctrine2) that Country and Team entities are related to each others such each Team has a unique Country, and each Country has one to many Teams.

/**
 * @ORM\ManyToOne(targetEntity="Country",inversedBy="teams",cascade={"persist"})
 * @ORM\JoinColumn(nullable=false)
 */
private $country;

We won’t go further in this subject as it is not our main focus.

Now we have to generate adders & removers  . Fortunately, the Symfony CLI provides an automated way to accomplish this task too:

P.S: Associations must be set before generating adders & removers.

# bin/console doctrine:generate:entities AppBundle --no-backup
Generating entities for bundle "AppBundle"
  > generating AppBundle\Entity\Country
  > generating AppBundle\Entity\Team

Our Entities should now look like this:

<?php
// src/Entity/Country.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Country
 *
 * @ORM\Table(name="country")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\CountryRepository")
 */
class Country
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="text", type="string", length=255, unique=true)
     */
    private $text;
    /**
     * @ORM\OneToMany(targetEntity="Team",mappedBy="country",cascade={"persist"})
     */
    private $teams;


    /**
     * Get id
     *
     * @return int
     */
    public function __construct()
    {
        $this->teams = new ArrayCollection();
    }
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set text
     *
     * @param string $text
     *
     * @return Country
     */
    public function setText($text)
    {
        $this->text = $text;

        return $this;
    }

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

    /**
     * Add team
     *
     * @param \AppBundle\Entity\Team $team
     *
     * @return Country
     */
    public function addTeam(\AppBundle\Entity\Team $team)
    {
        $this->teams[] = $team;

        return $this;
    }

    /**
     * Remove team
     *
     * @param \AppBundle\Entity\Team $team
     */
    public function removeTeam(\AppBundle\Entity\Team $team)
    {
        $this->teams->removeElement($team);
    }

    /**
     * Get teams
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getTeams()
    {
        return $this->teams;
    }
}
<?php
// src/Entity/Team.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Team
 *
 * @ORM\Table(name="team")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\TeamRepository")
 */
class Team
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

    /**
     * @ORM\ManyToOne(targetEntity="Country",inversedBy="teams",cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $country;


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

    /**
     * Set text
     *
     * @param string $text
     *
     * @return Team
     */
    public function setText($text)
    {
        $this->text = $text;

        return $this;
    }

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

    /**
     * Set country
     *
     * @param \AppBundle\Entity\Country $country
     *
     * @return Team
     */
    public function setCountry(\AppBundle\Entity\Country $country)
    {
        $this->country = $country;

        return $this;
    }

    /**
     * Get country
     *
     * @return \AppBundle\Entity\Country
     */
    public function getCountry()
    {
        return $this->country;
    }
}

Now we are ready to create the database and turn these PHP classes into some storage yard (ORM’s job!)

First, don’t forget to choose a name for your database, through your app/config/parameters.yml file.

parameters:
    database_name: top_soccer_team

Now we are ready to create the database:

bin/console doctrine:database:create

and write the schema

# bin/console doctrine:schema:update --force
Updating database schema...
Database schema updated successfully! "3" queries were executed

In the next article (Part 2) we will see how to

1- Associate a User entity to a Team entity  in order to model a “vote”

2- Create a simple View

3- Write a Form to handle User votes.

Stay connected!