@@ -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
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- 
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