@@ -12,44 +12,44 @@ 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/Entity/Product.php
19
19
namespace App\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 related entity::
53
53
54
54
// src/Form/ProductType.php
55
55
namespace App\Form;
@@ -59,14 +59,37 @@ Then, add a new ``brochure`` field to the form that manages the ``Product`` enti
59
59
use Symfony\Component\Form\Extension\Core\Type\FileType;
60
60
use Symfony\Component\Form\FormBuilderInterface;
61
61
use Symfony\Component\OptionsResolver\OptionsResolver;
62
+ use Symfony\Component\Validator\Constraints\File;
62
63
63
64
class ProductType extends AbstractType
64
65
{
65
66
public function buildForm(FormBuilderInterface $builder, array $options)
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
+
73
+ // unmapped means that this field is not associated to any entity property
74
+ 'mapped' => false,
75
+
76
+ // make it optional so you don't have to re-upload the PDF file
77
+ // everytime you edit the Product details
78
+ 'required' => false,
79
+
80
+ // unmapped fields can't define their validation using annotations
81
+ // in the associated entity, so you can use the PHP constraint classes
82
+ 'constraints' => [
83
+ new File([
84
+ 'maxSize' => '1024k',
85
+ 'mimeTypes' => [
86
+ 'application/pdf',
87
+ 'application/x-pdf',
88
+ ],
89
+ 'mimeTypesMessage' => 'Please upload a valid PDF document',
90
+ ])
91
+ ],
92
+ ])
70
93
// ...
71
94
;
72
95
}
@@ -103,6 +126,7 @@ Finally, you need to update the code of the controller that handles the form::
103
126
use App\Form\ProductType;
104
127
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
105
128
use Symfony\Component\HttpFoundation\File\Exception\FileException;
129
+ use Symfony\Component\HttpFoundation\File\UploadedFile;
106
130
use Symfony\Component\HttpFoundation\Request;
107
131
use Symfony\Component\Routing\Annotation\Route;
108
132
@@ -118,26 +142,32 @@ Finally, you need to update the code of the controller that handles the form::
118
142
$form->handleRequest($request);
119
143
120
144
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
-
127
- // Move the file to the directory where brochures are stored
128
- try {
129
- $file->move(
130
- $this->getParameter('brochures_directory'),
131
- $fileName
132
- );
133
- } catch (FileException $e) {
134
- // ... handle exception if something happens during file upload
145
+ /** @var UploadedFile $brochureFile */
146
+ $brochureFile = $form['brochure']->getData();
147
+
148
+ // this condition is needed because the 'brochure' field is not required
149
+ // so the PDF file must be processed only when a file is uploaded
150
+ if ($brochureFile) {
151
+ $originalFilename = pathinfo($brochureFile->getClientOriginalName(), PATHINFO_FILENAME);
152
+ // this is needed to safely include the file name as part of the URL
153
+ $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
154
+ $newFilename = $safeFilename.'-'.uniqid().'.'.$brochureFile->guessExtension();
155
+
156
+ // Move the file to the directory where brochures are stored
157
+ try {
158
+ $brochureFile->move(
159
+ $this->getParameter('brochures_directory'),
160
+ $newFilename
161
+ );
162
+ } catch (FileException $e) {
163
+ // ... handle exception if something happens during file upload
164
+ }
165
+
166
+ // updates the 'brochureFilename' property to store the PDF file name
167
+ // instead of its contents
168
+ $product->setBrochureFilename($newFilename);
135
169
}
136
170
137
- // updates the 'brochure' property to store the PDF file name
138
- // instead of its contents
139
- $product->setBrochure($fileName);
140
-
141
171
// ... persist the $product variable or any other work
142
172
143
173
return $this->redirect($this->generateUrl('app_product_list'));
@@ -147,16 +177,6 @@ Finally, you need to update the code of the controller that handles the form::
147
177
'form' => $form->createView(),
148
178
]);
149
179
}
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
180
}
161
181
162
182
Now, create the ``brochures_directory `` parameter that was used in the
@@ -172,9 +192,6 @@ controller to specify the directory in which the brochures should be stored:
172
192
173
193
There are some important things to consider in the code of the above controller:
174
194
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
195
#. In Symfony applications, uploaded files are objects of the
179
196
:class: `Symfony\\ Component\\ HttpFoundation\\ File\\ UploadedFile ` class. This class
180
197
provides methods for the most common operations when dealing with uploaded files;
@@ -193,7 +210,7 @@ You can use the following code to link to the PDF brochure of a product:
193
210
194
211
.. code-block :: html+twig
195
212
196
- <a href="{{ asset('uploads/brochures/' ~ product.brochure ) }}">View brochure (PDF)</a>
213
+ <a href="{{ asset('uploads/brochures/' ~ product.brochureFilename ) }}">View brochure (PDF)</a>
197
214
198
215
.. tip ::
199
216
@@ -206,8 +223,8 @@ You can use the following code to link to the PDF brochure of a product:
206
223
use Symfony\Component\HttpFoundation\File\File;
207
224
// ...
208
225
209
- $product->setBrochure (
210
- new File($this->getParameter('brochures_directory').'/'.$product->getBrochure ())
226
+ $product->setBrochureFilename (
227
+ new File($this->getParameter('brochures_directory').'/'.$product->getBrochureFilename ())
211
228
);
212
229
213
230
Creating an Uploader Service
@@ -233,7 +250,9 @@ logic to a separate service::
233
250
234
251
public function upload(UploadedFile $file)
235
252
{
236
- $fileName = md5(uniqid()).'.'.$file->guessExtension();
253
+ $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
254
+ $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
255
+ $fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension();
237
256
238
257
try {
239
258
$file->move($this->getTargetDirectory(), $fileName);
@@ -311,10 +330,12 @@ Now you're ready to use this service in the controller::
311
330
// ...
312
331
313
332
if ($form->isSubmitted() && $form->isValid()) {
314
- $file = $product->getBrochure();
315
- $fileName = $fileUploader->upload($file);
316
-
317
- $product->setBrochure($fileName);
333
+ /** @var UploadedFile $brochureFile */
334
+ $brochureFile = $form['brochure']->getData();
335
+ if ($brochureFile) {
336
+ $brochureFileName = $fileUploader->upload($brochureFile);
337
+ $product->setBrochureFilename($brochureFileName);
338
+ }
318
339
319
340
// ...
320
341
}
@@ -325,149 +346,16 @@ Now you're ready to use this service in the controller::
325
346
Using a Doctrine Listener
326
347
-------------------------
327
348
328
- If you are using Doctrine to store the Product entity, you can create a
329
- :doc: `Doctrine listener </doctrine/event_listeners_subscribers >` to
330
- automatically move the file when persisting the entity::
331
-
332
- // src/EventListener/BrochureUploadListener.php
333
- namespace App\EventListener;
334
-
335
- use App\Entity\Product;
336
- use App\Service\FileUploader;
337
- use Doctrine\ORM\Event\LifecycleEventArgs;
338
- use Doctrine\ORM\Event\PreUpdateEventArgs;
339
- use Symfony\Component\HttpFoundation\File\File;
340
- use Symfony\Component\HttpFoundation\File\UploadedFile;
341
-
342
- class BrochureUploadListener
343
- {
344
- private $uploader;
345
-
346
- public function __construct(FileUploader $uploader)
347
- {
348
- $this->uploader = $uploader;
349
- }
350
-
351
- public function prePersist(LifecycleEventArgs $args)
352
- {
353
- $entity = $args->getEntity();
354
-
355
- $this->uploadFile($entity);
356
- }
357
-
358
- public function preUpdate(PreUpdateEventArgs $args)
359
- {
360
- $entity = $args->getEntity();
361
-
362
- $this->uploadFile($entity);
363
- }
364
-
365
- private function uploadFile($entity)
366
- {
367
- // upload only works for Product entities
368
- if (!$entity instanceof Product) {
369
- return;
370
- }
371
-
372
- $file = $entity->getBrochure();
373
-
374
- // only upload new files
375
- if ($file instanceof UploadedFile) {
376
- $fileName = $this->uploader->upload($file);
377
- $entity->setBrochure($fileName);
378
- } elseif ($file instanceof File) {
379
- // prevents the full file path being saved on updates
380
- // as the path is set on the postLoad listener
381
- $entity->setBrochure($file->getFilename());
382
- }
383
- }
384
- }
385
-
386
- Now, register this class as a Doctrine listener:
387
-
388
- .. configuration-block ::
389
-
390
- .. code-block :: yaml
349
+ The previous versions of this article explained how to handle file uploads using
350
+ :doc: `Doctrine listeners </doctrine/event_listeners_subscribers >`. However, this
351
+ is no longer recommended, because Doctrine events shouldn't be used for your
352
+ domain logic.
391
353
392
- # config/services.yaml
393
- services :
394
- _defaults :
395
- # ... be sure autowiring is enabled
396
- autowire : true
397
- # ...
398
-
399
- App\EventListener\BrochureUploadListener :
400
- tags :
401
- - { name: doctrine.event_listener, event: prePersist }
402
- - { name: doctrine.event_listener, event: preUpdate }
403
-
404
- .. code-block :: xml
405
-
406
- <!-- config/services.xml -->
407
- <?xml version =" 1.0" encoding =" UTF-8" ?>
408
- <container xmlns =" http://symfony.com/schema/dic/services"
409
- xmlns : xsi =" http://www.w3.org/2001/XMLSchema-instance"
410
- xsi : schemaLocation =" http://symfony.com/schema/dic/services
411
- https://symfony.com/schema/dic/services/services-1.0.xsd" >
412
-
413
- <services >
414
- <!-- ... be sure autowiring is enabled -->
415
- <defaults autowire =" true" />
416
- <!-- ... -->
417
-
418
- <service id =" App\EventListener\BrochureUploaderListener" >
419
- <tag name =" doctrine.event_listener" event =" prePersist" />
420
- <tag name =" doctrine.event_listener" event =" preUpdate" />
421
- </service >
422
- </services >
423
- </container >
424
-
425
- .. code-block :: php
426
-
427
- // config/services.php
428
- use App\EventListener\BrochureUploaderListener;
429
-
430
- $container->autowire(BrochureUploaderListener::class)
431
- ->addTag('doctrine.event_listener', [
432
- 'event' => 'prePersist',
433
- ])
434
- ->addTag('doctrine.event_listener', [
435
- 'event' => 'preUpdate',
436
- ])
437
- ;
438
-
439
- This listener is now automatically executed when persisting a new Product
440
- entity. This way, you can remove everything related to uploading from the
441
- controller.
442
-
443
- .. tip ::
444
-
445
- This listener can also create the ``File `` instance based on the path when
446
- fetching entities from the database::
447
-
448
- // ...
449
- use Symfony\Component\HttpFoundation\File\File;
450
-
451
- // ...
452
- class BrochureUploadListener
453
- {
454
- // ...
455
-
456
- public function postLoad(LifecycleEventArgs $args)
457
- {
458
- $entity = $args->getEntity();
459
-
460
- if (!$entity instanceof Product) {
461
- return;
462
- }
463
-
464
- if ($fileName = $entity->getBrochure()) {
465
- $entity->setBrochure(new File($this->uploader->getTargetDirectory().'/'.$fileName));
466
- }
467
- }
468
- }
354
+ Moreover, Doctrine listeners are often dependent on internal Doctrine behaviour
355
+ which may change in future versions. Also, they can introduce performance issues
356
+ unawarely (because your listener persists entities which cause other entities to
357
+ be changed and persisted).
469
358
470
- After adding these lines, configure the listener to also listen for the
471
- ``postLoad `` event.
359
+ As an alternative, you can use :doc: `Symfony events, listeners and subscribers </event_dispatcher >`.
472
360
473
361
.. _`VichUploaderBundle` : https://github.com/dustin10/VichUploaderBundle
0 commit comments