How to Create a Custom Form Field Type

How to Create a Custom Form Field Type

Symfony comes with tens of form types (called “form fields” in other projects) ready to use in your applications. However, it’s common to create custom form types to solve specific purposes in your projects.

Creating Form Types Based on Symfony Built-in Types

The easiest way to create a form type is to base it on one of the existing form types. Imagine that your project displays a list of “shipping options” as a <select> HTML element. This can be implemented with a ChoiceType where the choices option is set to the list of available shipping options.

However, if you use the same form type in several forms, repeating the list of choices every time you use it quickly becomes boring. In this example, a better solution is to create a custom form type based on ChoiceType. The custom type looks and behaves like a ChoiceType but the list of choices is already populated with the shipping options so you don’t need to define them.

Form types are PHP classes that implement Symfony\Component\Form\FormTypeInterface, but you should instead extend from Symfony\Component\Form\AbstractType, which already implements that interface and provides some utilities. By convention they are stored in the src/Form/Type/ directory:

  1. // src/Form/Type/ShippingType.php
  2. namespace App\Form\Type;
  3. use Symfony\Component\Form\AbstractType;
  4. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  5. use Symfony\Component\OptionsResolver\OptionsResolver;
  6. class ShippingType extends AbstractType
  7. {
  8. public function configureOptions(OptionsResolver $resolver): void
  9. {
  10. $resolver->setDefaults([
  11. 'choices' => [
  12. 'Standard Shipping' => 'standard',
  13. 'Expedited Shipping' => 'expedited',
  14. 'Priority Shipping' => 'priority',
  15. ],
  16. ]);
  17. }
  18. public function getParent(): string
  19. {
  20. return ChoiceType::class;
  21. }
  22. }

The methods of the FormTypeInterface are explained in detail later in this article. Here, getParent() method defines the base type (ChoiceType) and configureOptions() overrides some of its options. The resulting form type is a choice field with predefined choices.

Note

The PHP class extension mechanism and the Symfony form field extension mechanism are not the same. The parent type returned in getParent() is what Symfony uses to build and manage the field type. Making the PHP class extend from AbstractType is only a convenient way of implementing the required FormTypeInterface.

Now you can add this form type when creating Symfony forms:

  1. // src/Form/Type/OrderType.php
  2. namespace App\Form\Type;
  3. use App\Form\Type\ShippingType;
  4. use Symfony\Component\Form\AbstractType;
  5. use Symfony\Component\Form\FormBuilderInterface;
  6. class OrderType extends AbstractType
  7. {
  8. public function buildForm(FormBuilderInterface $builder, array $options): void
  9. {
  10. $builder
  11. // ...
  12. ->add('shipping', ShippingType::class)
  13. ;
  14. }
  15. // ...
  16. }

That’s all. The shipping form field will be rendered correctly in any template because it reuses the templating logic defined by its parent type ChoiceType. If you prefer, you can also define a template for your custom types, as explained later in this article.

Creating Form Types Created From Scratch

Some form types are so specific to your projects that they cannot be based on any existing form types because they are too different. Consider an application that wants to reuse in different forms the following set of fields as the “postal address”:

As explained above, form types are PHP classes that implement Symfony\Component\Form\FormTypeInterface, although it’s more convenient to extend instead from Symfony\Component\Form\AbstractType:

  1. // src/Form/Type/PostalAddressType.php
  2. namespace App\Form\Type;
  3. use Symfony\Component\Form\AbstractType;
  4. use Symfony\Component\Form\Extension\Core\Type\FormType;
  5. use Symfony\Component\OptionsResolver\OptionsResolver;
  6. class PostalAddressType extends AbstractType
  7. {
  8. // ...
  9. }

When a form type doesn’t extend from another specific type, there’s no need to implement the getParent() method (Symfony will make the type extend from the generic Symfony\Component\Form\Extension\Core\Type\FormType, which is the parent of all the other types).

These are the most important methods that a form type class can define:

buildForm()

It adds and configures other types into this type. It’s the same method used when creating Symfony form classes.

buildView()

It sets any extra variables you’ll need when rendering the field in a template.

configureOptions()

It defines the options configurable when using the form type, which are also the options that can be used in buildForm() and buildView() methods. Options are inherited from parent types and parent type extensions, but you can create any custom option you need.

finishView()

When creating a form type that consists of many fields, this method allows to modify the “view” of any of those fields. For any other use case, it’s recommended to use instead the buildView() method.

getParent()

If your custom type is based on another type (i.e. they share some functionality) add this method to return the fully-qualified class name of that original type. Do not use PHP inheritance for this. Symfony will call all the form type methods (buildForm(), buildView(), etc.) of the parent type and it will call all its type extensions before calling the ones defined in your custom type.

By default, the AbstractType class returns the generic Symfony\Component\Form\Extension\Core\Type\FormType type, which is the root parent for all form types in the Form component.

