Enhanced file type for Symfony forms
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
The next official pages describe how to exploit this extension point wisely:
- How to Create a Custom Form Field Type
- How to Dynamically Modify Forms Using Form Events
- How to Use Data Transformers
- Form Events
- How to Customize Form Rendering
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
- Manages file upload: moves it to final location, renames it properly,
- Optionally removes previously uploaded file,
- Maps uploaded file to sting filed: assigns the name of newly uploaded file to string field,
- Shows the link to download previously uploaded file near the file input.
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:
- string field that holds file name and maps it into the database,
- file filed that maps it to the form.
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
Have you noticed that this snippet is strangely similar to ordinary form types
for your entities? You created them following
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
SuperLargeEntityWithMillionFieldsType for your
SuperLargeEntityWithMillionFields entity are the objects implementing the same
interface with same potential. Here is the trick. Once you define your
as a service with
form.type tag, you create a reusable, shared, common field
type. Naturally, you can alter the method
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 (
DateType …) or composite (
…). 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
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
at the table to get the idea of the pattern:
|Command||Form||you use this object to create view and handle request|
|integer||Form||This is a simple widget|
||Form||This is a compound widget that has children. Each child field is also a Form object whether it is simple or compound|
||Form||You can iterate
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:
which can be retrieved as:
Here is a visual representation of field in the entity, the form object and a
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:
- storing the name of previously uploaded file, while rendering. This can be used to show a download link
<a href="link_to_download">near to the file upload input
<input type="file" name="form[document][file]">
- when you edit the entity and no new file is uploaded,
fileNamewill still store the initial value. Therefore,
documentin the entity remains unchanged
- when there is a new file uploaded via
<input type="file" name="form[document][file]">the
fileNamewill contain the name of newly uploaded file
While creating a form object either with
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.
EnhancedFIleType the string value in entity field must be assigned to array of two elements (
fieldName receives the value from this entity field and
file will always be
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.
Transformers are attached to form fields by means of builder’s methods:
EnhancedFileType now will look like:
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 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:
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:
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.
Twig fragment that renders EnhancedFileType has a prefix of
enhaced_file. In Symfony 3 it is controlled through
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
finishView because the views for children (
fileName) are already constructed.
download_url will be accessible in
enhanced_file fragment and its
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.