Skip to content

Commit 2019e6d

Browse files
committed
Reworded the fiel upload article to use unampped fields
1 parent 7b8486a commit 2019e6d

File tree

1 file changed

+48
-183
lines changed

1 file changed

+48
-183
lines changed

controller/upload_file.rst

Lines changed: 48 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -12,44 +12,45 @@ How to Upload Files
1212
integrated with Doctrine ORM, MongoDB ODM, PHPCR ODM and Propel.
1313

1414
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::
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 ``brochureFilename`` entity
53+
property::
5354

5455
// src/AppBundle/Form/ProductType.php
5556
namespace AppBundle\Form;
@@ -66,7 +67,10 @@ Then, add a new ``brochure`` field to the form that manages the ``Product`` enti
6667
{
6768
$builder
6869
// ...
69-
->add('brochure', FileType::class, ['label' => 'Brochure (PDF file)'])
70+
->add('brochure', FileType::class, [
71+
'label' => 'Brochure (PDF file)',
72+
'mapped' => false,
73+
])
7074
// ...
7175
;
7276
}
@@ -103,6 +107,7 @@ Finally, you need to update the code of the controller that handles the form::
103107
use AppBundle\Form\ProductType;
104108
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
105109
use Symfony\Component\HttpFoundation\File\Exception\FileException;
110+
use Symfony\Component\HttpFoundation\File\UploadedFile;
106111
use Symfony\Component\HttpFoundation\Request;
107112
use Symfony\Component\Routing\Annotation\Route;
108113

@@ -118,25 +123,26 @@ Finally, you need to update the code of the controller that handles the form::
118123
$form->handleRequest($request);
119124

120125
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();
126132

127133
// Move the file to the directory where brochures are stored
128134
try {
129-
$file->move(
135+
$brochureFile->move(
130136
$this->getParameter('brochures_directory'),
131-
$fileName
137+
$newFilename
132138
);
133139
} catch (FileException $e) {
134140
// ... handle exception if something happens during file upload
135141
}
136142

137143
// updates the 'brochure' property to store the PDF file name
138144
// instead of its contents
139-
$product->setBrochure($fileName);
145+
$product->setBrochureFilename($newFilename);
140146

141147
// ... persist the $product variable or any other work
142148

@@ -147,16 +153,6 @@ Finally, you need to update the code of the controller that handles the form::
147153
'form' => $form->createView(),
148154
]);
149155
}
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-
}
160156
}
161157

162158
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:
172168
173169
There are some important things to consider in the code of the above controller:
174170

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;
178171
#. In Symfony applications, uploaded files are objects of the
179172
:class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` class. This class
180173
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:
193186

194187
.. code-block:: html+twig
195188

196-
<a href="{{ asset('uploads/brochures/' ~ product.brochure) }}">View brochure (PDF)</a>
189+
<a href="{{ asset('uploads/brochures/' ~ product.brochureFilename) }}">View brochure (PDF)</a>
197190

198191
.. tip::
199192

@@ -206,8 +199,8 @@ You can use the following code to link to the PDF brochure of a product:
206199
use Symfony\Component\HttpFoundation\File\File;
207200
// ...
208201

209-
$product->setBrochure(
210-
new File($this->getParameter('brochures_directory').'/'.$product->getBrochure())
202+
$product->setBrochureFilename(
203+
new File($this->getParameter('brochures_directory').'/'.$product->getBrochureFilename())
211204
);
212205

213206
Creating an Uploader Service
@@ -233,7 +226,9 @@ logic to a separate service::
233226

234227
public function upload(UploadedFile $file)
235228
{
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();
237232

238233
try {
239234
$file->move($this->getTargetDirectory(), $fileName);
@@ -299,10 +294,11 @@ Now you're ready to use this service in the controller::
299294
// ...
300295

301296
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);
304300

305-
$product->setBrochure($fileName);
301+
$product->setBrochureFilename($brochureFileName);
306302

307303
// ...
308304
}
@@ -313,147 +309,16 @@ Now you're ready to use this service in the controller::
313309
Using a Doctrine Listener
314310
-------------------------
315311

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.
410316

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).
455321

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>`.
458323

459324
.. _`VichUploaderBundle`: https://github.com/dustin10/VichUploaderBundle

0 commit comments

Comments
 (0)