@@ -12,44 +12,45 @@ How to Upload Files
12
12
integrated with Doctrine ORM, MongoDB ODM, PHPCR ODM and Propel.
13
13
14
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::
15
+ add a PDF brochure for each product. To do so, add a new property called
16
+ `` brochureFilename `` in the ``Product `` entity::
17
17
18
18
// src/AppBundle/Entity/Product.php
19
19
namespace AppBundle\Entity;
20
20
21
21
use Doctrine\ORM\Mapping as ORM;
22
- use Symfony\Component\Validator\Constraints as Assert;
23
22
24
23
class Product
25
24
{
26
25
// ...
27
26
28
27
/**
29
28
* @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
29
*/
34
- private $brochure ;
30
+ private $brochureFilename ;
35
31
36
- public function getBrochure ()
32
+ public function getBrochureFilename ()
37
33
{
38
- return $this->brochure ;
34
+ return $this->brochureFilename ;
39
35
}
40
36
41
- public function setBrochure($brochure )
37
+ public function setBrochureFilename($brochureFilename )
42
38
{
43
- $this->brochure = $brochure ;
39
+ $this->brochureFilename = $brochureFilename ;
44
40
45
41
return $this;
46
42
}
47
43
}
48
44
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.
45
+ Note that the type of the ``brochureFilename `` column is ``string `` instead of
46
+ ``binary `` or ``blob `` because it only stores the PDF file name instead of the
47
+ file contents.
51
48
52
- Then, add a new ``brochure `` field to the form that manages the ``Product `` entity::
49
+ The next step is to add a new field to the form that manages the ``Product ``
50
+ entity. This must be a ``FileType `` field so the browsers can display the file
51
+ upload widget. The trick to make it work is to add the form field as "unmapped",
52
+ so Symfony doesn't try to get/set its value from the ``brochureFilename `` entity
53
+ property::
53
54
54
55
// src/AppBundle/Form/ProductType.php
55
56
namespace AppBundle\Form;
@@ -66,7 +67,10 @@ Then, add a new ``brochure`` field to the form that manages the ``Product`` enti
66
67
{
67
68
$builder
68
69
// ...
69
- ->add('brochure', FileType::class, ['label' => 'Brochure (PDF file)'])
70
+ ->add('brochure', FileType::class, [
71
+ 'label' => 'Brochure (PDF file)',
72
+ 'mapped' => false,
73
+ ])
70
74
// ...
71
75
;
72
76
}
@@ -103,6 +107,7 @@ Finally, you need to update the code of the controller that handles the form::
103
107
use AppBundle\Form\ProductType;
104
108
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
105
109
use Symfony\Component\HttpFoundation\File\Exception\FileException;
110
+ use Symfony\Component\HttpFoundation\File\UploadedFile;
106
111
use Symfony\Component\HttpFoundation\Request;
107
112
use Symfony\Component\Routing\Annotation\Route;
108
113
@@ -118,25 +123,26 @@ Finally, you need to update the code of the controller that handles the form::
118
123
$form->handleRequest($request);
119
124
120
125
if ($form->isSubmitted() && $form->isValid()) {
121
- // $file stores the uploaded PDF file
122
- /** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */
123
- $file = $product->getBrochure();
124
-
125
- $fileName = $this->generateUniqueFileName().'.'.$file->guessExtension();
126
+ /** @var UploadedFile $brochureFile */
127
+ $brochureFile = $form['brochure']->getData();
128
+ $originalFilename = pathinfo($brochureFile->getClientOriginalName(), PATHINFO_FILENAME);
129
+ // this is needed to safely include the file name as part of the URL
130
+ $originalFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; Lower()', $originalFilename);
131
+ $newFilename = $originalFilename.'-'.uniqid().'.'.$brochureFile->guessExtension();
126
132
127
133
// Move the file to the directory where brochures are stored
128
134
try {
129
- $file ->move(
135
+ $brochureFile ->move(
130
136
$this->getParameter('brochures_directory'),
131
- $fileName
137
+ $newFilename
132
138
);
133
139
} catch (FileException $e) {
134
140
// ... handle exception if something happens during file upload
135
141
}
136
142
137
143
// updates the 'brochure' property to store the PDF file name
138
144
// instead of its contents
139
- $product->setBrochure($fileName );
145
+ $product->setBrochureFilename($newFilename );
140
146
141
147
// ... persist the $product variable or any other work
142
148
@@ -147,16 +153,6 @@ Finally, you need to update the code of the controller that handles the form::
147
153
'form' => $form->createView(),
148
154
]);
149
155
}
150
-
151
- /**
152
- * @return string
153
- */
154
- private function generateUniqueFileName()
155
- {
156
- // md5() reduces the similarity of the file names generated by
157
- // uniqid(), which is based on timestamps
158
- return md5(uniqid());
159
- }
160
156
}
161
157
162
158
Now, create the ``brochures_directory `` parameter that was used in the
@@ -172,9 +168,6 @@ controller to specify the directory in which the brochures should be stored:
172
168
173
169
There are some important things to consider in the code of the above controller:
174
170
175
- #. When the form is uploaded, the ``brochure `` property contains the whole PDF
176
- file contents. Since this property stores just the file name, you must set
177
- its new value before persisting the changes of the entity;
178
171
#. In Symfony applications, uploaded files are objects of the
179
172
:class: `Symfony\\ Component\\ HttpFoundation\\ File\\ UploadedFile ` class. This class
180
173
provides methods for the most common operations when dealing with uploaded files;
@@ -193,7 +186,7 @@ You can use the following code to link to the PDF brochure of a product:
193
186
194
187
.. code-block :: html+twig
195
188
196
- <a href="{{ asset('uploads/brochures/' ~ product.brochure ) }}">View brochure (PDF)</a>
189
+ <a href="{{ asset('uploads/brochures/' ~ product.brochureFilename ) }}">View brochure (PDF)</a>
197
190
198
191
.. tip ::
199
192
@@ -206,8 +199,8 @@ You can use the following code to link to the PDF brochure of a product:
206
199
use Symfony\Component\HttpFoundation\File\File;
207
200
// ...
208
201
209
- $product->setBrochure (
210
- new File($this->getParameter('brochures_directory').'/'.$product->getBrochure ())
202
+ $product->setBrochureFilename (
203
+ new File($this->getParameter('brochures_directory').'/'.$product->getBrochureFilename ())
211
204
);
212
205
213
206
Creating an Uploader Service
@@ -233,7 +226,9 @@ logic to a separate service::
233
226
234
227
public function upload(UploadedFile $file)
235
228
{
236
- $fileName = md5(uniqid()).'.'.$file->guessExtension();
229
+ $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
230
+ $originalFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; Lower()', $originalFilename);
231
+ $fileName = $originalFilename.'-'.uniqid().'.'.$file->guessExtension();
237
232
238
233
try {
239
234
$file->move($this->getTargetDirectory(), $fileName);
@@ -299,10 +294,11 @@ Now you're ready to use this service in the controller::
299
294
// ...
300
295
301
296
if ($form->isSubmitted() && $form->isValid()) {
302
- $file = $product->getBrochure();
303
- $fileName = $fileUploader->upload($file);
297
+ /** @var UploadedFile $brochureFile */
298
+ $brochureFile = $form['brochure']->getData();
299
+ $brochureFileName = $fileUploader->upload($brochureFile);
304
300
305
- $product->setBrochure($fileName );
301
+ $product->setBrochureFilename($brochureFileName );
306
302
307
303
// ...
308
304
}
@@ -313,147 +309,16 @@ Now you're ready to use this service in the controller::
313
309
Using a Doctrine Listener
314
310
-------------------------
315
311
316
- If you are using Doctrine to store the Product entity, you can create a
317
- :doc: `Doctrine listener </doctrine/event_listeners_subscribers >` to
318
- automatically move the file when persisting the entity::
319
-
320
- // src/AppBundle/EventListener/BrochureUploadListener.php
321
- namespace AppBundle\EventListener;
322
-
323
- use AppBundle\Entity\Product;
324
- use AppBundle\Service\FileUploader;
325
- use Doctrine\ORM\Event\LifecycleEventArgs;
326
- use Doctrine\ORM\Event\PreUpdateEventArgs;
327
- use Symfony\Component\HttpFoundation\File\File;
328
- use Symfony\Component\HttpFoundation\File\UploadedFile;
329
-
330
- class BrochureUploadListener
331
- {
332
- private $uploader;
333
-
334
- public function __construct(FileUploader $uploader)
335
- {
336
- $this->uploader = $uploader;
337
- }
338
-
339
- public function prePersist(LifecycleEventArgs $args)
340
- {
341
- $entity = $args->getEntity();
342
-
343
- $this->uploadFile($entity);
344
- }
345
-
346
- public function preUpdate(PreUpdateEventArgs $args)
347
- {
348
- $entity = $args->getEntity();
349
-
350
- $this->uploadFile($entity);
351
- }
352
-
353
- private function uploadFile($entity)
354
- {
355
- // upload only works for Product entities
356
- if (!$entity instanceof Product) {
357
- return;
358
- }
359
-
360
- $file = $entity->getBrochure();
361
-
362
- // only upload new files
363
- if ($file instanceof UploadedFile) {
364
- $fileName = $this->uploader->upload($file);
365
- $entity->setBrochure($fileName);
366
- } elseif ($file instanceof File) {
367
- // prevents the full file path being saved on updates
368
- // as the path is set on the postLoad listener
369
- $entity->setBrochure($file->getFilename());
370
- }
371
- }
372
- }
373
-
374
- Now, register this class as a Doctrine listener:
375
-
376
- .. configuration-block ::
377
-
378
- .. code-block :: yaml
379
-
380
- # app/config/services.yml
381
- services :
382
- _defaults :
383
- # ... be sure autowiring is enabled
384
- autowire : true
385
- # ...
386
-
387
- AppBundle\EventListener\BrochureUploadListener :
388
- tags :
389
- - { name: doctrine.event_listener, event: prePersist }
390
- - { name: doctrine.event_listener, event: preUpdate }
391
-
392
- .. code-block :: xml
393
-
394
- <!-- app/config/config.xml -->
395
- <?xml version =" 1.0" encoding =" UTF-8" ?>
396
- <container xmlns =" http://symfony.com/schema/dic/services"
397
- xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
398
- xsi : schemaLocation =" http://symfony.com/schema/dic/services
399
- https://symfony.com/schema/dic/services/services-1.0.xsd" >
400
-
401
- <!-- ... be sure autowiring is enabled -->
402
- <defaults autowire =" true" />
403
- <!-- ... -->
404
-
405
- <service id =" AppBundle\EventListener\BrochureUploaderListener" >
406
- <tag name =" doctrine.event_listener" event =" prePersist" />
407
- <tag name =" doctrine.event_listener" event =" preUpdate" />
408
- </service >
409
- </container >
312
+ The previous versions of this article explained how to handle file uploads using
313
+ :doc: `Doctrine listeners </doctrine/event_listeners_subscribers >`. However, this
314
+ is no longer recommended, because Doctrine events shouldn't be used for your
315
+ domain logic.
410
316
411
- .. code-block :: php
412
-
413
- // app/config/services.php
414
- use AppBundle\EventListener\BrochureUploaderListener;
415
-
416
- $container->autowire(BrochureUploaderListener::class)
417
- ->addTag('doctrine.event_listener', [
418
- 'event' => 'prePersist',
419
- ])
420
- ->addTag('doctrine.event_listener', [
421
- 'event' => 'preUpdate',
422
- ])
423
- ;
424
-
425
- This listener is now automatically executed when persisting a new Product
426
- entity. This way, you can remove everything related to uploading from the
427
- controller.
428
-
429
- .. tip ::
430
-
431
- This listener can also create the ``File `` instance based on the path when
432
- fetching entities from the database::
433
-
434
- // ...
435
- use Symfony\Component\HttpFoundation\File\File;
436
-
437
- // ...
438
- class BrochureUploadListener
439
- {
440
- // ...
441
-
442
- public function postLoad(LifecycleEventArgs $args)
443
- {
444
- $entity = $args->getEntity();
445
-
446
- if (!$entity instanceof Product) {
447
- return;
448
- }
449
-
450
- if ($fileName = $entity->getBrochure()) {
451
- $entity->setBrochure(new File($this->uploader->getTargetDirectory().'/'.$fileName));
452
- }
453
- }
454
- }
317
+ Moreover, Doctrine listeners are often dependent on internal Doctrine behaviour
318
+ which may change in future versions. Also, they can introduce performance issues
319
+ unawarely (because your listener persists entities which cause other entities to
320
+ be changed and persisted).
455
321
456
- After adding these lines, configure the listener to also listen for the
457
- ``postLoad `` event.
322
+ As an alternative, you can use :doc: `Symfony events, listeners and subscribers </event_dispatcher >`.
458
323
459
324
.. _`VichUploaderBundle` : https://github.com/dustin10/VichUploaderBundle
0 commit comments