Adding non-entity fields to your Symfony2 forms: Republished

This article was published here. However it refers to the interest of the problem that we are working on in other posts related to the form model. So here it is for reference.

I’ve had a case recently whereby I needed to implement a registration form in my current Symfony2 application, based on a Mandango model. This form consisted of selected fields from my model, along with a separate “I accept the terms and conditions” checkbox; pretty standard. In Symfony 1, it was straightforward to add in non-model fields, however in Symfony2 the convention for forms is to create a domain model which represents your form, which then gets populated with the data.

Initially I went with extending from the Mandango model (in my case, Model\MyOwnBundle\User being the original model) and adding in a separate get/setTermsAndConditions() method, but this caused problems when saving the model down, as Mandango couldn’t find the document class (since I’d extended it and called it UserRegistration).

After a bit of digging and experimentation however, the following code does what I need it to, without having to extend my existing model or create a new one (which I’d then have to use to populate my actual Mandango model):

class RegisterFormType extends AbstractType
{
    /**
     * Constructs our registration form
     * 
     * @param \Symfony\Component\Form\FormBuilder $builder
     * @param array $options
     * @return void
     */
    public function buildForm(FormBuilder $builder, array $options)
    {
        // Build the form
        $builder->
            add("firstName", "text")->
            add("lastName", "text")->
            add("emailAddress", "email")->
            add("t_and_c",
                "checkbox",
                array(
                    "property_path" => false,
                )
            );
 
        // "True" validator on the form for t&c
        $builder->
            addValidator(new CallbackValidator(function(FormInterface $form)
            {
                if (!$form["t_and_c"]->getData())
                {
                    $form->addError(new FormError('Please accept the terms and conditions in order to register'));
                }
            })
        );
    }
 
    // ...
}

The above adds the t_and_c field but using “property_path” => false means it’s not validated against the model and therefore no need to implement separate get/set() methods. Instead we use a callback validator on the form to check the value of the t_and_c field and ensure it’s been checked. Et voila 🙂

Symfony2 Famous Ozmerk’s Form Model Pattern

A problem arises when we try to create a form that does not conform to our entities.

The solution is said to be to create what we know as Form Model. The idea of this is basically that we don’t use the entity until we need to. This also demands things to be the way they are meant to be, that is that validation should occur on the entity or a wrapper for the entity but should not occur on the form. Therefore all form models created with this in mind end up being a wrapper for the entities.

So we start by defining a php class, the form model, with the validation logic embedded on the getters/setters of our new class. This results in enabling all of our validation rules to be relaxed at will within the form model. The goal is that by implementing a form model would let us partially persist information by relaxing the validation.
This is not all. The complete effort demands that when we are about to save (persist/flush) form information to entities, that we unwrap entities from the form model and persist it directly.

How does this looks in practice?

An example of this use can be found in FOSUB:
https://github.com/FriendsOfSymfony/FOSUserBundle/tree/master/Form/Model

So let’s take a look at this implementation of a model class:

namespace FOS\UserBundle\Form\Model;
 
class ChangePassword extends CheckPassword
{
    /**
     * @var string
     */
    public $new;
}

In this case CheckPassword is only:

namespace FOS\UserBundle\Form\Model;
 
use FOS\UserBundle\Model\UserInterface;
 
class CheckPassword
{
    /**
     * User whose password is changed
     *
     * @var UserInterface
     */
    public $user;
 
    /**
     * @var string
     */
    public $current;
 
    public function __construct(UserInterface $user)
    {
        $this->user = $user;
    }
}

So it is just basically adding a new public property to the main class which already has a constructor that accepts a user object. All the purpose of this class is to be able to change the password for an existing user object.

namespace FOS\UserBundle\Form\Handler;
 
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
 
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use FOS\UserBundle\Form\Model\ChangePassword;
 
class ChangePasswordFormHandler
{
    protected $request;
    protected $userManager;
    protected $form;
 
    public function __construct(Form $form, Request $request, UserManagerInterface $userManager)
    {
        $this->form = $form;
        $this->request = $request;
        $this->userManager = $userManager;
    }
 
    public function getNewPassword()
    {
        return $this->form->getData()->new;
    }
 
    public function process(UserInterface $user)
    {
        $this->form->setData(new ChangePassword($user));
 
        if ('POST' == $this->request->getMethod()) {
            $this->form->bindRequest($this->request);
 
            if ($this->form->isValid()) {
                $user->setPlainPassword($this->getNewPassword());
                $this->userManager->updateUser($user);
 
                return true;
            }
        }
 
        return false;
    }
}

So as we see here:

$this->form->setData(new ChangePassword($user));

ChangePassword is acting as a wrapper (form model) for setting values from form into entity $user. We know $user has so many other properties. And also we know that the form’s current purpose is just to change the password. So the form is just concerned with the password field out of the entire entity. This makes it a special case for form models and a special handler as we see.

The advantage of using a handler is that we can enclose form validation logic into it. Here it does it within the function process which will be probably called by a controller. The logic thereby has been shifted out of the controller and given to the handler. This is a good practice as we clearly see: Housing form validation logic outside the controller and shift it into the model/form layer of the application.

More over so you can know the power of the DI and services and how they are used to be exposed and do the job at the controller level here are the most significative 3 lines that accomplish the work at hand for changing the password for a given user:

$form = $this->container->get('fos_user.change_password.form');
$formHandler = $this->container->get('fos_user.change_password.form.handler');
$process = $formHandler->process($user);

First line gets the service form with the aim at that field for password. Second line gets the handler that maps this field to the user entity via the form-model for changing password. And finally the last thing to do is to call process method of the handler which does the persistence.

I have been working on two repos.
FormModelBundle
https://github.com/cordoval/FormModelProjectBundle
https://github.com/cordoval/FormModelBundle

The FormModelBundle is the bundle that house these experiments and the other is the project that embeds the first bundle. So you can git clone it and start working right away provided you run bin/vendors update to get the other bundle. Check the deps for the entry of the FormModelbundle.

With that we are now ready to explore the Partial Persistance strategies or Variable Persistance Strategies that can be developed on a form:

So there is going to be more than one of these $this->form->setData(new ChangePassword1($user)); and $this->form->setData(new ChangePassword2($user));.

For our example we are going to use a simple approach but with different entities:

 

This here is a snippet to develop in the future for multistage forms:

Wrapper 
__construct(entity)
 
validation_group1
validation_group2
 
Formtype class is Wrapper (wrapper does not have required)
 
$this->serialize()->save(Session[])
or
$this->serialize()->save_to_db
 
$wrapper->getEntity()

PhpStorm Stymfony2 Tip

We have received a tip from one of our readers about phpcs with phpstorm to setting it up as an External Tool under Settings. Here I paste the configuration so you can do it yourself and enjoy this nice feature of running right there quality control on any file.

thanks to Lucas Souza.

former post:
http://www.craftitonline.com/2011/06/set-phpstorm-to-remove-trailing-spaces-on-lines