Crear un tipo de campo de formulario en Symfony

En Symfony existen tipos de campos de formulario o form field types incorporados, pero también puedes crear un campo personalizado

Contenido modificable

Si ves errores o quieres modificar/añadir contenidos, puedes crear un pull request. Gracias

Symfony viene con campos de formulario incorporados para crear formularios, pero hay veces que puede ser necesario tener que crear un custom form field type.

Para crear un field type customizado primero hay que crear la clase que representa el campo. En este caso vamos a crear un campo género, cuya clase será GenderType, y el archivo se guardará en el directorio por defecto para campos de formulario: \Form\Type.

// src/AppBundle/Form/Type/GenderType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

class GenderType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'choices' => array(
                'm' => 'Male',
                'f' => 'Female',
            )
        ));
    }

    public function getParent()
    {
        return ChoiceType::class;
    }
}

El valor de retorno del método getParent indica que estás extendiendo al campo ChoiceType. Esto significa que por defecto heredas toda la lógica y renderización de ese field type. La lógica de la clase se puede ver desde aquí. Hay tres métodos particularmente importantes:

  • buildForm(). Cada field type tiene un método buildForm, que es donde se configuran y construyen los campos. Es el mismo método que se emplea para configurar los formularios, y funciona de la misma forma.
  • buildView(). Este método se usa para establecer cualquier variable extra que necesites cuando renderices el campo en una template. Por ejemplo, en ChiceType, se establece una variable multiple y se emplea en la template para establecer (o no) el atributo multiple en el campo select.
  • configureOptions(). Define opciones para tu form type que pueden usarse en buildForm() y buildView(). Hay muchas opciones comunes a todos los campos, pero puedes crear otras que necesites en este método.

Si estás creando un campo que consiste en varios campos, asegúrate de establecer el parent type en form o cualquiera que extienda a form. También, si necesitas modificar el view de cualquiera de los child types desde el parent type, puedes emplear el método finishView().

El objetivo de este campo en concreto es extender a choice type para permitir la selección de género. Esto se consigue modificando las choices para listar los géneros.

Crear una template para el campo

Cada field type es renderizado por un fragmento de template, que es determinado en parte por el nombre de clase del type.

La primera parte del prefijo (en este caso gender) viene del nombre de clase (GenderType -> gender). Esto puede modificarse sobreescribiendo getBlockPrefix() en GenderType.

En este caso, ya que el parent field es ChoiceType, no necesitas hacer nada ya que el campo customizado será automáticamente renderizado como un ChoiceType. Pero en este caso cuando el campo se expanda (por ejemplo radio buttons o checkboxes, en lugar de un select field), queremos renderizarlo en un elemento ul. En la form theme template, crea un bloque gender_widget para modificarlo:

{# app/Resources/views/Form/fields.html.twig #}
{% block gender_widget %}
    {% spaceless %}
        {% if expanded %}
            <ul {{ block('widget_container_attributes') }}>
            {% for child in form %}
                <li>
                    {{ form_widget(child) }}
                    {{ form_label(child) }}
                </li>
            {% endfor %}
            </ul>
        {% else %}
            {# dejar que el widget choice renderice la etiqueta select #}
            {{ block('choice_widget') }}
        {% endif %}
    {% endspaceless %}
{% endblock %}

Hay que asegurarse de que se emplea el prefijo widget correcto. En este ejemplo el nombre debería ser gender_widget. Además el archivo de configuración principal debe apuntar al custom form type para que se emplee cuando se renderiza cualquier formulario.

Si se usa Twig se incluye lo siguiente:

# app/config/config.yml
twig:
    form_themes:
        - 'form/fields.html.twig'

Utilizar el field type

Ahora ya podemos emplear el custom field type, simplemente creando una nueva instancia del type en uno de nuestros formularios:

// src/AppBundle/Form/Type/AuthorType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use AppBundle\Form\Type\GenderType;

class AuthorType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('gender_code', GenderType::class, array(
            'placeholder' => 'Choose a gender',
        ));
    }
}

Este ejemplo es realmente muy sencillo, pero si queremos obtener los gender codes desde un archivo de configuración o de una base de datos, tenemos que emplear services.

Crear el field type como service

En el mismo ejemplo, vamos a a guardar los parámetros del género en la configuración:

# app/config/config.yml
parameters:
    genders:
        m: Male
        f: Female

Para emplear el parámetro, definimos el custom field type como un service, inyectando el valor del parámetro genders como primer argumento del método __construct:

# src/AppBundle/Resources/config/services.yml
services:
    app.form.type.gender:
        class: AppBundle\Form\Type\GenderType
        arguments:
            - '%genders%'
        tags:
            - { name: form.type }

Añadimos el método constructor a GenterType, que recibe la configuración gender:

// src/AppBundle/Form/Type/GenderType.php
namespace AppBundle\Form\Type;

use Symfony\Component\OptionsResolver\OptionsResolver;

// ...

// ...
class GenderType extends AbstractType
{
    private $genderChoices;

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

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'choices' => $this->genderChoices,
        ));
    }

    // ...
}

El GenderType recibe ahora los parámetros de configuración y se ha registrado como service. Ya que hemos usado el alias form.type en la configuración, se usará el service en lugar de crear un nuevo GenderType. En otras palabras, el controller no tiene por qué cambiar, sigue siendo así:

// src/AppBundle/Form/Type/AuthorType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use AppBundle\Form\Type\GenderType;

class AuthorType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('gender_code', GenderType::class, array(
            'placeholder' => 'Choose a gender',
        ));
    }
}