Defining the Form Type

Start by adding the buildForm() method to configure all the types included in the postal address. For the moment, all fields are of type TextType:

  1. // src/Form/Type/PostalAddressType.php
  2. namespace App\Form\Type;
  3. use Symfony\Component\Form\AbstractType;
  4. use Symfony\Component\Form\Extension\Core\Type\TextType;
  5. use Symfony\Component\Form\FormBuilderInterface;
  6. class PostalAddressType extends AbstractType
  7. {
  8. // ...
  9. public function buildForm(FormBuilderInterface $builder, array $options): void
  10. {
  11. $builder
  12. ->add('addressLine1', TextType::class, [
  13. 'help' => 'Street address, P.O. box, company name',
  14. ])
  15. ->add('addressLine2', TextType::class, [
  16. 'help' => 'Apartment, suite, unit, building, floor',
  17. ])
  18. ->add('city', TextType::class)
  19. ->add('state', TextType::class, [
  20. 'label' => 'State',
  21. ])
  22. ->add('zipCode', TextType::class, [
  23. 'label' => 'ZIP Code',
  24. ])
  25. ;
  26. }
  27. }

Tip

Run the following command to verify that the form type was successfully registered in the application:

  1. $ php bin/console debug:form

This form type is ready to use it inside other forms and all its fields will be correctly rendered in any template:

  1. // src/Form/Type/OrderType.php
  2. namespace App\Form\Type;
  3. use App\Form\Type\PostalAddressType;
  4. use Symfony\Component\Form\AbstractType;
  5. use Symfony\Component\Form\FormBuilderInterface;
  6. class OrderType extends AbstractType
  7. {
  8. public function buildForm(FormBuilderInterface $builder, array $options): void
  9. {
  10. $builder
  11. // ...
  12. ->add('address', PostalAddressType::class)
  13. ;
  14. }
  15. // ...
  16. }

However, the real power of custom form types is achieved with custom form options (to make them flexible) and with custom templates (to make them look better).

Adding Configuration Options for the Form Type

Imagine that your project requires to make the PostalAddressType configurable in two ways:

  • In addition to “address line 1” and “address line 2”, some addresses should be allowed to display an “address line 3” to store extended address information;
  • Instead of displaying a free text input, some addresses should be able to restrict the possible states to a given list.

This is solved with “form type options”, which allow to configure the behavior of the form types. The options are defined in the configureOptions() method and you can use all the OptionsResolver component features to define, validate and process their values:

  1. // src/Form/Type/PostalAddressType.php
  2. namespace App\Form\Type;
  3. use Symfony\Component\Form\AbstractType;
  4. use Symfony\Component\Form\Extension\Core\Type\TextType;
  5. use Symfony\Component\OptionsResolver\Options;
  6. use Symfony\Component\OptionsResolver\OptionsResolver;
  7. class PostalAddressType extends AbstractType
  8. {
  9. // ...
  10. public function configureOptions(OptionsResolver $resolver): void
  11. {
  12. // this defines the available options and their default values when
  13. // they are not configured explicitly when using the form type
  14. $resolver->setDefaults([
  15. 'allowed_states' => null,
  16. 'is_extended_address' => false,
  17. ]);
  18. // optionally you can also restrict the options type or types (to get
  19. // automatic type validation and useful error messages for end users)
  20. $resolver->setAllowedTypes('allowed_states', ['null', 'string', 'array']);
  21. $resolver->setAllowedTypes('is_extended_address', 'bool');
  22. // optionally you can transform the given values for the options to
  23. // simplify the further processing of those options
  24. $resolver->setNormalizer('allowed_states', static function (Options $options, $states) {
  25. if (null === $states) {
  26. return $states;
  27. }
  28. if (is_string($states)) {
  29. $states = (array) $states;
  30. }
  31. return array_combine(array_values($states), array_values($states));
  32. });
  33. }
  34. }

Now you can configure these options when using the form type:

  1. // src/Form/Type/OrderType.php
  2. namespace App\Form\Type;
  3. // ...
  4. class OrderType extends AbstractType
  5. {
  6. public function buildForm(FormBuilderInterface $builder, array $options): void
  7. {
  8. $builder
  9. // ...
  10. ->add('address', PostalAddressType::class, [
  11. 'is_extended_address' => true,
  12. 'allowed_states' => ['CA', 'FL', 'TX'],
  13. // in this example, this config would also be valid:
  14. // 'allowed_states' => 'CA',
  15. ])
  16. ;
  17. }
  18. // ...
  19. }

