Skip to content

Commit 381c68e

Browse files
committed
Merge pull request #400 from symfony/form-file-uploads
Form file uploads
2 parents 605a5f4 + 9479539 commit 381c68e

File tree

6 files changed

+413
-12
lines changed

6 files changed

+413
-12
lines changed

book/forms.rst

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,14 +1149,17 @@ HTML form so that the user can modify that data. The second goal of a form is to
11491149
take the data submitted by the user and to re-apply it to the object.
11501150

11511151
There's still much more to learn about the powerful world of forms, such as
1152-
how to handle file uploads and how to create a form where a dynamic number
1153-
of sub-forms can be added (e.g. a todo list where you can keep adding more
1154-
fields via Javascript before submitting). See the cookbook for these topics.
1152+
how to handle :doc:`file uploads with Doctrine
1153+
</cookbook/doctrine/file_uploads>` or how to create a form where a dynamic
1154+
number of sub-forms can be added (e.g. a todo list where you can keep adding
1155+
more fields via Javascript before submitting). See the cookbook for these
1156+
topics.
11551157

11561158
Learn more from the Cookbook
11571159
----------------------------
11581160

1159-
* :doc:`Handling File Uploads </cookbook/form/file_uploads>`
1161+
* :doc:`/cookbook/doctrine/file_uploads`
1162+
* :doc:`File Field Reference </reference/forms/types/file>`
11601163
* :doc:`Creating Custom Field Types </cookbook/form/create_custom_field_type>`
11611164
* :doc:`/cookbook/form/twig_form_customization`
11621165

