Skip to content

Commit 8a437aa

Browse files
committed
added a cookbook entry about uploading files and Doctrine
1 parent 02543e4 commit 8a437aa

File tree

2 files changed

+295
-3
lines changed

2 files changed

+295
-3
lines changed

book/forms.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,16 +1146,19 @@ HTML form so that the user can modify that data. The second goal of a form is to
11461146
take the data submitted by the user and to re-apply it to the object.
11471147

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

11531155
Learn more from the Cookbook
11541156
----------------------------
11551157

11561158
* :doc:`Handling File Uploads </cookbook/form/file_uploads>`
11571159
* :doc:`Creating Custom Field Types </cookbook/form/create_custom_field_type>`
11581160
* :doc:`/cookbook/form/twig_form_customization`
1161+
* :doc:`/cookbook/doctrine/file_uploads`
11591162

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

cookbook/doctrine/file_uploads.rst

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
How to handle File Uploads with Doctrine
2+
========================================
3+
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.
8+
9+
First, let's create a simple Doctrine Entity to work with::
10+
11+
use Doctrine\ORM\Mapping as ORM;
12+
13+
/**
14+
* @ORM\Entity
15+
*/
16+
class Document
17+
{
18+
/**
19+
* @ORM\Id @ORM\Column(type="integer")
20+
* @ORM\GeneratedValue(strategy="AUTO")
21+
*/
22+
public $id;
23+
24+
/**
25+
* @ORM\Column(type="string", length=255)
26+
* @Assert\NotBlank
27+
*/
28+
public $name;
29+
30+
/**
31+
* @ORM\Column(type="string", length=255, nullable=true)
32+
*/
33+
public $path;
34+
35+
public function getFullPath()
36+
{
37+
return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->path;
38+
}
39+
40+
protected function getUploadRootDir()
41+
{
42+
return '/path/to/uploaded/documents';
43+
}
44+
}
45+
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.
49+
50+
.. tip::
51+
52+
If you have not done so yet, you should probably read the
53+
:doc:`file</reference/forms/types/file>` type documentation first to
54+
understand how the basic upload process works.
55+
56+
To receive the uploaded file, we use a "virtual" ``file`` field::
57+
58+
$form = $this->createFormBuilder($document)
59+
->add('name')
60+
->add('file')
61+
->getForm()
62+
;
63+
64+
Validation rules should be declared on this virtual ``file`` property::
65+
66+
use Symfony\Component\Validator\Constraints as Assert;
67+
68+
class Document
69+
{
70+
/**
71+
* @Assert\File(maxSize="6000000")
72+
*/
73+
public $file;
74+
75+
// ...
76+
}
77+
78+
.. note::
79+
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.
83+
84+
The following controller shows you how to manage the form::
85+
86+
public function uploadAction(Post $post)
87+
{
88+
$document = new Document();
89+
$form = $this->createFormBuilder($document)
90+
->add('name')
91+
->add('file')
92+
->getForm()
93+
;
94+
95+
if ($this->getRequest()->getMethod() === 'POST') {
96+
$form->bindRequest($this->getRequest());
97+
if ($form->isValid()) {
98+
$em = $this->getDoctrine()->getEntityManager();
99+
100+
$em->persist($document);
101+
$em->flush();
102+
103+
$this->redirect('...');
104+
}
105+
}
106+
107+
return array('post' => $post, 'form' => $form->createView());
108+
}
109+
110+
.. note::
111+
112+
When writing the template, don't forget to set the ``enctype`` attribute:
113+
114+
.. code-block:: html+php
115+
116+
<h1>Upload File</h1>
117+
118+
<form action="#" method="post" {{ form_enctype(form) }}>
119+
{{ form_widget(form) }}
120+
121+
<input type="submit" value="Upload Document" />
122+
</form>
123+
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::
130+
131+
if ($form->isValid()) {
132+
$em = $this->getDoctrine()->getEntityManager();
133+
134+
$document->upload();
135+
136+
$em->persist($document);
137+
$em->flush();
138+
139+
$this->redirect('...');
140+
}
141+
142+
And here is the implementation of the ``upload`` method::
143+
144+
public function upload()
145+
{
146+
// the file property can be empty if the field is not required
147+
if (!$this->file) {
148+
return;
149+
}
150+
151+
// we use the original file name here but you should
152+
// sanitize at least it to avoid any security issues
153+
$this->file->move($this->getUploadRootDir(), $this->file->getOriginalName());
154+
155+
$this->setPath($this->file->getOriginalName());
156+
157+
// clean up the file property as we won't need it anymore
158+
unset($this->file);
159+
}
160+
161+
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.
164+
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.
169+
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::
173+
174+
/**
175+
* @ORM\Entity
176+
* @ORM\HasLifecycleCallbacks
177+
*/
178+
class Document
179+
{
180+
}
181+
182+
And here is the ``Document`` class that shows the final version with all
183+
lifecycle callbacks implemented::
184+
185+
use Symfony\Component\HttpFoundation\File\UploadedFile;
186+
187+
/**
188+
* @ORM\Entity
189+
* @ORM\HasLifecycleCallbacks
190+
*/
191+
class Document
192+
{
193+
/**
194+
* @ORM\PrePersist()
195+
*/
196+
public function preUpload()
197+
{
198+
if ($this->file) {
199+
$this->setPath($this->generatePath($this->file));
200+
}
201+
}
202+
203+
/**
204+
* @ORM\PostPersist()
205+
*/
206+
public function upload()
207+
{
208+
if (!$this->file) {
209+
return;
210+
}
211+
212+
// you must throw an exception here if the file cannot be moved
213+
// so that the entity is not persisted to the database
214+
// which the UploadedFile move() method does
215+
$this->file->move($this->getUploadRootDir(), $this->generatePath($this->file));
216+
217+
unset($this->file);
218+
}
219+
220+
/**
221+
* @ORM\PostRemove()
222+
*/
223+
public function removeUpload()
224+
{
225+
if ($file = $this->getFullPath()) {
226+
unlink($file);
227+
}
228+
}
229+
230+
protected function generatePath(UploadedFile $file)
231+
{
232+
// do whatever you want to generate a unique name
233+
return uniq().'.'.$this->file->guessExtension();
234+
}
235+
}
236+
237+
If you want to use the ``id`` as the name of the file, the implementation is
238+
slightly different as we need to save the extension under the ``path``
239+
property, instead of the path::
240+
241+
use Symfony\Component\HttpFoundation\File\UploadedFile;
242+
243+
/**
244+
* @ORM\Entity
245+
* @ORM\HasLifecycleCallbacks
246+
*/
247+
class Document
248+
{
249+
/**
250+
* @ORM\PrePersist()
251+
*/
252+
public function preUpload()
253+
{
254+
if ($this->file) {
255+
$this->setPath($this->file->guessExtension());
256+
}
257+
}
258+
259+
/**
260+
* @ORM\PostPersist()
261+
*/
262+
public function upload()
263+
{
264+
if (!$this->file) {
265+
return;
266+
}
267+
268+
// you must throw an exception here if the file cannot be moved
269+
// so that the entity is not persisted to the database
270+
// which the UploadedFile move() method does
271+
$this->file->move($this->getUploadRootDir(), $this->id.'.'.$this->file->guessExtension());
272+
unset($this->file);
273+
}
274+
275+
/**
276+
* @ORM\PostRemove()
277+
*/
278+
public function removeUpload()
279+
{
280+
if ($file = $this->getFullPath()) {
281+
unlink($file);
282+
}
283+
}
284+
285+
public function getFullPath()
286+
{
287+
return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->id.'.'.$this->path;
288+
}
289+
}

0 commit comments

Comments
 (0)