Cómo crear un tipo de campo personalizado para formulario

Symfony viene con un montón de tipos de campos fundamentales para la construcción de formularios. Sin embargo, hay situaciones en las cuales quieres crear un tipo de campo de formulario personalizado para un propósito específico. Esta receta asume que necesitas una definición de campo que contiene el género de una persona, basándote en el campo choice existente. Esta sección explica cómo definir el campo, cómo puedes personalizar su diseño y, por último, cómo lo puedes registrar para usarlo en tu aplicación.

Definiendo el tipo de campo

Con el fin de crear el tipo de campo personalizado, primero tienes que crear la clase que representa el campo. En esta situación, la clase contendrá el tipo de campo que se llamará GenderType y el archivo se guardará en la ubicación predeterminada para campos de formulario, la cual es <NombrePaquete>\Form\Type. Asegúrate de que el campo se extiende de Symfony\Component\Form\AbstractType:

// src/Acme/DemoBundle/Form/Type/GenderType.php
namespace Acme\DemoBundle\Form\Type;

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

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

    public function getParent()
    {
        return 'choice';
    }

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

Truco

La ubicación de este archivo no es importante - el directorio Form\Type sólo es una convención.

En este caso, el valor de retorno de la función getParent indica que estas extendiendo el tipo de campo choice. Esto significa que, por omisión, heredas toda la lógica y represtación de ese tipo de campo. Para ver algo de la lógica, echa un vistazo a la clase ChoiceType. Hay tres métodos que son particularmente importantes:

  • buildForm() - Cada tipo de campo tiene un método buildForm, que es donde configuras y construyes cualquier campo(s). Ten en cuenta que este es el mismo método que utilizas para configurar tus formularios, y aquí funciona igual.
  • buildView() - Este método se utiliza para establecer las variables extra que necesitarás al reproducir el campo en una plantilla. Por ejemplo, en ChoiceType, está definida una variable multiple que se fija y utiliza en la plantilla para establecer (o no un conjunto), el atributo multiple en el campo select. Ve Creando una plantilla para el campo para más detalles.
  • setDefaultOptions() - Define opciones para tu tipo de formulario que puedes utilizar en buildForm() y buildView(). Hay un montón de opciones comunes a todos los campos (consulta Tipo de campo Form), pero aquí, puedes crear cualquier otra que necesites.

Truco

Si vas a crear un campo que consta de muchos campos, entonces, asegúrate de establecer tu tipo «padre» como form o algo que extienda a form. Además, si necesitas modificar la «vista» de cualquiera de tus tipos descendientes de tu tipo padre, usa el método finishView().

El método getName() devuelve un identificador que debe ser único en tu aplicación. Este se utiliza en varios lugares, tales como cuando personalizas cómo será pintado tu tipo de formulario.

El objetivo de este campo es extender el tipo choice para habilitar la selección de un género. Esto se consigue fijando las opciones a una lista de posibles géneros.

Creando una plantilla para el campo

Cada tipo de campo está representado por un fragmento de la plantilla, el cual se determina en parte por el valor de su método getName(). Para más información, consulta ¿Qué son los temas de formulario?.

