http://vria.eu SL

Wellcome to my professional blog dedicated to interesting things in programming, web-development and design patterns.

Creating enhanced file type for Symfony forms

Apr 10, 2016 php symfony forms

If you are searching universal solution for handling file uploads for Symfony forms you will be glad to read this article. In case you want to conceive the details of form field types, form events, data transformers and see them in action read this article carefully in addition to official tutorials that I will suggest below.

enhanced_file.png

Creating reusable software is far more complicated then ad-hoc one. However, developing reusable component for existing framework is assumed an easier task because there are always guidelines in case if extension points are provided. Nevertheless, sometimes the guidelines are confusing and extension points are limited.

Hopefully, Symfony in general and its Form Component in particular are highly extendible and customizable. All form field types are plugins of the Form Component added to framework by means of form.type service tag and FormTypeInterface interface or AbstractType class. I call these entities an extension point.

The next official pages describe how to exploit this extension point wisely:

Practical content on how to create custom form field type is given below in this article. The aim is to develop field type for file upload without repetitive code that you have to normally write behind each file input.

Usually only the file name or file path is persisted in database while real file is stored in filesystem. E.g. for ‘Candidate’ entity there is often a string field to hold file name, for example CV. Imagine you create a form to enable user to enter his information and upload a file with CV. When you build this form, there is no given method to transform uploaded file inside form object into string value inside entity.

The idea is to hide file manipulation in FormType class. Wouldn’t it be nice to have only the string field to store the file name in the entity while form widget performs all the heavy lifting? We are going to create the form field type that:

There is an official guide on how to organize file uploads How to Handle File Uploads with Doctrine. This guidance advises you on how to make the entity containing the file responsible for its handling (uploading, moving). With this approach, you have to take care of two fields:

All the code of manipulation and transformation values between these fields is located in the entity. If there are multiple entity classes that store files, you will duplicate the handling code unless you are using inheritance or traits. I strongly dissuade you from this decision – a lot of complexity for nothing. Moreover, this solution is quite specific (simply because of hardcoded field name) and is hardly reusable.

So, for a new form field type we need to create a class extending from AbstractType:



use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class EnhancedFileType extends AbstractType
{
    /**
     * Builds the form.
     *
     * This method is called for each type in the hierarchy starting from the
     * top most type. Type extensions can further modify the form.
     *
     * @see FormTypeExtensionInterface::buildForm()
     *
     * @param FormBuilderInterface $builder The form builder
     * @param array                $options The options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('fileName', 'hidden')
            ->add('file', 'file', array(
                'label' => false
            ));
    }

    /**
     * Returns the name of this type.
     *
     * @return string The name of this type
     */
    public function getName()
    {
        return 'enhanced_file';
    }
}

Have you noticed that this snippet is strangely similar to ordinary form types for your entities? You created them following this guide. The FormType that you create to build the form object for your entity is the same type as for the custom field type. The tiniest TextType and the largest SuperLargeEntityWithMillionFieldsType for your SuperLargeEntityWithMillionFields entity are the objects implementing the same interface with same potential. Here is the trick. Once you define your FormType as a service with form.type tag, you create a reusable, shared, common field type. Naturally, you can alter the method buildFrom of TextType and add as many other field as you want, just the same way you build SuperLargeEntityWithMillionFieldsType by adding a lot of TextType. Although this idea seems to be silly and one would never do it, it demonstrates the core pattern of Symfony forms component: composite pattern.

As you know fields in Symfony forms can be simple (TextType, IntegerType, FileType, DateType ...) or composite (CollectionType, RepeatedType, ...). Composite fields contain simple or composite fields, simple fields generally renders simple inputs and cannot contain any children fields. Composite and simple fields implement the same interface FormTypeInterface. Each form item is guaranteed to be the same type: form itself, its composite fields and its simple fields. The tree structure of the form’s object reflects the structure of the entity:

In the example above form is built by adding two fields name and items. Look at the table to get the idea of the pattern:

