Skip to content

Commit 9479539

Browse files
committed
[forms] Updating the file upload cookbook article
1 parent f220cd1 commit 9479539

File tree

5 files changed

+111
-63
lines changed

5 files changed

+111
-63
lines changed

book/forms.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,10 +1155,10 @@ topics.
11551155
Learn more from the Cookbook
11561156
----------------------------
11571157

1158-
* :doc:`Handling File Uploads </cookbook/form/file_uploads>`
1158+
* :doc:`/cookbook/doctrine/file_uploads`
1159+
* :doc:`File Field Reference </reference/forms/types/file>`
11591160
* :doc:`Creating Custom Field Types </cookbook/form/create_custom_field_type>`
11601161
* :doc:`/cookbook/form/twig_form_customization`
1161-
* :doc:`/cookbook/doctrine/file_uploads`
11621162

11631163
.. _`Symfony2 Form Component`: https://github.com/symfony/Form
11641164
.. _`Twig Bridge`: https://github.com/symfony/symfony/tree/master/src/Symfony/Bridge/Twig

cookbook/doctrine/file_uploads.rst

Lines changed: 106 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
11
How to handle File Uploads with Doctrine
22
========================================
33

4-
Handling file uploads with Doctrine entities is no much different than
5-
handling any other upload. But if you want to integrate the file upload into
6-
the entity lifecycle (creation, update, and removal), you need to take care of
7-
a lot of details we will talk about in this cookbook entry.
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.
88

9-
First, let's create a simple Doctrine Entity to work with::
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;
1025

1126
use Doctrine\ORM\Mapping as ORM;
27+
use Symfony\Component\Validator\Constraints as Assert;
1228

1329
/**
1430
* @ORM\Entity
1531
*/
16-
class Document
32+
class Download
1733
{
1834
/**
1935
* @ORM\Id @ORM\Column(type="integer")
@@ -43,29 +59,41 @@ First, let's create a simple Doctrine Entity to work with::
4359
}
4460
}
4561

46-
A ``Document`` has a name and it is associated with a file. The ``path``
47-
property stores the relative path to the file and ``getFullPath()`` uses the
48-
``getUploadRootDir()`` to return the absolute path to the document.
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.
4966

5067
.. tip::
5168

52-
If you have not done so yet, you should probably read the
69+
If you have not done so already, you should probably read the
5370
:doc:`file</reference/forms/types/file>` type documentation first to
5471
understand how the basic upload process works.
5572

56-
To receive the uploaded file, we use a "virtual" ``file`` field::
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::
5776

58-
$form = $this->createFormBuilder($document)
59-
->add('name')
60-
->add('file')
61-
->getForm()
62-
;
77+
public function uploadAction()
78+
{
79+
// ...
6380

64-
Validation rules should be declared on this virtual ``file`` property::
81+
$form = $this->createFormBuilder($document)
82+
->add('name')
83+
->add('file')
84+
->getForm()
85+
;
6586

66-
use Symfony\Component\Validator\Constraints as Assert;
87+
// ...
88+
}
6789

68-
class Document
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
6997
{
7098
/**
7199
* @Assert\File(maxSize="6000000")
@@ -77,16 +105,23 @@ Validation rules should be declared on this virtual ``file`` property::
77105

78106
.. note::
79107

80-
As we are using the ``File`` constraint, Symfony2 will automatically guess
81-
that the field is a file upload input; that's why we have not set it
82-
explicitly during form creation.
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::
83113

84-
The following controller shows you how to manage the form::
114+
use Acme\DemoBundle\Entity\Download;
115+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
116+
// ...
85117

86-
public function uploadAction(Post $post)
118+
/**
119+
* @Template()
120+
*/
121+
public function uploadAction()
87122
{
88-
$document = new Document();
89-
$form = $this->createFormBuilder($document)
123+
$download = new Download();
124+
$form = $this->createFormBuilder($download)
90125
->add('name')
91126
->add('file')
92127
->getForm()
@@ -97,14 +132,14 @@ The following controller shows you how to manage the form::
97132
if ($form->isValid()) {
98133
$em = $this->getDoctrine()->getEntityManager();
99134

100-
$em->persist($document);
135+
$em->persist($download);
101136
$em->flush();
102137

103-
$this->redirect('...');
138+
$this->redirect($this->generateUrl('...'));
104139
}
105140
}
106141

107-
return array('post' => $post, 'form' => $form->createView());
142+
return array('form' => $form->createView());
108143
}
109144

