@@ -12,44 +12,44 @@ How to Upload Files
1212 integrated with Doctrine ORM, MongoDB ODM, PHPCR ODM and Propel.
1313
1414Imagine 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::
1717
1818 // src/AppBundle/Entity/Product.php
1919 namespace AppBundle\Entity;
2020
2121 use Doctrine\ORM\Mapping as ORM;
22- use Symfony\Component\Validator\Constraints as Assert;
2322
2423 class Product
2524 {
2625 // ...
2726
2827 /**
2928 * @ORM\Column(type="string")
30- *
31- * @Assert\NotBlank(message="Please, upload the product brochure as a PDF file.")
32- * @Assert\File(mimeTypes={ "application/pdf" })
3329 */
34- private $brochure ;
30+ private $brochureFilename ;
3531
36- public function getBrochure ()
32+ public function getBrochureFilename ()
3733 {
38- return $this->brochure ;
34+ return $this->brochureFilename ;
3935 }
4036
41- public function setBrochure($brochure )
37+ public function setBrochureFilename($brochureFilename )
4238 {
43- $this->brochure = $brochure ;
39+ $this->brochureFilename = $brochureFilename ;
4440
4541 return $this;
4642 }
4743 }
4844
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.
5148
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::
5353
5454 // src/AppBundle/Form/ProductType.php
5555 namespace AppBundle\Form;
@@ -59,14 +59,37 @@ Then, add a new ``brochure`` field to the form that manages the ``Product`` enti
5959 use Symfony\Component\Form\Extension\Core\Type\FileType;
6060 use Symfony\Component\Form\FormBuilderInterface;
6161 use Symfony\Component\OptionsResolver\OptionsResolver;
62+ use Symfony\Component\Validator\Constraints\File;
6263
6364 class ProductType extends AbstractType
6465 {
6566 public function buildForm(FormBuilderInterface $builder, array $options)
6667 {
6768 $builder
6869 // ...
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+ ])
7093 // ...
7194 ;
7295 }
@@ -103,6 +126,7 @@ Finally, you need to update the code of the controller that handles the form::
103126 use AppBundle\Form\ProductType;
104127 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
105128 use Symfony\Component\HttpFoundation\File\Exception\FileException;
129+ use Symfony\Component\HttpFoundation\File\UploadedFile;
106130 use Symfony\Component\HttpFoundation\Request;
107131 use Symfony\Component\Routing\Annotation\Route;
108132
@@ -118,26 +142,32 @@ Finally, you need to update the code of the controller that handles the form::
118142 $form->handleRequest($request);
119143
120144 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);
135169 }
136170
137- // updates the 'brochure' property to store the PDF file name
138- // instead of its contents
139- $product->setBrochure($fileName);
140-
141171 // ... persist the $product variable or any other work
142172
143173 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::
147177 'form' => $form->createView(),
148178 ]);
149179 }
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- }
160180 }
161181
162182Now, 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:
172192
173193 There are some important things to consider in the code of the above controller:
174194
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;
178195#. In Symfony applications, uploaded files are objects of the
179196 :class: `Symfony\\ Component\\ HttpFoundation\\ File\\ UploadedFile ` class. This class
180197 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:
193210
194211.. code-block :: html+twig
195212
196- <a href="{{ asset('uploads/brochures/' ~ product.brochure ) }}">View brochure (PDF)</a>
213+ <a href="{{ asset('uploads/brochures/' ~ product.brochureFilename ) }}">View brochure (PDF)</a>
197214
198215.. tip ::
199216
@@ -206,8 +223,8 @@ You can use the following code to link to the PDF brochure of a product:
206223 use Symfony\Component\HttpFoundation\File\File;
207224 // ...
208225
209- $product->setBrochure (
210- new File($this->getParameter('brochures_directory').'/'.$product->getBrochure ())
226+ $product->setBrochureFilename (
227+ new File($this->getParameter('brochures_directory').'/'.$product->getBrochureFilename ())
211228 );
212229
213230Creating an Uploader Service
@@ -233,7 +250,9 @@ logic to a separate service::
233250
234251 public function upload(UploadedFile $file)
235252 {
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();
237256
238257 try {
239258 $file->move($this->getTargetDirectory(), $fileName);
@@ -299,10 +318,12 @@ Now you're ready to use this service in the controller::
299318 // ...
300319
301320 if ($form->isSubmitted() && $form->isValid()) {
302- $file = $product->getBrochure();
303- $fileName = $fileUploader->upload($file);
304-
305- $product->setBrochure($fileName);
321+ /** @var UploadedFile $brochureFile */
322+ $brochureFile = $form['brochure']->getData();
323+ if ($brochureFile) {
324+ $brochureFileName = $fileUploader->upload($brochureFile);
325+ $product->setBrochureFilename($brochureFileName);
326+ }
306327
307328 // ...
308329 }
@@ -313,147 +334,16 @@ Now you're ready to use this service in the controller::
313334Using a Doctrine Listener
314335-------------------------
315336
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 >
410-
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();
337+ The previous versions of this article explained how to handle file uploads using
338+ :doc: `Doctrine listeners </doctrine/event_listeners_subscribers >`. However, this
339+ is no longer recommended, because Doctrine events shouldn't be used for your
340+ domain logic.
445341
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- }
342+ Moreover, Doctrine listeners are often dependent on internal Doctrine behaviour
343+ which may change in future versions. Also, they can introduce performance issues
344+ unawarely (because your listener persists entities which cause other entities to
345+ be changed and persisted).
455346
456- After adding these lines, configure the listener to also listen for the
457- ``postLoad `` event.
347+ As an alternative, you can use :doc: `Symfony events, listeners and subscribers </event_dispatcher >`.
458348
459349.. _`VichUploaderBundle` : https://github.com/dustin10/VichUploaderBundle
0 commit comments