Skip to content

Commit 8f4aa8c

Browse files
committed
feature symfony#5375 Added a new cookbook about file uploading (javiereguiluz)
This PR was merged into the 2.3 branch. Discussion ---------- Added a new cookbook about file uploading | Q | A | ------------- | --- | Doc fix? | no | New docs? | yes | Applies to | all | Fixed tickets | symfony#2346 This PR is completely based on the work made by @saro0h in symfony#4018. All the credit for the original work goes to her. I just picked her work and applied the suggestions made by @weaverryan @xabbuh and @wouterj to make it mergeable. If you agree with this PR, please merge it as soon as possible because this is a very important topic and we're a bit late on this doc. Thanks! Commits ------- 4a7709b Fixed all the issues spotted by Ryan 20ae21b Added a new cookbook about file uploading
2 parents 1226b20 + 4a7709b commit 8f4aa8c

File tree

3 files changed

+179
-0
lines changed

3 files changed

+179
-0
lines changed

cookbook/controller/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ Controller
66

77
error_pages
88
service
9+
upload_file

cookbook/controller/upload_file.rst

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
.. index::
2+
single: Controller; Upload; File
3+
4+
How to Upload Files
5+
===================
6+
7+
.. note::
8+
9+
Instead of handling file uploading yourself, you may consider using the
10+
`VichUploaderBundle`_ community bundle. This bundle provides all the common
11+
operations (such as file renaming, saving and deleting) and it's tightly
12+
integrated with Doctrine ORM, MongoDB ODM, PHPCR ODM and Propel.
13+
14+
Imagine that you have a ``Product`` entity in your application and you want to
15+
add a PDF brochure for each product. To do so, add a new property called ``brochure``
16+
in the ``Product`` entity::
17+
18+
// src/AppBundle/Entity/Product.php
19+
namespace AppBundle\Entity;
20+
21+
use Doctrine\ORM\Mapping as ORM;
22+
use Symfony\Component\Validator\Constraints as Assert;
23+
24+
class Product
25+
{
26+
// ...
27+
28+
/**
29+
* @ORM\Column(type="string")
30+
*
31+
* @Assert\NotBlank(message="Please, upload the product brochure as a PDF file.")
32+
* @Assert\File(mimeTypes={ "application/pdf" })
33+
*/
34+
private $brochure;
35+
36+
public function getBrochure()
37+
{
38+
return $this->brochure;
39+
}
40+
41+
public function setBrochure($brochure)
42+
{
43+
$this->brochure = $brochure;
44+
45+
return $this;
46+
}
47+
}
48+
49+
Note that the type of the ``brochure`` column is ``string`` instead of ``binary``
50+
or ``blob`` because it just stores the PDF file name instead of the file contents.
51+
52+
Then, add a new ``brochure`` field to the form that manage the ``Product`` entity::
53+
54+
// src/AppBundle/Form/ProductType.php
55+
namespace AppBundle\Form;
56+
57+
use Symfony\Component\Form\AbstractType;
58+
use Symfony\Component\Form\FormBuilderInterface;
59+
use Symfony\Component\OptionsResolver\OptionsResolver;
60+
61+
class ProductType extends AbstractType
62+
{
63+
public function buildForm(FormBuilderInterface $builder, array $options)
64+
{
65+
$builder
66+
// ...
67+
->add('brochure', 'file', array('label' => 'Brochure (PDF file)'))
68+
// ...
69+
;
70+
}
71+
72+
public function configureOptions(OptionsResolver $resolver)
73+
{
74+
$resolver->setDefaults(array(
75+
'data_class' => 'AppBundle\Entity\Product',
76+
));
77+
}
78+
79+
public function getName()
80+
{
81+
return 'product';
82+
}
83+
}
84+
85+
Now, update the template that renders the form to display the new ``brochure``
86+
field (the exact template code to add depends on the method used by your application
87+
to :doc:`customize form rendering </cookbook/form/form_customization>`):
88+
89+
.. code-block:: html+jinja
90+
91+
{# app/Resources/views/product/new.html.twig #}
92+
<h1>Adding a new product</h1>
93+
94+
{{ form_start() }}
95+
{# ... #}
96+
97+
{{ form_row(form.brochure) }}
98+
{{ form_end() }}
99+
100+
Finally, you need to update the code of the controller that handles the form::
101+
102+
// src/AppBundle/Controller/ProductController.php
103+
namespace AppBundle\ProductController;
104+
105+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
106+
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
107+
use Symfony\Component\HttpFoundation\Request;
108+
use AppBundle\Entity\Product;
109+
use AppBundle\Form\ProductType;
110+
111+
class ProductController extends Controller
112+
{
113+
/**
114+
* @Route("/product/new", name="app_product_new")
115+
*/
116+
public function newAction(Request $request)
117+
{
118+
$product = new Product();
119+
$form = $this->createForm(new ProductType(), $product);
120+
$form->handleRequest($request);
121+
122+
if ($form->isValid()) {
123+
// $file stores the uploaded PDF file
124+
/** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */
125+
$file = $product->getBrochure()
126+
127+
// Generate a unique name for the file before saving it
128+
$fileName = md5(uniqid()).'.'.$file->guessExtension();
129+
130+
// Move the file to the directory where brochures are stored
131+
$brochuresDir = $this->container->getParameter('kernel.root_dir').'/../web/uploads/brochures';
132+
$file->move($brochuresDir, $fileName);
133+
134+
// Update the 'brochure' property to store the PDF file name
135+
// instead of its contents
136+
$product->setBrochure($filename);
137+
138+
// persist the $product variable or any other work...
139+
140+
return $this->redirect($this->generateUrl('app_product_list'));
141+
}
142+
143+
return $this->render('product/new.html.twig', array(
144+
'form' => $form->createView()
145+
));
146+
}
147+
}
148+
149+
There are some important things to consider in the code of the above controller:
150+
151+
#. When the form is uploaded, the ``brochure`` property contains the whole PDF
152+
file contents. Since this property stores just the file name, you must set
153+
its new value before persisting the changes of the entity.
154+
#. In Symfony applications, uploaded files are objects of the
155+
:class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` class, which
156+
provides methods for the most common operations when dealing with uploaded files.
157+
#. A well-known security best practice is to never trust the input provided by
158+
users. This also applies to the files uploaded by your visitors. The ``Uploaded``
159+
class provides methods to get the original file extension (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getExtension()`),
160+
the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getSize()`)
161+
and the original file name (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalName()`).
162+
However, they are considered *not safe* because a malicious user could tamper
163+
that information. That's why it's always better to generate a unique name and
164+
use the :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::guessExtension()`
165+
method to let Symfony guess the right extension according to the file MIME type.
166+
#. The ``UploadedFile`` class also provides a :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::move()`
167+
method to store the file in its intended directory. Defining this directory
168+
path as an application configuration option is considered a good practice that
169+
simplifies the code: ``$this->container->getParameter('brochures_dir')``.
170+
171+
You can now use the following code to link to the PDF brochure of an product:
172+
173+
.. code-block:: html+jinja
174+
175+
<a href="{{ asset('uploads/brochures' ~ product.brochure) }}">View brochure (PDF)</a>
176+
177+
.. _`VichUploaderBundle`: https://github.com/dustin10/VichUploaderBundle

cookbook/map.rst.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050

5151
* :doc:`/cookbook/controller/error_pages`
5252
* :doc:`/cookbook/controller/service`
53+
* :doc:`/cookbook/controller/upload_file`
5354

5455
* **Debugging**
5556

0 commit comments

Comments
 (0)