Variable Type Variable Type Comments
$command Command $form Form you use this $form object to create view and handle request
$command->getNumber() integer $form->get('number') Form This is a simple widget
$command->getItems() array $form->get('items') Form This is a compound widget that has children. Each child field is also a Form object whether it is simple or compound
$command->getItems()[0]
$command->getItems()[1]
Item $form->get('items')->get(0)
$form->get('items')->get(1)
Form You can iterate $form->get('items') to get/handle/show each Item Form object

That is how Composite pattern is implemented in Symfony. Sometimes it is reasonable to add several widgets for some mapped field in entity. In our case, we add two fields:


$builder->add('fileName', HiddenType::class)
        ->add('file', FileType::class, array(
              'label' => false
        ))

which can be retrieved as:


$form->get('name_of_file_field')->get('fileName');
$form->get('name_of_file_field')->get('file');

Here is a visual representation of field in the entity, the form object and a formView object:

file field is added to show file upload input. When you show the form, fileName will store the name of previously uploaded file. When the form is submitted, fileName will store the name of a newly uploaded file.

filename has multiple functions:

While creating a form object either with createFormBuilder or createForm you can assign an object or array to the form. For the mapped (you remember mapped option of each form type) fields their values will be assigned automatically to form widget.

As for EnhancedFIleType the string value in entity field must be assigned to array of two elements (fieldName, file) where fieldName receives the value from this entity field and file will always be null.

While submitting the form each mapped field of entity will receive the value from corresponding form widget. In EnhancedFIleType the array should be transformed to the string.

For this kind of transformations there is a concept of data transformers How to Use Data Transformers. In fact, each time the form is created or submitted, data is transformed like that:

The "model" data format is required by the form's object, the "normalized" data format is used for internal processing, the "view" data format is displayed in the html document. In other words, model data is just your entity (or its aggregated part), while normalized data belongs to form’s object and is destined for internal processing, and view data belongs to FormView and is used for displaying.

Transformers are to implement DataTransformerInterface, usually they are relatively simple.


namespace VRia\Bundle\EnhancedFileBundle\Form;

use Symfony\Component\Form\DataTransformerInterface;

class StringToEnhancedFileTransformer implements DataTransformerInterface
{
    public function transform($value)
    {
        return array('fileName' => $value, 'file' => null);
    }

    public function reverseTransform($value)
    {
        return $value['fileName'];
    }
}

Transformers are attached to form fields by means of builder’s methods: addViewTransformer or addModelTransformer. buildForm of EnhancedFileType now will look like:


public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->add('fileName', HiddenType::class)
        ->add('file', FileType::class, array(
            'label' => false
        ))
        ->addModelTransformer(new StringToEnhancedFileTransformer());
}

With model transformer attached to EnhancedFileType the field type is already operational. There will be no errors anymore during the assignment of the string in entity to array in form object and vice versa.

Now in the browser you should see only <input type="file" name="form[document][file]"> because <input name="form[document][fileName]" type="hidden"> input is hidden. You can try to upload file but it will go nowhere because we did not handle uploaded files yet. Handling uploaded files will be performed just before transformer executes reserseTransform function. The idea is that we move newly uploaded file stored in file form field to its final destination and assign its name to fileName form field. After that, array ('fileName' => 'the_name_of_newly_uploaded_and_moved_file', 'file' => 'newly_uploaded_and_moved_file') is transformed to just the_name_of_newly_uploaded_and_moved_file. The latter is the model data that is bound to entity.

The handling code will be placed to form event listener in order to be executed just before model transformer. I suggest you reading Form Events official documentation and observer pattern article.


public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->add('fileName', HiddenType::class)
        ->add('file', FileType::class, array(
            'label' => false
        ))
        ->addModelTransformer(new StringToEnhancedFileTransformer())
        ->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) use ($options) {
            $data = $event->getData();

            if ($data['file'] instanceof UploadedFile) {
                if ($options['delete_previous_file']) {
                    $oldFilePath = $options['directory_path'] . '/' . $data['fileName'];
                    if (is_file($oldFilePath)) {
                        unlink($oldFilePath);
                    }
                }
                $fileName = md5(uniqid()) . '.' . $data['file']->guessClientExtension();
                $data['file']->move($options['directory_path'], $fileName);
                $data['fileName'] = $fileName;
            }

            $event->setData($data);
        });
}

