diff --git a/cookbook/form/create_form_type_extension.rst b/cookbook/form/create_form_type_extension.rst new file mode 100644 index 00000000000..31047475a1a --- /dev/null +++ b/cookbook/form/create_form_type_extension.rst @@ -0,0 +1,335 @@ +.. index:: + single: Form; Form type extension + +How to Create a Form Type Extension +==================================== + +:doc:`Custom form field types` are great when +you need field types with a specific purpose, such as a gender selector, +or a VAT number input. + +But sometimes, you don't really need to add new field types - you want +to add features on top of existing types. This is where form type +extensions come in. + +Form type extensions have 2 main use cases: + +#. You want to add a **generic feature to several types** (such as + adding a "help" text to every field type); +#. You want to add a **specific feature to a single type** (such + as adding a "download" feature to the "file" field type). + +In both those cases, it might be possible to achieve your goal with custom +form rendering, or custom form field types. But using form type extensions +can be cleaner (by limiting the amount of business logic in templates) +and more flexible (you can add several type extensions to a single form +type). + +Form type extensions can achieve most of what custom field types can do, +but instead of being field types of their own, **they plug into existing types**. + +Imagine that you manage a ``Media`` entity, and that each media is associated +to a file. Your ``Media`` form uses a file type, but when editing the entity, +you would like to see its image automatically rendered next to the file +input. + +You could of course do this by customizing how this field is rendered in a template. But field +type extensions allow you to do this in a nice DRY fashion. + +Defining the Form Type Extension +--------------------------------- + +Your first task will be to create the form type extension class. Let's +call it ``ImageTypeExtension``. You will store the class in a file called +``ImageTypeExtension.php``, in the ``\Form\Type`` directory. + +When creating a form type extension, you can either implement the +:class:`Symfony\\Component\\Form\\FormTypeExtensionInterface` interface, +or extend the :class:`Symfony\\Component\\Form\\AbstractTypeExtension` +class. Most of the time, you will end up extending the abstract class. +That's what you will do in this tutorial:: + + // src/Acme/DemoBundle/Form/Type/ImageTypeExtension.php + namespace Acme\DemoBundle\Form\Type; + + use Symfony\Component\Form\AbstractTypeExtension; + + class ImageTypeExtension extends AbstractTypeExtension + { + /** + * Returns the name of the type being extended. + * + * @return string The name of the type being extended + */ + public function getExtendedType() + { + return 'file'; + } + + } + +The only method you **must** implement is the ``getExtendedType`` function. +It is used to indicate the name of the form type that will be extended +by your extension. + +.. tip:: + + The value you return in the ``getExtendedType`` method corresponds + to the value returned by the ``getName`` method in the form type class + you wish to extend. + +In addition to the ``getExtendedType`` function, you will probably want +to override one of the following methods: + +* ``buildForm()`` + +* ``buildView()`` + +* ``getDefaultOptions()`` + +* ``getAllowedOptionValues()`` + +* ``buildViewBottomUp()`` + +For more information on what those methods do, you can refer to the +:doc:`Creating Custom Field Types` +cookbook article. + +Registering your Form Type Extension as a Service +-------------------------------------------------- + +The next step is to make Symfony aware of your extension. All you +need to do is to declare it as a service by using the ``form.type_extension`` +tag: + +.. configuration-block:: + + .. code-block:: yaml + + services: + acme_demo_bundle.image_type_extension: + class: Acme\DemoBundle\Form\Type\ImageTypeExtension + tags: + - { name: form.type_extension, alias: file } + + .. code-block:: xml + + + + + + .. code-block:: php + + $container + ->register('acme_demo_bundle.image_type_extension', 'Acme\DemoBundle\Form\Type\ImageTypeExtension') + ->addTag('form.type_extension', array('alias' => 'file')); + +The ``alias`` key of the tag is the type of field that this extension should +be applied to. In your case, as you want to extend the ``file`` field type, +you will use ``file`` as an alias. + +Adding the extension business logic +----------------------------------- + +The goal of your extension is to display a nice image next to file inputs +(when the underlying model contains images). For that purpose, let's assume +that you use an approach similar to the one described in +:doc:`How to handle File Uploads with Doctrine`: +you have a Media model with a file property (corresponding to the file field +in the form) and a path property (corresponding to the image path in the +database). + +.. code-block:: php + + // src/Acme/DemoBundle/Entity/Media.php + namespace Acme\DemoBundle\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + /** + * @ORM\Entity + * @ORM\Table + */ + class Media + { + // ... + + /** + * @var string + * + * @ORM\Column(name="path", type="string", length=255) + */ + private $path; + + /** + * @var \Symfony\Component\HttpFoundation\File\UploadedFile + * @Assert\File(maxSize="2M") + */ + public $file; + + // ... + + /** + * Get the image url + * + * @return null|string + */ + public function getWebPath() + { + // ... $webPath being the full image url, to be used in templates + + return $webPath; + } + +Your form type extension class will need to do two things: + +#. Override the ``getDefaultOptions`` method in order to add an image_path + option; +#. Override the ``buildForm`` and ``buildView`` methods in order to pass the image + url to the view. + +The logic is the following: when adding a form field of type ``file``, +you will be able to specify a new option: ``image_path``. This option will +tell the file field how to get the actual image url in order to display +it in the view. + +.. code-block:: php + + // src/Acme/DemoBundle/Form/Type/ImageTypeExtension.php + namespace Acme\DemoBundle\Form\Type; + + use Symfony\Component\Form\AbstractTypeExtension; + use Symfony\Component\Form\FormBuilder; + use Symfony\Component\Form\FormView; + use Symfony\Component\Form\FormInterface; + use Symfony\Component\Form\Util\PropertyPath; + + class ImageTypeExtension extends AbstractTypeExtension + { + /** + * Returns the name of the type being extended. + * + * @return string The name of the type being extended + */ + public function getExtendedType() + { + return 'file'; + } + + /** + * Add the image_path option + * + * @param array $options + */ + public function getDefaultOptions(array $options) + { + return array('image_path' => null); + } + + /** + * Store the image_path option as a builder attribute + * + * @param \Symfony\Component\Form\FormBuilder $builder + * @param array $options + */ + public function buildForm(FormBuilder $builder, array $options) + { + if (null !== $options['image_path']) { + $builder->setAttribute('image_path', $options['image_path']); + } + } + + /** + * Pass the image url to the view + * + * @param \Symfony\Component\Form\FormView $view + * @param \Symfony\Component\Form\FormInterface $form + */ + public function buildView(FormView $view, FormInterface $form) + { + if ($form->hasAttribute('image_path')) { + $parentData = $form->getParent()->getData(); + + $propertyPath = new PropertyPath($form->getAttribute('image_path')); + $imageUrl = $propertyPath->getValue($parentData); + $view->set('image_url', $imageUrl); + } + } + + } + +Override the file widget template fragment +------------------------------------------ + +Each field type is rendered by a template fragment. Those template fragments +can be overridden in order to customize form rendering; for more information, +you can refer to the :ref:`cookbook-form-customization-form-themes` article. + +In your extension class, you have added a new variable (``image_url``), but +you still need to take advantage of this new variable in your templates. +You need to override the ``file_widget`` block: + +.. configuration-block:: + + .. code-block:: html+jinja + + {# src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #} + {% extends 'form_div_layout.html.twig' %} + + {% block file_widget %} + {% spaceless %} + + {{ block('field_widget') }} + {% if image_url is not null %} + + {% endif %} + + {% endspaceless %} + {% endblock %} + + .. code-block:: html+php + + + widget($form) ?> + + + + +.. note:: + + You will need to change your config file or to explicitly specify how + you want your form to be themed in order for Symfony to use your overridden + block. See :ref:`cookbook-form-customization-form-themes` for more + information. + +Using the Form Type Extension +------------------------------ + +From now on, when adding a field of type ``file`` in your form, you can +specify an ``image_path`` option that will be used to display an image +next to the file field. As an example:: + + // src/Acme/DemoBundle/Form/Type/MediaType.php + namespace Acme\DemoBundle\Form; + + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\FormBuilder; + + class MediaType extends AbstractType + { + public function buildForm(FormBuilder $builder, array $options) + { + $builder + ->add('name', 'text') + ->add('file', 'file', array('image_path' => 'webPath')); + } + + public function getName() + { + return 'media'; + } + } + +When displaying the form, if the underlying model has already been associated +with an image, you will see it displayed next to the file input. \ No newline at end of file diff --git a/cookbook/form/index.rst b/cookbook/form/index.rst index 1dba8aa6f98..e60f02c9a68 100644 --- a/cookbook/form/index.rst +++ b/cookbook/form/index.rst @@ -9,4 +9,5 @@ Form dynamic_form_generation form_collections create_custom_field_type + create_form_type_extension use_virtuals_forms diff --git a/cookbook/map.rst.inc b/cookbook/map.rst.inc index e838e76be3b..64808463e75 100644 --- a/cookbook/map.rst.inc +++ b/cookbook/map.rst.inc @@ -71,6 +71,7 @@ * :doc:`/cookbook/form/dynamic_form_generation` * :doc:`/cookbook/form/form_collections` * :doc:`/cookbook/form/create_custom_field_type` + * :doc:`/cookbook/form/create_form_type_extension` * :doc:`/cookbook/form/use_virtuals_forms` * (validation) :doc:`/cookbook/validation/custom_constraint` * (doctrine) :doc:`/cookbook/doctrine/file_uploads`