110145
.. note::
@@ -121,12 +156,14 @@ The following controller shows you how to manage the form::
121156
<input type="submit" value="Upload Document" />
122157
</form>
123158

124-
The previous code will automatically persist document entities with their
125-
names, but it will do nothing about the file, because it is not managed by
126-
Doctrine. However, moving the file can be done just before the document is
127-
persisted to the database by calling the ``move()`` method of the
128-
:class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` instance
129-
returned for the ``file`` field when the form is submitted::
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::
130167

131168
if ($form->isValid()) {
132169
$em = $this->getDoctrine()->getEntityManager();
@@ -139,7 +176,8 @@ returned for the ``file`` field when the form is submitted::
139176
$this->redirect('...');
140177
}
141178

142-
And here is the implementation of the ``upload`` method::
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::
143181

144182
public function upload()
145183
{
@@ -149,46 +187,52 @@ And here is the implementation of the ``upload`` method::
149187
}
150188

151189
// we use the original file name here but you should
152-
// sanitize at least it to avoid any security issues
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
153193
$this->file->move($this->getUploadRootDir(), $this->file->getOriginalName());
154194

195+
// set the path property to the filename where you'ved saved the file
155196
$this->setPath($this->file->getOriginalName());
156197

157-
// clean up the file property as we won't need it anymore
198+
// clean up the file property as you won't need it anymore
158199
unset($this->file);
159200
}
160201

202+
Using Lifecycle Callbacks
203+
-------------------------
204+
161205
Even if this implementation works, it suffers from a major flaw: What if there
162-
is a problem when the entity is persisted? The file is already moved to its
163-
final location but the entity still references the previous file.
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.
164209

165-
To avoid these issues, we are going to change the implementation so that the
166-
database operation and the moving of the file becomes atomic: if there is a
167-
problem when persisting the entity or if the file cannot be moved, then
168-
nothing happens.
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.
169214

170-
To make the operation atomic, we need to do the moving of the file when
171-
Doctrine persists the entity to the database. This can be accomplished by
172-
hooking into the entity lifecycle::
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::
173218

174219
/**
175220
* @ORM\Entity
176221
* @ORM\HasLifecycleCallbacks
177222
*/
178-
class Document
223+
class Download
179224
{
180225
}
181226

182-
And here is the ``Document`` class that shows the final version with all
183-
lifecycle callbacks implemented::
227+
Next, refactor the ``Download`` class to take advantage of these callbacks::
184228

185229
use Symfony\Component\HttpFoundation\File\UploadedFile;
186230

187231
/**
188232
* @ORM\Entity
189233
* @ORM\HasLifecycleCallbacks
190234
*/
191-
class Document
235+
class Download
192236
{
193237
/**
194238
* @ORM\PrePersist()
@@ -212,7 +256,7 @@ lifecycle callbacks implemented::
212256

213257
// you must throw an exception here if the file cannot be moved
214258
// so that the entity is not persisted to the database
215-
// which the UploadedFile move() method does
259+
// which the UploadedFile move() method does automatically
216260
$this->file->move($this->getUploadRootDir(), $this->path);
217261

218262
unset($this->file);
@@ -229,9 +273,16 @@ lifecycle callbacks implemented::
229273
}
230274
}
231275

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+
232283
If you want to use the ``id`` as the name of the file, the implementation is
233-
slightly different as we need to save the extension under the ``path``
234-
property, instead of the path::
284+
slightly different as you need to save the extension under the ``path``
285+
property, instead of the actual filename::
235286

236287
use Symfony\Component\HttpFoundation\File\UploadedFile;
237288

@@ -264,6 +315,7 @@ property, instead of the path::
264315
// so that the entity is not persisted to the database
265316
// which the UploadedFile move() method does
266317
$this->file->move($this->getUploadRootDir(), $this->id.'.'.$this->file->guessExtension());
318+
267319
unset($this->file);
268320
}
269321

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)