The last step is to use these options when building the form:

  1. // src/Form/Type/PostalAddressType.php
  2. namespace App\Form\Type;
  3. // ...
  4. class PostalAddressType extends AbstractType
  5. {
  6. // ...
  7. public function buildForm(FormBuilderInterface $builder, array $options): void
  8. {
  9. // ...
  10. if (true === $options['is_extended_address']) {
  11. $builder->add('addressLine3', TextType::class, [
  12. 'help' => 'Extended address info',
  13. ]);
  14. }
  15. if (null !== $options['allowed_states']) {
  16. $builder->add('state', ChoiceType::class, [
  17. 'choices' => $options['allowed_states'],
  18. ]);
  19. } else {
  20. $builder->add('state', TextType::class, [
  21. 'label' => 'State/Province/Region',
  22. ]);
  23. }
  24. }
  25. }

Creating the Form Type Template

By default, custom form types will be rendered using the form themes configured in the application. However, for some types you may prefer to create a custom template in order to customize how they look or their HTML structure.

First, create a new Twig template anywhere in the application to store the fragments used to render the types:

  1. {# templates/form/custom_types.html.twig #}
  2. {# ... here you will add the Twig code ... #}

Then, update the form_themes option to add this new template at the beginning of the list (the first one overrides the rest of files):

  • YAML

    1. # config/packages/twig.yaml
    2. twig:
    3. form_themes:
    4. - 'form/custom_types.html.twig'
    5. - '...'
  • XML

    1. <!-- config/packages/twig.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:twig="http://symfony.com/schema/dic/twig"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd
    8. http://symfony.com/schema/dic/twig
    9. https://symfony.com/schema/dic/twig/twig-1.0.xsd">
    10. <twig:config>
    11. <twig:form-theme>form/custom_types.html.twig</twig:form-theme>
    12. <twig:form-theme>...</twig:form-theme>
    13. </twig:config>
    14. </container>
  • PHP

    1. // config/packages/twig.php
    2. $container->loadFromExtension('twig', [
    3. 'form_themes' => [
    4. 'form/custom_types.html.twig',
    5. '...',
    6. ],
    7. ]);

The last step is to create the actual Twig template that will render the type. The template contents depend on which HTML, CSS and JavaScript frameworks and libraries are used in your application:

  1. {# templates/form/custom_types.html.twig #}
  2. {% block postal_address_row %}
  3. {% for child in form.children|filter(child => not child.rendered) %}
  4. <div class="form-group">
  5. {{ form_label(child) }}
  6. {{ form_widget(child) }}
  7. {{ form_help(child) }}
  8. {{ form_errors(child) }}
  9. </div>
  10. {% endfor %}
  11. {% endblock %}

Note

Symfony 4.2 deprecated calling FormRenderer::searchAndRenderBlock for fields that have already been rendered. That’s why the previous example includes the ... if not child.rendered statement.

The first part of the Twig block name (e.g. postal_address) comes from the class name (PostalAddressType -> postal_address). This can be controlled by overriding the getBlockPrefix() method in PostalAddressType. The second part of the Twig block name (e.g. _row) defines which form type part is being rendered (row, widget, help, errors, etc.)

The article about form themes explains the form fragment naming rules in detail. The following diagram shows some of the Twig block names defined in this example:

Caution

When the name of your form class matches any of the built-in field types, your form might not be rendered correctly. A form type named App\Form\PasswordType will have the same block name as the built-in PasswordType and won’t be rendered correctly. Override the getBlockPrefix() method to return a unique block prefix (e.g. app_password) to avoid collisions.

Passing Variables to the Form Type Template

Symfony passes a series of variables to the template used to render the form type. You can also pass your own variables, which can be based on the options defined by the form or be completely independent:

  1. // src/Form/Type/PostalAddressType.php
  2. namespace App\Form\Type;
  3. use Doctrine\ORM\EntityManagerInterface;
  4. // ...
  5. class PostalAddressType extends AbstractType
  6. {
  7. private $entityManager;
  8. public function __construct(EntityManagerInterface $entityManager)
  9. {
  10. $this->entityManager = $entityManager;
  11. }
  12. // ...
  13. public function buildView(FormView $view, FormInterface $form, array $options): void
  14. {
  15. // pass the form type option directly to the template
  16. $view->vars['isExtendedAddress'] = $options['is_extended_address'];
  17. // make a database query to find possible notifications related to postal addresses (e.g. to
  18. // display dynamic messages such as 'Delivery to XX and YY states will be added next week!')
  19. $view->vars['notification'] = $this->entityManager->find('...');
  20. }
  21. }

If you’re using the default services.yaml configuration, this example will already work! Otherwise, create a service for this form class and tag it with form.type.

The variables added in buildView() are available in the form type template as any other regular Twig variable:

  1. {# templates/form/custom_types.html.twig #}
  2. {% block postal_address_row %}
  3. {# ... #}
  4. {% if isExtendedAddress %}
  5. {# ... #}
  6. {% endif %}
  7. {% if notification is not empty %}
  8. <div class="alert alert-primary" role="alert">
  9. {{ notification }}
  10. </div>
  11. {% endif %}
  12. {% endblock %}

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.