cookbook/doctrine/file_uploads.rst

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
How to handle File Uploads with Doctrine
2+
========================================
3+
4+
Handling file uploads with Doctrine entities is no different than handling
5+
any other file upload. In other words, you're free to move the file in your
6+
controller after handling a form submission. For examples of how to do this,
7+
see the :doc:`file type reference</reference/forms/types/file>` page.
8+
9+
If you choose to, you can also integrate the file upload into your entity
10+
lifecycle (i.e. creation, update and removal). In this case, as your entity
11+
is created, updated, and removed from Doctrine, the file uploading and removal
12+
processing will take place automatically (without needing to do anything in
13+
your controller);
14+
15+
To make this work, you'll need to take care of a number of details, which
16+
will be covered in this cookbook entry.
17+
18+
Basic Setup
19+
-----------
20+
21+
First, create a simple Doctrine Entity class to work with::
22+
23+
// src/Acme/DemoBundle/Entity/Download.php
24+
namespace Acme\DemoBundle\Entity;
25+
26+
use Doctrine\ORM\Mapping as ORM;
27+
use Symfony\Component\Validator\Constraints as Assert;
28+
29+
/**
30+
* @ORM\Entity
31+
*/
32+
class Download
33+
{
34+
/**
35+
* @ORM\Id @ORM\Column(type="integer")
36+
* @ORM\GeneratedValue(strategy="AUTO")
37+
*/
38+
public $id;
39+
40+
/**
41+
* @ORM\Column(type="string", length=255)
42+
* @Assert\NotBlank
43+
*/
44+
public $name;
45+
46+
/**
47+
* @ORM\Column(type="string", length=255, nullable=true)
48+
*/
49+
public $path;
50+
51+
public function getFullPath()
52+
{
53+
return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->path;
54+
}
55+
56+
protected function getUploadRootDir()
57+
{
58+
return '/path/to/uploaded/documents';
59+
}
60+
}
61+
62+
The ``Download`` entity has a name and it is associated with a file. The ``path``
63+
property stores the relative path to the file and is persisted to the database.
64+
The ``getFullPath()`` is a convenience method that uses the ``getUploadRootDir()``
65+
method to return the absolute path to the download file.
66+
67+
.. tip::
68+
69+
If you have not done so already, you should probably read the
70+
:doc:`file</reference/forms/types/file>` type documentation first to
71+
understand how the basic upload process works.
72+
73+
To handle the actual file upload in the form, use a "virtual" ``file`` field.
74+
For example, if you're building your form directly in a controller, it might
75+
look like this::
76+
77+
public function uploadAction()
78+
{
79+
// ...
80+
81+
$form = $this->createFormBuilder($document)
82+
->add('name')
83+
->add('file')
84+
->getForm()
85+
;
86+
87+
// ...
88+
}
89+
90+
Next, create this property on your ``Download`` class and add some validation
91+
rules::
92+
93+
// src/Acme/DemoBundle/Entity/Download.php
94+
95+
// ...
96+
class Download
97+
{
98+
/**
99+
* @Assert\File(maxSize="6000000")
100+
*/
101+
public $file;
102+
103+
// ...
104+
}
105+
106+
.. note::
107+
108+
As you are using the ``File`` constraint, Symfony2 will automatically guess
109+
that the form field is a file upload input. That's why you did not have
110+
to set it explicitly when creating the form above (``->add('file')``).
111+
112+
The following controller shows you how to handle the entire process::
113+
114+
use Acme\DemoBundle\Entity\Download;
115+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
116+
// ...
117+
118+
/**
119+
* @Template()
120+
*/
121+
public function uploadAction()
122+
{
123+
$download = new Download();
124+
$form = $this->createFormBuilder($download)
125+
->add('name')
126+
->add('file')
127+
->getForm()
128+
;
129+
130+
if ($this->getRequest()->getMethod() === 'POST') {
131+
$form->bindRequest($this->getRequest());
132+
if ($form->isValid()) {
133+
$em = $this->getDoctrine()->getEntityManager();
134+
135+
$em->persist($download);
136+
$em->flush();
137+
138+
$this->redirect($this->generateUrl('...'));
139+
}
140+
}
141+
142+
return array('form' => $form->createView());
143+
}
144+
145+
.. note::
146+
147+
When writing the template, don't forget to set the ``enctype`` attribute:
148+
149+
.. code-block:: html+php
150+
151+
<h1>Upload File</h1>
152+
153+
<form action="#" method="post" {{ form_enctype(form) }}>
154+
{{ form_widget(form) }}
155+
156+
<input type="submit" value="Upload Document" />
157+
</form>
158+
159+
The previous controller will automatically persist the ``Download`` entity
160+
with the submitted name, but it will do nothing about the file and the ``path``
161+
property will be blank.
162+
163+
An easy way to handle the file upload is to move it just before the entity is
164+
persisted and then set the ``path`` property accordingly. Start by calling
165+
a new ``upload()`` method on the ``Download`` class, which you'll create
166+
in a moment to handle the file upload::
167+
168+
if ($form->isValid()) {
169+
$em = $this->getDoctrine()->getEntityManager();
170+
171+
$document->upload();
172+
173+
$em->persist($document);
174+
$em->flush();
175+
176+
$this->redirect('...');
177+
}
178+
179+
The ``upload()`` method will take advantage of the :class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile`
180+
object, which is what's returned after a ``file`` field is submitted::
181+
182+
public function upload()
183+
{
184+
// the file property can be empty if the field is not required
185+
if (!$this->file) {
186+
return;
187+
}
188+
189+
// we use the original file name here but you should
190+
// sanitize it at least to avoid any security issues
191+
192+
// move takes the target directory and then the target filename to move to
193+
$this->file->move($this->getUploadRootDir(), $this->file->getOriginalName());
194+
195+
// set the path property to the filename where you'ved saved the file
196+
$this->setPath($this->file->getOriginalName());
197+
198+
// clean up the file property as you won't need it anymore
199+
unset($this->file);
200+
}
201+
202+
Using Lifecycle Callbacks
203+
-------------------------
204+
205+
Even if this implementation works, it suffers from a major flaw: What if there
206+
is a problem when the entity is persisted? The file would have already moved
207+
to its final location even though the entity's ``path`` property didn't
208+
persist correctly.
209+
210+
To avoid these issues, you should change the implementation so that the database
211+
operation and the moving of the file become atomic: if there is a problem
212+
persisting the entity or if the file cannot be moved, then *nothing* should
213+
happen.
214+
215+
To do this, you need to move the file right as Doctrine persists the entity
216+
to the database. This can be accomplished by hooking into an entity lifecycle
217+
callback::
218+
219+
/**
220+
* @ORM\Entity
221+
* @ORM\HasLifecycleCallbacks
222+
*/
223+
class Download
224+
{
225+
}
226+
227+
Next, refactor the ``Download`` class to take advantage of these callbacks::
228+
229+
use Symfony\Component\HttpFoundation\File\UploadedFile;
230+
231+
/**
232+
* @ORM\Entity
233+
* @ORM\HasLifecycleCallbacks
234+
*/
235+
class Download
236+
{
237+
/**
238+
* @ORM\PrePersist()
239+
*/
240+
public function preUpload()
241+
{
242+
if ($this->file) {
243+
// do whatever you want to generate a unique name
244+
$this->setPath(uniq().'.'.$this->file->guessExtension());
245+
}
246+
}
247+
248+
/**
249+
* @ORM\PostPersist()
250+
*/
251+
public function upload()
252+
{
253+
if (!$this->file) {
254+
return;
255+
}
256+
257+
// you must throw an exception here if the file cannot be moved
258+
// so that the entity is not persisted to the database
259+
// which the UploadedFile move() method does automatically
260+
$this->file->move($this->getUploadRootDir(), $this->path);
261+
262+
unset($this->file);
263+
}
264+
265+
/**
266+
* @ORM\PostRemove()
267+
*/
268+
public function removeUpload()
269+
{
270+
if ($file = $this->getFullPath()) {
271+
unlink($file);
272+
}
273+
}
274+
}
275+
276+
The class now does everything you need: it generates a unique filename before
277+
persisting, moves the file after persisting, and removes the file if the
278+
entity is ever deleted.
279+
280+
Using the ``id`` as the filename
281+
--------------------------------
282+
283+
If you want to use the ``id`` as the name of the file, the implementation is
284+
slightly different as you need to save the extension under the ``path``
285+
property, instead of the actual filename::
286+
287+
use Symfony\Component\HttpFoundation\File\UploadedFile;
288+
289+
/**
290+
* @ORM\Entity
291+
* @ORM\HasLifecycleCallbacks
292+
*/
293+
class Document
294+
{
295+
/**
296+
* @ORM\PrePersist()
297+
*/
298+
public function preUpload()
299+
{
300+
if ($this->file) {
301+
$this->setPath($this->file->guessExtension());
302+
}
303+
}
304+
305+
/**
306+
* @ORM\PostPersist()
307+
*/
308+
public function upload()
309+
{
310+
if (!$this->file) {
311+
return;
312+
}
313+
314+
// you must throw an exception here if the file cannot be moved
315+
// so that the entity is not persisted to the database
316+
// which the UploadedFile move() method does
317+
$this->file->move($this->getUploadRootDir(), $this->id.'.'.$this->file->guessExtension());
318+
319+
unset($this->file);
320+
}
321+
322+
/**
323+
* @ORM\PostRemove()
324+
*/
325+
public function removeUpload()
326+
{
327+
if ($file = $this->getFullPath()) {
328+
unlink($file);
329+
}
330+
}
331+
332+
public function getFullPath()
333+
{
334+
return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->id.'.'.$this->path;
335+
}
336+
}

cookbook/form/file_uploads.rst

Lines changed: 0 additions & 5 deletions
This file was deleted.

cookbook/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Cookbook
1515
doctrine/doctrine_fixtures
1616
doctrine/mongodb
1717
doctrine/migrations
18+
doctrine/file_uploads
1819
doctrine/common_extensions
1920
doctrine/event_listeners_subscribers
2021
doctrine/reverse_engineering
@@ -24,7 +25,6 @@ Cookbook
2425

2526
form/twig_form_customization
2627
form/create_custom_field_type
27-
form/file_uploads
2828
validation/custom_constraint
2929

3030
configuration/environments

cookbook/map.rst.inc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* :doc:`/cookbook/doctrine/doctrine_fixtures`
1818
* :doc:`/cookbook/doctrine/mongodb`
1919
* :doc:`/cookbook/doctrine/migrations`
20+
* :doc:`/cookbook/doctrine/file_uploads`
2021
* :doc:`/cookbook/doctrine/common_extensions`
2122
* :doc:`/cookbook/doctrine/event_listeners_subscribers`
2223
* :doc:`/cookbook/doctrine/dbal`
@@ -28,8 +29,8 @@
2829

2930
* :doc:`/cookbook/form/twig_form_customization`
3031
* :doc:`/cookbook/form/create_custom_field_type`
31-
* :doc:`/cookbook/form/file_uploads`
3232
* :doc:`/cookbook/validation/custom_constraint`
33+
* (doctrine) :doc:`/cookbook/doctrine/file_uploads`
3334

3435
* **Configuration and the Service Container**
3536

0 commit comments

Comments
 (0)