How to Create a Custom Form Field Type
Warning: You are browsing the documentation for Symfony 2.x, which is no longer maintained.
Read the updated version of this page for Symfony 7.1 (the current stable version).
Symfony comes with a bunch of core field types available for building forms. However there are situations where you may want to create a custom form field type for a specific purpose. This recipe assumes you need a field definition that holds a shipping option, based on the existing choice field. This section explains how the field is defined, how you can customize its layout and finally, how you can register it for use in your application.
Defining the Field Type
In order to create the custom field type, first you have to create the class
representing the field. In this situation the class holding the field type
will be called ShippingType
and the file will be stored in the default location
for form fields, which is <BundleName>\Form\Type
. Make sure the field extends
AbstractType:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
// src/AppBundle/Form/Type/ShippingType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class ShippingType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choices' => array(
'Standard Shipping' => 'standard',
'Expedited Shipping' => 'expedited',
'Priority Shipping' => 'priority',
),
'choices_as_values' => true,
));
}
public function getParent()
{
return ChoiceType::class;
}
}
Tip
The location of this file is not important - the Form\Type
directory
is just a convention.
2.8
In 2.8, the getName()
method was removed. Now, fields are always referred
to by their fully-qualified class name.
Here, the return value of the getParent()
function indicates that you're
extending the ChoiceType
field. This means that, by default, you inherit
all of the logic and rendering of that field type. To see some of the logic,
check out the ChoiceType class. There are three methods that are particularly
important:
buildForm()
-
Each field type has a
buildForm()
method, which is where you configure and build any field(s). Notice that this is the same method you use to setup your forms, and it works the same here. buildView()
-
This method is used to set any extra variables you'll
need when rendering your field in a template. For example, in ChoiceType,
a
multiple
variable is set and used in the template to set (or not set) themultiple
attribute on theselect
field. See Creating a Template for the Field for more details. - .. versionadded:: 2.7
-
The
configureOptions()
method was introduced in Symfony 2.7. Previously, the method was calledsetDefaultOptions()
. configureOptions()
-
This defines options for your form type that
can be used in
buildForm()
andbuildView()
. There are a lot of options common to all fields (see FormType Field), but you can create any others that you need here.
Tip
If you're creating a field that consists of many fields, then be sure
to set your "parent" type as form
or something that extends form
.
Also, if you need to modify the "view" of any of your child types from
your parent type, use the finishView()
method.
The goal of this field was to extend the choice type to enable selection of the
shipping type. This is achieved by fixing the choices
to a list of available
shipping options.
Creating a Template for the Field
Each field type is rendered by a template fragment, which is determined in part by the class name of your type. For more information, see How to Customize Form Rendering.
Note
The first part of the prefix (e.g. shipping
) comes from the class name
(ShippingType
-> shipping
). This can be controlled by overriding getBlockPrefix()
in ShippingType
.
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
AppBundle\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.
In this case, since the parent field is ChoiceType
, you don't need to do
any work as the custom field type will automatically be rendered like a ChoiceType
.
But for the sake of this example, suppose that when your field is "expanded"
(i.e. radio buttons or checkboxes, instead of a select field), you want to
always render it in a ul
element. In your form theme template (see above
link for details), create a shipping_widget
block to handle this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
{# app/Resources/views/form/fields.html.twig #}
{% block shipping_widget %}
{% spaceless %}
{% if expanded %}
<ul {{ block('widget_container_attributes') }}>
{% for child in form %}
<li>
{{ form_widget(child) }}
{{ form_label(child) }}
</li>
{% endfor %}
</ul>
{% else %}
{# just let the choice widget render the select tag #}
{{ block('choice_widget') }}
{% endif %}
{% endspaceless %}
{% endblock %}
Tip
You can further customize the template used to render each children of the
choice type. The block to override in that case is named "block name" +
_entry
+ "element name" (label
, errors
or widget
) (e.g. to
customize the labels of the children of the Shipping widget you'd need to
define {% block shipping_entry_label %} ... {% endblock %}
).
Note
Make sure the correct widget prefix is used. In this example the name should
be shipping_widget
(see How to Customize Form Rendering).
Further, the main config file should point to the custom form template
so that it's used when rendering all forms.
When using Twig this is:
1 2 3 4
# app/config/config.yml
twig:
form_themes:
- 'form/fields.html.twig'
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:twig="http://symfony.com/schema/dic/twig"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/twig
http://symfony.com/schema/dic/twig/twig-1.0.xsd">
<twig:config>
<twig:form-theme>form/fields.html.twig</twig:form-theme>
</twig:config>
</container>
1 2 3 4 5 6
// app/config/config.php
$container->loadFromExtension('twig', array(
'form_themes' => array(
'form/fields.html.twig',
),
));
For the PHP templating engine, your configuration should look like this:
1 2 3 4 5 6
# app/config/config.yml
framework:
templating:
form:
resources:
- ':form:fields.html.php'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:templating>
<framework:form>
<framework:resource>:form:fields.html.php</twig:resource>
</framework:form>
</framework:templating>
</framework:config>
</container>
1 2 3 4 5 6 7 8 9 10
// app/config/config.php
$container->loadFromExtension('framework', array(
'templating' => array(
'form' => array(
'resources' => array(
':form:fields.html.php',
),
),
),
));
Using the Field Type
You can now use your custom field type immediately, simply by creating a new instance of the type in one of your forms:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/AppBundle/Form/Type/AuthorType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use AppBundle\Form\Type\ShippingType;
class OrderType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('shipping_code', ShippingType::class, array(
'placeholder' => 'Choose a delivery option',
));
}
}
But this only works because the ShippingType()
is very simple. What if
the shipping codes were stored in configuration or in a database? The next
section explains how more complex field types solve this problem.
Creating your Field Type as a Service
So far, this article has assumed that you have a very simple custom field type. But if you need access to configuration, a database connection, or some other service, then you'll want to register your custom type as a service. For example, suppose that you're storing the shipping parameters in configuration:
1 2 3 4 5 6
# app/config/config.yml
parameters:
shipping_options:
standard: Standard Shipping
expedited: Expedited Shipping
priority: Priority Shipping
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="shipping_options" type="collection">
<parameter key="standard">Standard Shipping</parameter>
<parameter key="expedited">Expedited Shipping</parameter>
<parameter key="priority">Priority Shipping</parameter>
</parameter>
</parameters>
</container>
1 2 3 4 5 6
// app/config/config.php
$container->setParameter('shipping_options', array(
'standard' => 'Standard Shipping',
'expedited' => 'Expedited Shipping',
'priority' => 'Priority Shipping',
));
To use the parameter, define your custom field type as a service, injecting the
shipping_options
parameter value as the first argument to its to-be-created
__construct()
function:
1 2 3 4 5 6 7 8
# src/AppBundle/Resources/config/services.yml
services:
app.form.type.shipping:
class: AppBundle\Form\Type\ShippingType
arguments:
- '%shipping_options%'
tags:
- { name: form.type }
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<!-- src/AppBundle/Resources/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="app.form.type.shipping" class="AppBundle\Form\Type\ShippingType">
<argument>%shipping_options%</argument>
<tag name="form.type" />
</service>
</services>
</container>
1 2 3 4 5 6 7
// src/AppBundle/Resources/config/services.php
use AppBundle\Form\Type\ShippingType;
$container->register('app.form.type.shipping', ShippingType::class)
->addArgument('%shipping_options%')
->addTag('form.type')
;
Tip
Make sure the services file is being imported. See How to Import Configuration Files/Resources for details.
First, add a __construct
method to ShippingType
, which receives the
shipping configuration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// src/AppBundle/Form/Type/ShippingType.php
namespace AppBundle\Form\Type;
use Symfony\Component\OptionsResolver\OptionsResolver;
// ...
// ...
class ShippingType extends AbstractType
{
private $shippingOptions;
public function __construct(array $shippingOptions)
{
$this->shippingOptions = $shippingOptions;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choices' => array_flip($this->shippingOptions),
'choices_as_values' => true,
));
}
// ...
}
Great! The ShippingType
is now fueled by the configuration parameters and
registered as a service. Because you used the form.type
tag in its configuration,
your service will be used instead of creating a new ShippingType
. In other words,
your controller does not need to change, it still looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/AppBundle/Form/Type/OrderType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use AppBundle\Form\Type\ShippingType;
class OrderType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('shipping_code', ShippingType::class, array(
'placeholder' => 'Choose a delivery option',
));
}
}
Have fun!