⚠️ This content has been written a long time ago. As such, it might not reflect my current thoughts anymore. I keep this page online because it might still contain valid information.
Enforcing Data Encapsulation with Symfony Forms
Clermont-Fd Area, France2016-12-17 // @mathiasverraes and I exchanged a few tweets so I updated this post a bit to give the context.
Having classes (entities or whatever related to your model layer) that exposes attributes publicly, i.e. setters and getters for all attributes is just a non sense. Basically, a class with attributes/getters/setters is the same thing as a class with public properties or even as an array. Don’t do that!
You should rather write classes that own data, and keep them in a safe place. That is called encapsulation. Encapsulation is one of the four fundamentals in Object-Oriented Programming (OOP). It is used to hide the values or state of a structured object inside a class, preventing unauthorized parties direct access to them. That is why you should avoid getters and setters as much as you can, not to say all the time!
Working with the Symfony Form, it may be hard to decouple your code, and to not rely on getters and setters. Mathias Verraes describes a great approach in Decoupling (Symfony2) Forms from Entities, and I strongly agree with him. To sum up, the Form component should be used in your Presentation Layer, and it should not mess with your Model Layer. Use commands or DTOs with Forms, then create or act on your Model entities in the Application Layer (i.e. in your controllers).
However, the aim of this blog post is to show you how to keep decent model classes even with the Form component. In other words, don’t write poor model classes because of the framework you are using. You should design your model classes to fit your business, and according to OOP paradigms, not because framework X sucks at hydrating an object. I guess it is Hibernate’s fault at some point, at least in the Java world with their POJOs/JavaBeans, but I am digressing…
The solution to not rely on getters and setters is to control how objects are
created into the Form component. In order to do that, you can rely on the
empty_data
option.
Let’s take an example. Among other things, your model layer defines a Customer
entity that owns a name
and an Email
value object. You don’t want to break
encapsulation and write SOLID
code instead, so
you end up writing the following Customer
class definition:
<?php
namespace My\Model;
class Customer
{
private $name;
/**
* @var Email
*/
private $email;
public function __construct($name, Email $email)
{
$this->name = $name;
$this->email = $email;
}
}
The Email
value object can be defined as follows. Thank you
Mathias by the way.
<?php
namespace My\Model;
class Email
{
private $email;
public function __construct($email)
{
$this->email = $email;
}
}
In your CustomerType
, all you need to do is configure the empty_data
option in the setDefaultOptions()
method using a closure:
<?php
namespace My\Form\Type;
use My\Model\Customer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CustomerType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'string')
->add('email', new EmailType())
;
}
/**
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'My\Model\Customer',
'empty_data' => function (FormInterface $form) {
return new Customer(
$form->get('name')->getData(),
$form->get('email')->getData()
);
}
));
}
// ...
}
It is worth mentioning that it works fine with custom types, collections, and so
on. In the example above, it relies on a custom EmailType
rather than directly
using the built-in email
type and creating the value object in the
CustomerType
. The creation is left to this EmailType
. Calling getData()
on
$form->get('email')
actually returns an Email
object.
For the record, here is the EmailType
definition:
<?php
namespace My\Form\Type;
use My\Model\Email;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class EmailType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('email', 'email');
}
/**
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'My\Model\Email',
'empty_data' => function (FormInterface $form) {
return new Email(
$form->get('email')->getData()
);
}
));
}
// ...
}
No more excuse to write crappy model classes now!
Feel free to fork and edit this post if you find a typo, thank you so much! This post is licensed under the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.
Comments
No comments here. You can get in touch with me on Mastodon or send me an email if you prefer.