5 Comments // Reading Time: 11 min.
Note: This article is part of a series. The first article discussed the creation of an initial Symfony 2 project.
There are two ways to make forms in symfony.
- The classic way - making a form in a template file
- The symfony way - by using symfony forms
Which one to use?
At first sight, I didn’t like Symfony forms much, because it is a lot of different from the classical way. All the form setup is done in a PHP class and it doesn’t respect MVC.
But Symfony forms can speed-up building processes especially for CRUD environments. Other benefit is support for bootstrap since Symfony 2.6 which gives you nice design in matter of seconds. ‘Symfony forms’ is a big feature of Symfony. They definitely invested a lot of time in it, so it has to be good. ;)
So I would use Symfony forms for CRUD environments and if there is need for a lot of customisation, I would make it on ‘classical’ way. But keep in mind that Symfony2 forms offer a lot of customization possibilities which might be sufficient for your needs.
Symfony form with many to many relationship
I will demonstrate the creation of a Symfony form with a N to N relationship on the User - Roles example, except in my example my User class is called Employee. Many Employees can have same role and many roles can be assigned to one employee, that’s why it’s a many to many relationship. In next few lines I will show you how to make a Symfony2 form which allows editing of the Employee entity and allows adding many roles to our employee.
1. Create the employee entity
This can be done via the simple console command that generates the entity through interactive mode. The console asks for the entity name and for the name and type of each field.
php app/console doctrine:generate:entity
In the end you should end up with a class like this:
<?php
// file: AppBundle/Entity/Employee.php
namespace AppBundle\Entity;
use AppBundle\Entity\Role;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Employee entity.
*
* @ORM\Table(name="employee")
* @ORM\Entity(repositoryClass="AppBundle\Entity\EmployeeRepository")
*/
class Employee extends Person {
/**
* @var ArrayCollection
*
* @ORM\ManyToMany(targetEntity="AppBundle\Entity\Role")
* @ORM\JoinTable(name="employees_roles",
* joinColumns={@ORM\JoinColumn(name="employee_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")}
* )
*/
protected $roles;
/**
* @var string
*
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $salary;
/**
* Constructor.
*/
public function __construct() {
parent::__construct();
$this->roles = new ArrayCollection();
}
/**
* Get roles as array of strings.
*
* @return array
*/
public function getRoles() {
$roleNames = [];
foreach ($this->roles as $role) {
$roleNames[] = $role->getName();
}
return $roleNames;
}
/**
* Get roles ass ArrayCollection.
*
* @return ArrayCollection
*/
public function getRolesCollection() {
return $this->roles;
}
/**
* Returns TRUE if employee has ROLE_ADMIN.
*
* @return bool
*/
public function isAdmin() {
return in_array('ROLE_ADMIN', $this->getRoles(), TRUE);
}
/**
* Set roles.
*
* @param ArrayCollection $roles
* @return Employee
*/
public function setRolesCollection($roles) {
$this->roles = $roles;
return $this;
}
/**
* Add roles
*
* @param Role $roles
* @return Employee
*/
public function addRolesCollection(Role $roles) {
$this->roles[] = $roles;
return $this;
}
/**
* Remove roles
*
* @param Role $roles
*/
public function removeRolesCollection(Role $roles) {
$this->roles->removeElement($roles);
}
/**
* Set salary
*
* @param string $salary
* @return Employee
*/
public function setSalary($salary) {
$this->salary = $salary;
return $this;
}
/**
* Get salary
*
* @return string
*/
public function getSalary() {
return $this->salary;
}
}
The employee class extends the person class which contains fields and their getters and setters like firstName, lastName, email, activeFrom, activeTo, ID. Those two entities are mapped by Class Table Inheritance which is not a subject of this post, so I won’t explain it now. For us, most important in this class is the $roles field which represents one side of the many to many relationship.
<?php
// file: AppBundle/Entity/Role.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Role
*
* @ORM\Table(name="role")
* @ORM\Entity(repositoryClass="AppBundle\Entity\RoleRepository")
* @UniqueEntity("name")
*/
class Role implements BaseEntityInterface {
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255, unique=true)
*/
private $name;
/**
* Get id
*
* @return integer
*/
public function getId() {
return $this->id;
}
/**
* Set name
*
* @param string $name
* @return Role
*/
public function setName($name) {
$this->name = $name;
return $this;
}
/**
* Get name
*
* @return string
*/
public function getName() {
return $this->name;
}
}
3. Implementation of the Symfony form
It is possible to define symfony form within a controller action, but it is bad practice to put too much stuff in controllers. Best way is to make a form type in a separate file. To have something to start, you can use this command:
php app/console doctrine:generate:form AppBundle:Employee --no-interaction
<?php
// file: AppBundle/Form/Type/EmployeeType.php
namespace AppBundle\Form\Type;
use AppBundle\Entity\Employee;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class EmployeeType extends AbstractType {
/**
* @var Employee
*/
protected $loggedInUser;
/**
* Constructor.
*
* @param Employee $loggedInUser
*/
public function __construct(Employee $loggedInUser) {
$this->loggedInUser = $loggedInUser;
}
/**
* Used to build the form.
*
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('firstName', 'text')
->add('lastName', 'text')
->add('email', 'text')
->add(
'rolesCollection', 'entity',
[
'class' => 'AppBundle\Entity\Role',
'property' => 'name',
'multiple' => TRUE,
'expanded' => TRUE,
'label' => 'Roles',
'disabled' => !$this->loggedInUser->isAdmin(),
]
)
->add(
'salary', 'money',
[
'required' => FALSE,
'currency' => 'EUR',
'disabled' => !$this->loggedInUser->isAdmin(),
]
)
->add(
'activeFrom', 'date',
[
'input' => 'datetime',
'widget' => 'choice',
'disabled' => !$this->loggedInUser->isAdmin(),
]
)
->add(
'activeTo', 'date',
[
'required' => FALSE,
'input' => 'datetime',
'widget' => 'choice',
'placeholder' => '',
'disabled' => !$this->loggedInUser->isAdmin(),
]
)
->add('save', 'submit', ['label' => 'Save employee']);
}
/**
* Returns the name of this type.
*
* @return string The name of this type
*/
public function getName() {
return 'app_employee';
}
/**
* Configures form's options.
*
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(
[
'data_class' => 'AppBundle\Entity\Employee',
]
);
}
}
As you can see, this class has 3 methods. The most important is the buildForm method, since that is the place where is each form element added to the form by calling $builder->add method. The first argument must be the string - field name, and it has to be exact name as it is in the entity. Also it needs to have a corresponding getter function. The second argument is a field type. The third and last argument (not required) is an array of settings for the form element. As you can see, I didn’t need any settings for the firstName field.
For example, $builder->add('firstName', 'text') will render the label ‘First name’ and right of it the input text field. If you wish to change the label, you can do it by adding 'label' => 'Custom label name' to the settings array. Let’s analyze our many-to-many relationship form element definition line by line:
'rolesCollection', 'entity', - you might have noticed that my entity Employee doesn’t have a rolesCollection field, but it has a corresponding getter (public function getRolesCollection()) which returns the field $roles as it is (ArrayCollection), and only that is important for us. I had to change the original getter (public function getRoles()) to return an array of role names for the authentication process, so I couldn’t use that getter here. And it is important to choose ‘entity’ as a second argument.
'class' => 'AppBundle\Entity\Role', - targeted entity class
'property' => 'name', - property of Role to display. This option is not necessary if targeted entity has __toString() method, which would be called when rendering each role on this form
'multiple' => TRUE, - says is it possible to select multiple fields.
'expanded' => TRUE, - if TRUE, all roles will be presented like checkboxes or radio buttons depending on ‘multiple’ attribute, if FALSE, roles will be in drop down list or in multi select list, depending on ‘multiple’ attribute.
'label' => 'Roles', - as explained, custom label
'disabled' => !$this->loggedInUser->isAdmin(), - if TRUE, input element will be disabled.
Lets see how the roles element will look depending on ‘multiple’ and ‘expanded’ fields:
'multiple' => TRUE,
'expanded' => TRUE,
'multiple' => TRUE,
'expanded' => FALSE,
'multiple' => FALSE,
'expanded' => TRUE,
'multiple' => FALSE,
'expanded' => FALSE,
In the getName() method we define the name of our form type. This name should be unique in the entire application. In the configureOptions() method I informed Symfony what is the full name of the entity class that stands behind this form. Symfony can find out the class name automatically, but it is good practice to define it here.
Just to mention that $loggedInUser field is not important for form functioning. I am using it just to determine if currently logged-in user is an admin, so I can renderthe form according that.
Form Validation
The backend validation of the form is done based on constraints defined in entities. For e.g. if we wish to make a required field, we just need to add this annotation in the entity above the field: @Assert\NotBlank().
/**
* @var string
*
* @ORM\Column(name="email", type="string", length=255)
* @Assert\NotBlank()
* @Assert\Email()
*/
private $email;
So this email field is required, it will be validated if it is email, and it must be unique, because it is not nullable. If there are validation errors, upon form submission, user will end up on form page with error message next to the invalid field. All that is done automatically by Symfony.
/**
* Opens edit page for employee with passed $id.
*
* @Route("/admin/edit_employee/{id}", name="edit_employee", defaults={"id" = -1})
* @Template
*
* @param $id
* @param Request $request
* @return array
*/
public function editEmployeeAction($id, Request $request) {
/** @var Employee $loggedInUser */
$loggedInUser = $this->getUser();
/** @var EmployeeRepository $employeeRepository */
$employeeRepository = $this->getDoctrine()->getRepository('AppBundle:Employee');
if ($id > -1) {
// Editing employee
/** @var Employee $employee */
$employee = $employeeRepository->find($id);
} else {
// Adding new employee
/** @var Employee $employee */
$employee = new Employee();
}
$form = $this->createForm(new EmployeeType($loggedInUser), $employee);
$form->handleRequest($request);
if ($form->isValid()) {
$employeeRepository->persistEntity($employee);
return $this->redirectToRoute('table_employee');
}
return [
'loggedInUser' => $loggedInUser,
'employee' => $employee,
'form' => $form->createView(),
];
}
I am using this same method for adding new employees and for editing existing ones. Lets talk about editing an employee only. This action has 3 outcomes:
1. User clicked on edit link in employee table
This action will be called with the employee id. The employee will be fetched from database so we can see in form fields the current values of the employee fields. Now comes the most important part - the form will be created from our type:
$form = $this->createForm(new EmployeeType($loggedInUser), $employee);
At next comes the handling of the request, but since we didn’t submit the form, $form->isValid() will return FALSE, and the edit employee template will be rendered with the form and initial field values.
2. User submited the form and it is valid
After handling the request ($form->handleRequest($request)), all values entered in the form fields will be set in the $employee object. Since the form is valid, we will enter the if block:
if ($form->isValid()) {
$employeeRepository->persistEntity($employee);
return $this->redirectToRoute('table_employee');
}
The employee object with new values will be persisted to the database and we will be redirected to the list page.
3. User submited the form and it is invalid
If the form is invalid, we won’t enter the if block and the edit page with the form will be rendered with error messages.
5. Realising the template
Since symfony forms did all the job for us, including front end/back end validation, we just need to add the form to twig file. This is the simplest way:
{# file: AppBundle/Resources/views/Employee/editEmployee.html.twig #}
{% block body %}
{{ form(form) }}
{% endblock %}
If it is needed to make some customisation and styling on the elements, each element can be separately inserted in the twig file.
6. Styling
We don’t want to spend too much time on styling, so lets use the bootstrap theme. We just need to enable it in config.yml file:
# Twig Configuration
twig:
form:
resources: ['bootstrap_3_horizontal_layout.html.twig']
And here is how our form looks like:
Awesome, isn’t it? :D
Contact us!
We are a digital agency, which is specialized in the development of digital products. Our core topics are websites and portals with TYPO3, eCommerce with Shopware and Android and iOS-Apps. In addition, we deal with many other topics in the field of web development. Feel free to contact us with your concerns!
Comments
Cedric Ziel
at 28.07.2015I'd be interested in the reason why you claim forms act contrary to your specific definition/interpretation of MVC as you claimed at the beginning. Could you go a little more in detail upon that? [...] I'd be interested in the reason why you claim forms act contrary to your specific definition/interpretation of MVC as you claimed at the beginning. Could you go a little more in detail upon that? Maybe even what you consider to be the classical way you mentioned in a sidenote.
Thx a lot,
Cedric
Damjan
at 28.07.2015Hi Cedric. Thanks for the comment.
When I say "classical way" I think on making a form in a template file, by using twig or HTML elements.
MVC architecture dictates clear separation of [...] Hi Cedric. Thanks for the comment.
When I say "classical way" I think on making a form in a template file, by using twig or HTML elements.
MVC architecture dictates clear separation of model, view and control layers in a web application, and for me, that separation is not so clear in the case of Symfony forms. Since we define form fields and its attributes in a Type class (e.g. EmployeeType), it is part of the view layer. And yet Symfony forms also do the model manipulation, which is job of the control layer.
-Damjan
Sam
at 13.06.2016Hi Mr. Damjan. Your Guide is very useful especially for us (newbies). I like your guide, it's simple and clean. I tried doing the same form, the same strategy, but i only included the part where i am [...] Hi Mr. Damjan. Your Guide is very useful especially for us (newbies). I like your guide, it's simple and clean. I tried doing the same form, the same strategy, but i only included the part where i am displaying a Dropdown where a user has to choose only one record and Checkboxes where a user could check as many as he/she wants. The field for the categories and checkboxes are taken from another entity. But i have a question about the project I am currently developing. The dropdowns and checkboxes shows up but when i try to submit it, it throws an error "Found entity of type Doctrine\Common\Collections\ArrayCollection on association SampleProj\CheckBundle\Entity\Dropdown#parameter, but expecting SampleProj\CheckBundle\Entity\Parameters.
P.S. I used ManyToOne Relationship for the two entities (category, parameter).
Boris
at 29.07.2017Old version of Symfony, but still helpful :-) Thank for your article. Old version of Symfony, but still helpful :-) Thank for your article.
Ketan Chavda
at 04.09.2017Its really good article for beginners as well as experienced.
Its really short and sweet explanation.
Thanks for this good article. Its really good article for beginners as well as experienced.
Its really short and sweet explanation.
Thanks for this good article.