Skip to content

Commit 834b1ac

Browse files
committed
Merge branch 'issue_814_20' of github.com:pvanliefland/symfony-docs into pvanliefland-issue_814_20
2 parents a025372 + 4a238cb commit 834b1ac

File tree

3 files changed

+337
-0
lines changed

3 files changed

+337
-0
lines changed
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
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+
* Returns the name of the type being extended.
61+
*
62+
* @return string The name of the type being extended
63+
*/
64+
public function getExtendedType()
65+
{
66+
return 'file';
67+
}
68+
69+
}
70+
71+
The only method you **must** implement is the ``getExtendedType`` function.
72+
It is used to indicate the name of the form type that will be extended
73+
by your extension.
74+
75+
.. tip::
76+
77+
The value you return in the ``getExtendedType`` method corresponds
78+
to the value returned by the ``getName`` method in the form type class
79+
you wish to extend.
80+
81+
In addition to the ``getExtendedType`` function, you will probably want
82+
to override one of the following methods:
83+
84+
* ``buildForm()``
85+
86+
* ``buildView()``
87+
88+
* ``getDefaultOptions()``
89+
90+
* ``getAllowedOptionValues()``
91+
92+
* ``buildViewBottomUp()``
93+
94+
For more information on what those methods do, you can refer to the
95+
:doc:`Creating Custom Field Types</cookbook/form/create_custom_field_type>`
96+
cookbook article.
97+
98+
Registering your Form Type Extension as a Service
99+
--------------------------------------------------
100+
101+
The next step is to make Symfony aware of your extension. All you
102+
need to do is to declare it as a service by using the ``form.type_extension``
103+
tag:
104+
105+
.. configuration-block::
106+
107+
.. code-block:: yaml
108+
109+
services:
110+
acme_demo_bundle.image_type_extension:
111+
class: Acme\DemoBundle\Form\Type\ImageTypeExtension
112+
tags:
113+
- { name: form.type_extension, alias: file }
114+
115+
.. code-block:: xml
116+
117+
<service id="acme_demo_bundle.image_type_extension" class="Acme\DemoBundle\Form\Type\ImageTypeExtension">
118+
<tag name="form.type_extension" alias="file" />
119+
</service>
120+
121+
.. code-block:: php
122+
123+
$container
124+
->register('acme_demo_bundle.image_type_extension', 'Acme\DemoBundle\Form\Type\ImageTypeExtension')
125+
->addTag('form.type_extension', array('alias' => 'file'));
126+
127+
The ``alias`` key of the tag is the type of field that this extension should
128+
be applied to. In your case, as you want to extend the ``file`` field type,
129+
you will use ``file`` as an alias.
130+
131+
Adding the extension business logic
132+
-----------------------------------
133+
134+
The goal of your extension is to display a nice image next to file inputs
135+
(when the underlying model contains images). For that purpose, let's assume
136+
that you use an approach similar to the one described in
137+
:doc:`How to handle File Uploads with Doctrine</cookbook/doctrine/file_uploads>`:
138+
you have a Media model with a file property (corresponding to the file field
139+
in the form) and a path property (corresponding to the image path in the
140+
database).
141+
142+
.. code-block:: php
143+
144+
// src/Acme/DemoBundle/Entity/Media.php
145+
namespace Acme\DemoBundle\Entity;
146+
147+
use Doctrine\ORM\Mapping as ORM;
148+
use Symfony\Component\Validator\Constraints as Assert;
149+
150+
/**
151+
* @ORM\Entity
152+
* @ORM\Table
153+
*/
154+
class Media
155+
{
156+
// ...
157+
158+
/**
159+
* @var string
160+
*
161+
* @ORM\Column(name="path", type="string", length=255)
162+
*/
163+
private $path;
164+
165+
/**
166+
* @var \Symfony\Component\HttpFoundation\File\UploadedFile
167+
* @Assert\File(maxSize="2M")
168+
*/
169+
public $file;
170+
171+
// ...
172+
173+
/**
174+
* Get the image url
175+
*
176+
* @return null|string
177+
*/
178+
public function getWebPath()
179+
{
180+
// ... $webPath being the full image url, to be used in templates
181+
182+
return $webPath;
183+
}
184+
185+
Your form type extension class will need to do two things:
186+
187+
#. Override the ``getDefaultOptions`` method in order to add an image_path
188+
option;
189+
#. Override the ``buildForm`` and ``buildView`` methods in order to pass the image
190+
url to the view.
191+
192+
The logic is the following: when adding a form field of type ``file``,
193+
you will be able to specify a new option: ``image_path``. This option will
194+
tell the file field how to get the actual image url in order to display
195+
it in the view.
196+
197+
.. code-block:: php
198+
199+
// src/Acme/DemoBundle/Form/Type/ImageTypeExtension.php
200+
namespace Acme\DemoBundle\Form\Type;
201+
202+
use Symfony\Component\Form\AbstractTypeExtension;
203+
use Symfony\Component\Form\FormBuilder;
204+
use Symfony\Component\Form\FormView;
205+
use Symfony\Component\Form\FormInterface;
206+
use Symfony\Component\Form\Util\PropertyPath;
207+
208+
class ImageTypeExtension extends AbstractTypeExtension
209+
{
210+
/**
211+
* Returns the name of the type being extended.
212+
*
213+
* @return string The name of the type being extended
214+
*/
215+
public function getExtendedType()
216+
{
217+
return 'file';
218+
}
219+
220+
/**
221+
* Add the image_path option
222+
*
223+
* @param array $options
224+
*/
225+
public function getDefaultOptions(array $options)
226+
{
227+
return array('image_path' => null);
228+
}
229+
230+
/**
231+
* Store the image_path option as a builder attribute
232+
*
233+
* @param \Symfony\Component\Form\FormBuilder $builder
234+
* @param array $options
235+
*/
236+
public function buildForm(FormBuilder $builder, array $options)
237+
{
238+
if (null !== $options['image_path']) {
239+
$builder->setAttribute('image_path', $options['image_path']);
240+
}
241+
}
242+
243+
/**
244+
* Pass the image url to the view
245+
*
246+
* @param \Symfony\Component\Form\FormView $view
247+
* @param \Symfony\Component\Form\FormInterface $form
248+
*/
249+
public function buildView(FormView $view, FormInterface $form)
250+
{
251+
if ($form->hasAttribute('image_path')) {
252+
$parentData = $form->getParent()->getData();
253+
254+
$propertyPath = new PropertyPath($form->getAttribute('image_path'));
255+
$imageUrl = $propertyPath->getValue($parentData);
256+
$view->set('image_url', $imageUrl);
257+
}
258+
}
259+
260+
}
261+
262+
Override the file widget template fragment
263+
------------------------------------------
264+
265+
Each field type is rendered by a template fragment. Those template fragments
266+
can be overridden in order to customize form rendering; for more information,
267+
you can refer to the :ref:`cookbook-form-customization-form-themes` article.
268+
269+
In your extension class, you have added a new variable (``image_url``), but
270+
you still need to take advantage of this new variable in your templates.
271+
You need to override the ``file_widget`` block:
272+
273+
.. configuration-block::
274+
275+
.. code-block:: html+jinja
276+
277+
{# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
278+
{% extends 'form_div_layout.html.twig' %}
279+
280+
{% block file_widget %}
281+
{% spaceless %}
282+
283+
{{ block('field_widget') }}
284+
{% if image_url is not null %}
285+
<img src="{{ asset(image_url) }}"/>
286+
{% endif %}
287+
288+
{% endspaceless %}
289+
{% endblock %}
290+
291+
.. code-block:: html+php
292+
293+
<!-- src/Acme/DemoBundle/Resources/views/Form/file_widget.html.php -->
294+
<?php echo $view['form']->widget($form) ?>
295+
<?php if (null !== $image_url): ?>
296+
<img src="<?php echo $view['assets']->getUrl($image_url) ?>"/>
297+
<?php endif ?>
298+
299+
.. note::
300+
301+
You will need to change your config file or to explicitly specify how
302+
you want your form to be themed in order for Symfony to use your overridden
303+
block. See :ref:`cookbook-form-customization-form-themes` for more
304+
information.
305+
306+
Using the Form Type Extension
307+
------------------------------
308+
309+
From now on, when adding a field of type ``file`` in your form, you can
310+
specify an ``image_path`` option that will be used to display an image
311+
next to the file field. As an example::
312+
313+
// src/Acme/DemoBundle/Form/Type/MediaType.php
314+
namespace Acme\DemoBundle\Form;
315+
316+
use Symfony\Component\Form\AbstractType;
317+
use Symfony\Component\Form\FormBuilder;
318+
319+
class MediaType extends AbstractType
320+
{
321+
public function buildForm(FormBuilder $builder, array $options)
322+
{
323+
$builder
324+
->add('name', 'text')
325+
->add('file', 'file', array('image_path' => 'webPath'));
326+
}
327+
328+
public function getName()
329+
{
330+
return 'media';
331+
}
332+
}
333+
334+
When displaying the form, if the underlying model has already been associated
335+
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)