The closure passed to addEventListener will be executed during PRE_SUBMIT event. Form data, which you can modify, is passed to this closure as an argument. When the real file is uploaded it will be stored in $data[‘file’] enveloped into UploadedFile object, otherwise $data[‘file’] will be null. If $data[‘file’] contains UploadedFile, we move it to directory_path directory and overwrite fileName with the name of new uploaded file.

directory_path is the option that we should specify during form construction:


$formBuilder->add('file', EnhancedFileType::class, array(
    'label' => ‘My awesome file ',
    'directory_path' => $this->get('kernel')->getRootDir() . '/../web/upload/',
    'public_directory_path' => '/upload/',
    'delete_previous_file' => false
))

directory_path, publilc_directory_path and delete_previous_file are the custom options that will permit users to configure EnhancedFileType. directory_path is the full path to directory where you want to store uploaded files, publilc_directory_path is the same directory accessible from the web, ‘delete_previous_file’ controls whether we should delete previously uploaded file or not. To allow EnhancedFileType receive additional options configureOptions method should be used:


/**
 * Configures the options for this type.
 *
 * @param OptionsResolver $resolver The resolver for the options
 */
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array('compound' => true, 'delete_previous_file' => true));
    $resolver->setRequired(array('directory_path', 'public_directory_path'));
}

The final step of development is to add download link. The latter will be shown on condition that the entity has already been persisted to database and the file has already been uploaded.


{% use 'form_div_layout.html.twig' %}

{% block enhanced_file_widget -%}
    {{ block('form_widget_compound') -}}
    {% if download_url %}Download file{% endif %}
{%- endblock %}

Twig fragment that renders EnhancedFileType has a prefix of enhaced_file. In Symfony 3 it is controlled through getBlockPrefix method:


class EnhancedFileType extends AbstractType
{
    /**
     * Returns the prefix of the template block name for this type.
     *
     * The block prefix defaults to the underscored short class name with
     * the "Type" suffix removed (e.g. "UserProfileType" => "user_profile").
     *
     * @return string The prefix of the template block name
     */
    public function getBlockPrefix()
    {
        return 'enhanced_file';
    }
}

Inside the enhanced_file_widget fragment you can access variables from FormView, namely from $formView->vars array. Obviously, you can add and remove variables from vars array. If we want to access download_url variable in the fragment we have to add it in buildView or finishView method:


class EnhancedFileType extends AbstractType
{
    /**
     * Finishes the form view.
     *
     * This method gets called for each type in the hierarchy starting from the
     * top most type. Type extensions can further modify the view.
     *
     * When this method is called, views of the form's children have already
     * been built and finished and can be accessed. You should only implement
     * such logic in this method that actually accesses child views. For everything
     * else you are recommended to implement {@link buildView()} instead.
     *
     * @see FormTypeExtensionInterface::finishView()
     *
     * @param FormView      $view    The view
     * @param FormInterface $form    The form
     * @param array         $options The options
     */
    public function finishView(FormView $view, FormInterface $form, array $options)
    {
        $fileName = $form->get('fileName')->getData();
        $view->vars['download_url'] = $fileName ? $options['public_directory_path'] . $fileName : null;
    
        $view->children['file']->vars = array_replace($view->vars, $view->children['file']->vars);
    }
}

I use finishView because the views for children (file, fileName) are already constructed. download_url will be accessible in enhanced_file fragment and its file child.

To sum up, I would like emphasize that Symfony form bundle provides many useful extension points. You are allowed to create any form field type, you are able to intrude at any step of data processing and to customize form rendering.

I encourage you to examine the code of EnhancedFieldType for Symfony 2 and Symfony 3. Also you can use it in your projects. To do so just run:


composer require vria/enhanced-file