Skip to content

Commit 4634ce3

Browse files
author
pvanliefland
committed
[#814] [Form] Added documentation for Form Type Extensions
1 parent 6e69f44 commit 4634ce3

File tree

3 files changed

+331
-0
lines changed

3 files changed

+331
-0
lines changed
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
.. index::
2+
single: Form; Form type extension
3+
4+
How to Create a Form Type Extension
5+
====================================
6+
7+
:doc:`Custom form field types<create_custom_field_type>` are great when
8+
you need field types with a specific purpose, such as a gender selector,
9+
or a VAT number input.
10+
11+
But sometimes, you don't really need to add new field types - you want
12+
to add features on top of existing types. This is where form type
13+
extensions come in.
14+
15+
Form type extensions have 2 main use cases:
16+
17+
#. You want to add a **generic feature to several types** (such as
18+
adding a "help" text to every field type)
19+
#. You want to add a **specific feature to a single type** (such
20+
as adding a "download" feature to the "file" field type)
21+
22+
In both those cases, it might be possible to achieve your goal with custom
23+
form rendering, or custom form field types. But using form type extensions
24+
can be cleaner (by limiting the amount of business logic in templates)
25+
and more flexible (you can add several type extensions to a single form
26+
type).
27+
28+
Form type extensions can achieve most of what custom field types can do,
29+
but instead of being field types of their own, **they plug into existing types**.
30+
31+
Imagine that you manage a ``Media`` entity, and that each media is associated
32+
to a file. Your ``Media`` form uses a file type, but when editing the entity,
33+
you would like to see its image automatically rendered next to the file
34+
input.
35+
36+
You could of course do this by customizing how this field is rendered in a template. But field
37+
type extensions allow you to do this in a nice DRY fashion.
38+
39+
Defining the Form Type Extension
40+
---------------------------------
41+
42+
Your first task will be to create the form type extension class. Let's
43+
call it ``ImageTypeExtension``. You will store the class in a file called
44+
``ImageTypeExtension.php``, in the ``<BundleName>\Form\Type`` directory.
45+
46+
When creating a form type extension, you can either implement the
47+
:class:`Symfony\\Component\\Form\\FormTypeExtensionInterface` interface,
48+
or extend the :class:`Symfony\\Component\\Form\\AbstractTypeExtension`
49+
class. Most of the time, you will end up extending the abstract class.
50+
That's what you will do in this tutorial::
51+
52+
// src/Acme/DemoBundle/Form/Type/ImageTypeExtension.php
53+
namespace Acme\DemoBundle\Form\Type;
54+
55+
use Symfony\Component\Form\AbstractTypeExtension;
56+
57+
class ImageTypeExtension extends AbstractTypeExtension
58+
{
59+
60+
/**
61+
* Returns the name of the type being extended.
62+
*
63+
* @return string The name of the type being extended
64+
*/
65+
public function getExtendedType()
66+
{
67+
return 'file';
68+
}
69+
70+
}
71+
72+
The only method you **must** implement is the ``getExtendedType`` function.
73+
It is used to indicate the name of the form type that will be extended
74+
by your extension.
75+
76+
.. tip::
77+
78+
The value you return in the ``getExtendedType`` method corresponds
79+
to the value returned by the ``getName`` method in the form type class
80+
you wish to extend.
81+
82+
In addition to the ``getExtendedType`` function, you will probably want
83+
to override one of the following methods:
84+
85+
* ``buildForm()``
86+
87+
* ``buildView()``
88+
89+
* ``getDefaultOptions()``
90+
91+
* ``getAllowedOptionValues()``
92+
93+
* ``buildViewBottomUp()``
94+
95+
For more information on what those methods do, you can refer to the
96+
:doc:`Creating Custom Field Types</cookbook/form/create_custom_field_type>`
97+
cookbook article.
98+
99+
Registering your Form Type Extension as a Service
100+
--------------------------------------------------
101+
102+
The next step is to make Symfony aware of your extension. All you
103+
need to do is to declare it as a service by using the ``form.type_extension``
104+
tag:
105+
106+
.. configuration-block::
107+
108+
.. code-block:: yaml
109+
110+
services:
111+
acme_demo_bundle.image_type_extension:
112+
class: Acme\DemoBundle\Form\Type\ImageTypeExtension
113+
tags:
114+
- { name: form.type_extension, alias: file }
115+
116+
.. code-block:: xml
117+
118+
<service id="acme_demo_bundle.image_type_extension" class="Acme\DemoBundle\Form\Type\ImageTypeExtension">
119+
<tag name="form.type_extension" alias="file" />
120+
</service>
121+
122+
.. code-block:: php
123+
124+
$container
125+
->register('acme_demo_bundle.image_type_extension', 'Acme\DemoBundle\Form\Type\ImageTypeExtension')
126+
->addTag('form.type_extension', array('alias' => 'file'));
127+
128+
The ``alias`` key of the tag is the type of field that this extension should
129+
be applied to. In your case, as you want to extend the ``file`` field type,
130+
you will use ``file`` as an alias.
131+
132+
Adding the extension business logic
133+
-----------------------------------
134+
135+
The goal of your extension is to display a nice image next to file inputs
136+
(when the underlying model contains images). For that purpose, let's assume
137+
that you use an approach similar to the one described in
138+
:doc:`How to handle File Uploads with Doctrine</cookbook/doctrine/file_uploads>`:
139+
you have a Media model with a file property (corresponding to the file field
140+
in the form) and a path property (corresponding to the image path in the
141+
database).
142+
143+
.. code-block:: php
144+
145+
// src/Acme/DemoBundle/Entity/Media.php
146+
namespace Acme\DemoBundle\Entity;
147+
148+
use Doctrine\ORM\Mapping as ORM;
149+
use Symfony\Component\Validator\Constraints as Assert;
150+
151+
/**
152+
* @ORM\Entity
153+
* @ORM\Table
154+
*/
155+
class Media
156+
{
157+
158+
// ...
159+
160+
/**
161+
* @var string
162+
*
163+
* @ORM\Column(name="path", type="string", length=255)
164+
*/
165+
private $path;
166+
167+
/**
168+
* @var \Symfony\Component\HttpFoundation\File\UploadedFile
169+
* @Assert\File(maxSize="2M")
170+
*/
171+
public $file;
172+
173+
// ...
174+
175+
/**
176+
* Get the image url
177+
*
178+
* @return null|string
179+
*/
180+
public function getWebPath()
181+
{
182+
// ... $webPath being the full image url, to be used in templates
183+
184+
return $webPath;
185+
}
186+
187+
Your form type extension class will need to do two things:
188+
189+
1) Override the ``getDefaultOptions`` method in order to add an image_path
190+
option
191+
2) Override the ``buildForm`` and ``buildView`` methods in order to pass the image
192+
url to the view
193+
194+
The logic is the following: when adding a form field of type ``file``,
195+
you will be able to specify a new option: ``image_path``. This option will
196+
tell the file field how to get the actual image url in order to display
197+
it in the view.
198+
199+
.. code-block:: php
200+
201+
// src/Acme/DemoBundle/Form/Type/ImageTypeExtension.php
202+
namespace Acme\DemoBundle\Form\Type;
203+
204+
use Symfony\Component\Form\AbstractTypeExtension;
205+
use Symfony\Component\Form\FormBuilder;
206+
use Symfony\Component\Form\FormView;
207+
use Symfony\Component\Form\FormInterface;
208+
use Symfony\Component\Form\Util\PropertyPath;
209+
210+
class ImageTypeExtension extends AbstractTypeExtension
211+
{
212+
213+
/**
214+
* Returns the name of the type being extended.
215+
*
216+
* @return string The name of the type being extended
217+
*/
218+
public function getExtendedType()
219+
{
220+
return 'file';
221+
}
222+
223+
/**
224+
* Add the image_path option
225+
*
226+
* @param array $options
227+
*/
228+
public function getDefaultOptions(array $options)
229+
{
230+
return array('image_path' => null);
231+
}
232+
233+
/**
234+
* Store the image_path option as a builder attribute
235+
*
236+
* @param \Symfony\Component\Form\FormBuilder $builder
237+
* @param array $options
238+
*/
239+
public function buildForm(FormBuilder $builder, array $options)
240+
{
241+
if (null !== $options['image_path']) {
242+
$builder->setAttribute('image_path', $options['image_path']);
243+
}
244+
}
245+
246+
/**
247+
* Pass the image url to the view
248+
*
249+
* @param \Symfony\Component\Form\FormView $view
250+
* @param \Symfony\Component\Form\FormInterface $form
251+
*/
252+
public function buildView(FormView $view, FormInterface $form)
253+
{
254+
if ($form->hasAttribute('image_path')) {
255+
$parentData = $form->getParent()->getData();
256+
257+
$propertyPath = new PropertyPath($form->getAttribute('image_path'));
258+
$imageUrl = $propertyPath->getValue($parentData);
259+
$view->set('image_url', $imageUrl);
260+
}
261+
}
262+
263+
}
264+
265+
Override the file widget template fragment
266+
------------------------------------------
267+
268+
Each field type is rendered by a template fragment. Those template fragments
269+
can be overridden in order to customize form rendering; for more information,
270+
you can refer to the :ref:`cookbook-form-customization-form-themes` article.
271+
272+
In your extension class, you have added a new variable (``image_url``), but
273+
you still need to take advantage of this new variable in your templates.
274+
You need to override the ``file_widget`` block:
275+
276+
.. code-block:: html+jinja
277+
278+
{# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
279+
{% extends 'form_div_layout.html.twig' %}
280+
281+
{% block file_widget %}
282+
{% spaceless %}
283+
284+
{{ block('field_widget') }}
285+
{% if image_url is not null %}
286+
<img src="{{ asset(image_url) }}"/>
287+
{% endif %}
288+
289+
{% endspaceless %}
290+
{% endblock %}
291+
292+
.. note::
293+
294+
You will need to change your config file or to explicitly specify how
295+
you want your form to be themed in order for Symfony to use your overridden
296+
block. See :ref:`cookbook-form-customization-form-themes` for more
297+
information.
298+
299+
Using the Form Type Extension
300+
------------------------------
301+
302+
From now on, when adding a field of type ``file`` in your form, you can
303+
specify an ``image_path`` option that will be used to display an image
304+
next to the file field. As an example::
305+
306+
// src/Acme/DemoBundle/Form/Type/MediaType.php
307+
namespace Acme\DemoBundle\Form;
308+
309+
use Symfony\Component\Form\AbstractType;
310+
use Symfony\Component\Form\FormBuilder;
311+
312+
class MediaType extends AbstractType
313+
{
314+
315+
public function buildForm(FormBuilder $builder, array $options)
316+
{
317+
$builder
318+
->add('name', 'text')
319+
->add('file', 'file', array('image_path' => 'webPath'));
320+
}
321+
322+
public function getName()
323+
{
324+
return 'media';
325+
}
326+
}
327+
328+
When displaying the form, if the underlying model has already been associated
329+
with an image, you will see it displayed next to the file input.

cookbook/form/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ Form
99
dynamic_form_generation
1010
form_collections
1111
create_custom_field_type
12+
create_form_type_extension
1213
use_virtuals_forms

cookbook/map.rst.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
* :doc:`/cookbook/form/dynamic_form_generation`
7272
* :doc:`/cookbook/form/form_collections`
7373
* :doc:`/cookbook/form/create_custom_field_type`
74+
* :doc:`/cookbook/form/create_form_type_extension`
7475
* :doc:`/cookbook/form/use_virtuals_forms`
7576
* (validation) :doc:`/cookbook/validation/custom_constraint`
7677
* (doctrine) :doc:`/cookbook/doctrine/file_uploads`

0 commit comments

Comments
 (0)