Last Updated: May 27, 2017
·
1.34K
· zdenekdrahos

Cleaner Symfony forms

Cleaner Symfony forms

Forms are essential component for development of Symfony web application. But if I look at typical Symfony form then I see several violations of Four Rules of Simple design.

Problems in symfony forms

  1. Duplicated form alias - getName and alias defined in services must match
  2. Unnecessary coupling to Symfony - do I really need to know about OptionsResolverInterface?
  3. Optional $options -
    1. static analyser like PMD or even your IDE can report unused parameter
    2. buildForm is less readable when code combines building form and reading options

Typical Symfony form

namespace AppBundle\Form\Type;

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

class TaskType extends AbstractType
{
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Task'
        ));
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, array('widget' => 'single_text'))
            ->add('save', 'submit');
        if (is_null($options['data']->getId())) {
            $builder->add('assignee');
        }
    }

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

Form registered as service:

services:
    acme_demo.form.type.task:
        class: AppBundle\Form\Type\TaskType
        tags:
            - { name: form.type, alias: task }

Cleaner solution

I follow simple design rules, rather than Symfony Best Practices, to keep form as readable as possible and as simple as possible. This approach has worked very well for me especially in apps with many forms. Don't inherit Symfony's AbstractType, but CleanFormType (excuse dummy name) which solves previous problems:

  1. Alias is defined in one place (in services.yml)
  2. Form is coupled only to FormBuilderInterface
  3. Options are truly optional
namespace AppBundle\Form\Type;

use Symfony\Component\Form\FormBuilderInterface;

class TaskType extends CleanFormType
{
    private $isNewTaskCreated;

    public function getDefaults()
    {
        return array(
            'data_class' => 'AppBundle\Entity\Task'
        );
    }

    protected function loadOptions(array $options)
    {
        $this->isNewTaskCreated = is_null($options['data']->getId());
    }

    protected function build(FormBuilderInterface $builder)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, array('widget' => 'single_text'))
            ->add('save', 'submit');
        if ($this->isNewTaskCreated) {
            $builder->add('assignee');
        }
    }
}
services:
    acme_demo.form.type.task:
        class: AppBundle\Form\Type\TaskType
        arguments:
            - "task"
        tags:
            - { name: form.type, alias: task }

Form definition is even simpler without defaults and options:

class TaskType extends CleanFormType
{
    protected function build(FormBuilderInterface $builder)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, array('widget' => 'single_text'))
            ->add('save', 'submit');
    }
}

Source code

The CleanFormType is available in the gist. You can use it and modify it as you please. Here is the class:

namespace AppBundle\Form\Type;

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

abstract class CleanFormType extends AbstractType
{
    private $formName;

    public function __construct($formName)
    {
        $this->formName = $formName;
    }

    public function getName()
    {
        return $this->formName;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(
            $this->getDefaults()
        );
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $this->loadOptions($options);
        $this->build($builder);
    }

    protected function getDefaults()
    {
        return array();
    }

    /** @SuppressWarnings("unused") */
    protected function loadOptions(array $options)
    {
    }

    abstract protected function build(FormBuilderInterface $builder);
}