diff --git a/controller/upload_file.rst b/controller/upload_file.rst
index 84c7c5b93c9..2072c4756f7 100644
--- a/controller/upload_file.rst
+++ b/controller/upload_file.rst
@@ -12,14 +12,13 @@ How to Upload Files
integrated with Doctrine ORM, MongoDB ODM, PHPCR ODM and Propel.
Imagine that you have a ``Product`` entity in your application and you want to
-add a PDF brochure for each product. To do so, add a new property called ``brochure``
-in the ``Product`` entity::
+add a PDF brochure for each product. To do so, add a new property called
+``brochureFilename`` in the ``Product`` entity::
// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
- use Symfony\Component\Validator\Constraints as Assert;
class Product
{
@@ -27,29 +26,30 @@ in the ``Product`` entity::
/**
* @ORM\Column(type="string")
- *
- * @Assert\NotBlank(message="Please, upload the product brochure as a PDF file.")
- * @Assert\File(mimeTypes={ "application/pdf" })
*/
- private $brochure;
+ private $brochureFilename;
- public function getBrochure()
+ public function getBrochureFilename()
{
- return $this->brochure;
+ return $this->brochureFilename;
}
- public function setBrochure($brochure)
+ public function setBrochureFilename($brochureFilename)
{
- $this->brochure = $brochure;
+ $this->brochureFilename = $brochureFilename;
return $this;
}
}
-Note that the type of the ``brochure`` column is ``string`` instead of ``binary``
-or ``blob`` because it just stores the PDF file name instead of the file contents.
+Note that the type of the ``brochureFilename`` column is ``string`` instead of
+``binary`` or ``blob`` because it only stores the PDF file name instead of the
+file contents.
-Then, add a new ``brochure`` field to the form that manages the ``Product`` entity::
+The next step is to add a new field to the form that manages the ``Product``
+entity. This must be a ``FileType`` field so the browsers can display the file
+upload widget. The trick to make it work is to add the form field as "unmapped",
+so Symfony doesn't try to get/set its value from the related entity::
// src/AppBundle/Form/ProductType.php
namespace AppBundle\Form;
@@ -59,6 +59,7 @@ Then, add a new ``brochure`` field to the form that manages the ``Product`` enti
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
+ use Symfony\Component\Validator\Constraints\File;
class ProductType extends AbstractType
{
@@ -66,7 +67,29 @@ Then, add a new ``brochure`` field to the form that manages the ``Product`` enti
{
$builder
// ...
- ->add('brochure', FileType::class, ['label' => 'Brochure (PDF file)'])
+ ->add('brochure', FileType::class, [
+ 'label' => 'Brochure (PDF file)',
+
+ // unmapped means that this field is not associated to any entity property
+ 'mapped' => false,
+
+ // make it optional so you don't have to re-upload the PDF file
+ // everytime you edit the Product details
+ 'required' => false,
+
+ // unmapped fields can't define their validation using annotations
+ // in the associated entity, so you can use the PHP constraint classes
+ 'constraints' => [
+ new File([
+ 'maxSize' => '1024k',
+ 'mimeTypes' => [
+ 'application/pdf',
+ 'application/x-pdf',
+ ],
+ 'mimeTypesMessage' => 'Please upload a valid PDF document',
+ ])
+ ],
+ ])
// ...
;
}
@@ -103,6 +126,7 @@ Finally, you need to update the code of the controller that handles the form::
use AppBundle\Form\ProductType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
+ use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
@@ -118,26 +142,32 @@ Finally, you need to update the code of the controller that handles the form::
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
- // $file stores the uploaded PDF file
- /** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */
- $file = $product->getBrochure();
-
- $fileName = $this->generateUniqueFileName().'.'.$file->guessExtension();
-
- // Move the file to the directory where brochures are stored
- try {
- $file->move(
- $this->getParameter('brochures_directory'),
- $fileName
- );
- } catch (FileException $e) {
- // ... handle exception if something happens during file upload
+ /** @var UploadedFile $brochureFile */
+ $brochureFile = $form['brochure']->getData();
+
+ // this condition is needed because the 'brochure' field is not required
+ // so the PDF file must be processed only when a file is uploaded
+ if ($brochureFile) {
+ $originalFilename = pathinfo($brochureFile->getClientOriginalName(), PATHINFO_FILENAME);
+ // this is needed to safely include the file name as part of the URL
+ $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
+ $newFilename = $safeFilename.'-'.uniqid().'.'.$brochureFile->guessExtension();
+
+ // Move the file to the directory where brochures are stored
+ try {
+ $brochureFile->move(
+ $this->getParameter('brochures_directory'),
+ $newFilename
+ );
+ } catch (FileException $e) {
+ // ... handle exception if something happens during file upload
+ }
+
+ // updates the 'brochureFilename' property to store the PDF file name
+ // instead of its contents
+ $product->setBrochureFilename($newFilename);
}
- // updates the 'brochure' property to store the PDF file name
- // instead of its contents
- $product->setBrochure($fileName);
-
// ... persist the $product variable or any other work
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::
'form' => $form->createView(),
]);
}
-
- /**
- * @return string
- */
- private function generateUniqueFileName()
- {
- // md5() reduces the similarity of the file names generated by
- // uniqid(), which is based on timestamps
- return md5(uniqid());
- }
}
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:
There are some important things to consider in the code of the above controller:
-#. When the form is uploaded, the ``brochure`` property contains the whole PDF
- file contents. Since this property stores just the file name, you must set
- its new value before persisting the changes of the entity;
#. In Symfony applications, uploaded files are objects of the
:class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` class. This class
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:
.. code-block:: html+twig
- View brochure (PDF)
+ View brochure (PDF)
.. tip::
@@ -206,8 +223,8 @@ You can use the following code to link to the PDF brochure of a product:
use Symfony\Component\HttpFoundation\File\File;
// ...
- $product->setBrochure(
- new File($this->getParameter('brochures_directory').'/'.$product->getBrochure())
+ $product->setBrochureFilename(
+ new File($this->getParameter('brochures_directory').'/'.$product->getBrochureFilename())
);
Creating an Uploader Service
@@ -233,7 +250,9 @@ logic to a separate service::
public function upload(UploadedFile $file)
{
- $fileName = md5(uniqid()).'.'.$file->guessExtension();
+ $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
+ $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
+ $fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension();
try {
$file->move($this->getTargetDirectory(), $fileName);
@@ -299,10 +318,12 @@ Now you're ready to use this service in the controller::
// ...
if ($form->isSubmitted() && $form->isValid()) {
- $file = $product->getBrochure();
- $fileName = $fileUploader->upload($file);
-
- $product->setBrochure($fileName);
+ /** @var UploadedFile $brochureFile */
+ $brochureFile = $form['brochure']->getData();
+ if ($brochureFile) {
+ $brochureFileName = $fileUploader->upload($brochureFile);
+ $product->setBrochureFilename($brochureFileName);
+ }
// ...
}
@@ -313,147 +334,16 @@ Now you're ready to use this service in the controller::
Using a Doctrine Listener
-------------------------
-If you are using Doctrine to store the Product entity, you can create a
-:doc:`Doctrine listener ` to
-automatically move the file when persisting the entity::
-
- // src/AppBundle/EventListener/BrochureUploadListener.php
- namespace AppBundle\EventListener;
-
- use AppBundle\Entity\Product;
- use AppBundle\Service\FileUploader;
- use Doctrine\ORM\Event\LifecycleEventArgs;
- use Doctrine\ORM\Event\PreUpdateEventArgs;
- use Symfony\Component\HttpFoundation\File\File;
- use Symfony\Component\HttpFoundation\File\UploadedFile;
-
- class BrochureUploadListener
- {
- private $uploader;
-
- public function __construct(FileUploader $uploader)
- {
- $this->uploader = $uploader;
- }
-
- public function prePersist(LifecycleEventArgs $args)
- {
- $entity = $args->getEntity();
-
- $this->uploadFile($entity);
- }
-
- public function preUpdate(PreUpdateEventArgs $args)
- {
- $entity = $args->getEntity();
-
- $this->uploadFile($entity);
- }
-
- private function uploadFile($entity)
- {
- // upload only works for Product entities
- if (!$entity instanceof Product) {
- return;
- }
-
- $file = $entity->getBrochure();
-
- // only upload new files
- if ($file instanceof UploadedFile) {
- $fileName = $this->uploader->upload($file);
- $entity->setBrochure($fileName);
- } elseif ($file instanceof File) {
- // prevents the full file path being saved on updates
- // as the path is set on the postLoad listener
- $entity->setBrochure($file->getFilename());
- }
- }
- }
-
-Now, register this class as a Doctrine listener:
-
-.. configuration-block::
-
- .. code-block:: yaml
-
- # app/config/services.yml
- services:
- _defaults:
- # ... be sure autowiring is enabled
- autowire: true
- # ...
-
- AppBundle\EventListener\BrochureUploadListener:
- tags:
- - { name: doctrine.event_listener, event: prePersist }
- - { name: doctrine.event_listener, event: preUpdate }
-
- .. code-block:: xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- .. code-block:: php
-
- // app/config/services.php
- use AppBundle\EventListener\BrochureUploaderListener;
-
- $container->autowire(BrochureUploaderListener::class)
- ->addTag('doctrine.event_listener', [
- 'event' => 'prePersist',
- ])
- ->addTag('doctrine.event_listener', [
- 'event' => 'preUpdate',
- ])
- ;
-
-This listener is now automatically executed when persisting a new Product
-entity. This way, you can remove everything related to uploading from the
-controller.
-
-.. tip::
-
- This listener can also create the ``File`` instance based on the path when
- fetching entities from the database::
-
- // ...
- use Symfony\Component\HttpFoundation\File\File;
-
- // ...
- class BrochureUploadListener
- {
- // ...
-
- public function postLoad(LifecycleEventArgs $args)
- {
- $entity = $args->getEntity();
+The previous versions of this article explained how to handle file uploads using
+:doc:`Doctrine listeners `. However, this
+is no longer recommended, because Doctrine events shouldn't be used for your
+domain logic.
- if (!$entity instanceof Product) {
- return;
- }
-
- if ($fileName = $entity->getBrochure()) {
- $entity->setBrochure(new File($this->uploader->getTargetDirectory().'/'.$fileName));
- }
- }
- }
+Moreover, Doctrine listeners are often dependent on internal Doctrine behaviour
+which may change in future versions. Also, they can introduce performance issues
+unawarely (because your listener persists entities which cause other entities to
+be changed and persisted).
- After adding these lines, configure the listener to also listen for the
- ``postLoad`` event.
+As an alternative, you can use :doc:`Symfony events, listeners and subscribers `.
.. _`VichUploaderBundle`: https://github.com/dustin10/VichUploaderBundle