En este caso, debido a que el campo padre es choice, no necesitas hacer ningún trabajo debido a que el tipo de campo personalizado automáticamente lo dibuja como el tipo choice. Pero por el bien de este ejemplo, supón que al «expandir» tu campo (es decir, botones de radio o casillas de verificación, en vez de un campo de selección), lo quieres dibujar siempre en un elemento ul. En la plantilla del tema de tu formulario (consulta el enlace de arriba para más detalles), crea un bloque gender_widget para manejar esto:

  • Twig
    {# src/Acme/DemoBundle/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 %}
                {# simplemente deja que el elemento gráfico 'choice' reproduzca la etiqueta select #}
                {{ block('choice_widget') }}
            {% endif %}
        {% endspaceless %}
    {% endblock %}
    
  • PHP
    <!-- src/Acme/DemoBundle/Resources/views/Form/gender_widget.html.twig -->
    <?php if ($expanded) : ?>
        <ul <?php $view['form']->block($form, 'widget_container_attributes') ?>>
        <?php foreach ($form as $child) : ?>
            <li>
                <?php echo $view['form']->widget($child) ?>
                <?php echo $view['form']->label($child) ?>
            </li>
        <?php endforeach ?>
        </ul>
    <?php else : ?>
        <!-- simplemente deja que el elemento gráfico 'choice' reproduzca la etiqueta select -->
        <?php echo $view['form']->renderBlock('choice_widget') ?>
    <?php endif ?>
    

Nota

Asegúrate que utilizas el prefijo correcto para el elemento gráfico. En este ejemplo, el nombre debe set gender_widget, de acuerdo con el valor devuelto por getName. Además, el archivo de configuración principal debe apuntar a la plantilla del formulario personalizado de modo que este se utilice al reproducir todos los formularios.

  • YAML
    # app/config/config.yml
    twig:
        form:
            resources:
                - 'AcmeDemoBundle:Form:fields.html.twig'
    
  • XML
    <!-- app/config/config.xml -->
    <twig:config>
        <twig:form>
            <twig:resource>AcmeDemoBundle:Form:fields.html.twig</twig:resource>
        </twig:form>
    </twig:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('twig', array(
        'form' => array(
            'resources' => array(
                'AcmeDemoBundle:Form:fields.html.twig',
            ),
        ),
    ));
    

Usando el tipo de campo

Ahora puedes utilizar el tipo de campo personalizado de inmediato, simplemente creando una nueva instancia del tipo en uno de tus formularios:

// src/Acme/DemoBundle/Form/Type/AuthorType.php
namespace Acme\DemoBundle\Form\Type;

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

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

Pero esto sólo funciona porque el GenderType() es muy sencillo. ¿Qué pasa si los códigos de género se almacena en la configuración o en una base de datos? La siguiente sección se explica cómo resuelven este problema los tipos de campo más complejos.

Creando tu tipo de campo como un servicio

Hasta ahora, este artículo ha supuesto que tienes un tipo de campo personalizado muy simple. Pero si necesitas acceder a la configuración de una conexión base de datos, o a algún otro servicio, entonces querrás registrar tu tipo personalizado como un servicio. Por ejemplo, supón que estas almacenando los parámetros de género en la configuración:

  • YAML
    # app/config/config.yml
    parameters:
        genders:
            m: Male
            f: Female
    
  • XML
    <!-- app/config/config.xml -->
    <parameters>
        <parameter key="genders" type="collection">
            <parameter key="m">Male</parameter>
            <parameter key="f">Female</parameter>
        </parameter>
    </parameters>
    
  • PHP
    // app/config/config.php
    $container->setParameter('genders.m', 'Male');
    $container->setParameter('genders.f', 'Female');
    

Para utilizar el parámetro, define tu tipo de campo personalizado como un servicio, inyectando el valor del parámetro genders como el primer argumento de la función __construct que vas a crear:

  • YAML
    # src/Acme/DemoBundle/Resources/config/services.yml
    services:
        acme_demo.form.type.gender:
            class: Acme\DemoBundle\Form\Type\GenderType
            arguments:
                - "%genders%"
            tags:
                    - { name:     form.type, alias: gender }
    
  • XML
    <!-- src/Acme/DemoBundle/Resources/config/services.xml -->
    <service id="acme_demo.form.type.gender" class="Acme\DemoBundle\Form\Type\GenderType">
        <argument>%genders%</argument>
        <tag name="form.type" alias="gender" />
    </service>
    
  • PHP
    // src/Acme/DemoBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container
        ->setDefinition('acme_demo.form.type.gender', new Definition(
            'Acme\DemoBundle\Form\Type\GenderType',
            array('%genders%')
        ))
        ->addTag('form.type', array(
            'alias' => 'gender',
        ))
    ;
    

Truco

Asegúrate de que estás importando el archivo de servicios. Consulta Importando configuración con imports para más detalles.

Asegúrate de que la etiqueta del atributo alias corresponde con el valor devuelto por el método getName definido anteriormente. Verás la importancia de esto en un momento cuando utilices el tipo de campo personalizado. Pero primero, agrega un método __construct, el cual recibe la configuración del género:

// src/Acme/DemoBundle/Form/Type/GenderType.php
namespace Acme\DemoBundle\Form\Type;

use Symfony\Component\OptionsResolver\OptionsResolverInterface;

// ...

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

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

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

    // ...
}

¡Genial! El GenderType ahora es impulsado por los parámetros de configuración y está registrado como un servicio. Adicionalmente, debido a que utilizaste el alias form.type en tu configuración, es mucho más fácil utilizar el campo:

// src/Acme/DemoBundle/Form/Type/AuthorType.php
namespace Acme\DemoBundle\Form\Type;

use Symfony\Component\Form\FormBuilderInterface;

// ...

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

Ten en cuenta que en vez de crear una nueva instancia, puedes simplemente referirte a ella por el alias utilizado en tu configuración del servicio, gender. ¡Que te diviertas!

Bifúrcame en GitHub