10000 feature #6040 Remove old File Upload article + improve the new one (W… · symfony/symfony-docs@758e083 · GitHub
[go: up one dir, main page]

Skip to content

Commit 758e083

Browse files
committed
feature #6040 Remove old File Upload article + improve the new one (WouterJ)
This PR was squashed before being merged into the 2.3 branch (closes #6040). Discussion ---------- Remove old File Upload article + improve the new one | Q | A | --- | --- | Doc fix? | yes | New docs? | yes | Applies to | 2.3+ | Fixed tickets | #5375 The old file upload article wasn't good, a new one was written by @javiereguiluz but the old one remained online. This PR removes the old one and documents the missing bits in the new article. Commits ------- 888c61c Remove old File Upload article + improve the new one
2 parents b9a3606 + 888c61c commit 758e083

File tree

8 files changed

+310
-618
lines changed

8 files changed

+310
-618
lines changed

book/forms.rst

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,18 +1953,17 @@ HTML form so that the user can modify that data. The second goal of a form is to
19531953
take the data submitted by the user and to re-apply it to the object.
19541954

19551955
There's still much more to learn about the powerful world of forms, such as
1956-
how to handle
1957-
:doc:`file uploads with Doctrine </cookbook/doctrine/file_uploads>` or how
1958-
to create a form where a dynamic number of sub-forms can be added (e.g. a
1959-
todo list where you can keep adding more fields via JavaScript before submitting).
1956+
how to handle :doc:`file uploads </cookbook/controller/upload_file>` or how to
1957+
create a form where a dynamic number of sub-forms can be added (e.g. a todo
1958+
list where you can keep adding more fields via JavaScript before submitting).
19601959
See the cookbook for these topics. Also, be sure to lean on the
19611960
:doc:`field type reference documentation </reference/forms/types>`, which
19621961
includes examples of how to use each field type and its options.
19631962

19641963
Learn more from the Cookbook
19651964
----------------------------
19661965

1967-
* :doc:`/cookbook/doctrine/file_uploads`
1966+
* :doc:`/cookbook/controller/upload_file`
19681967
* :doc:`File Field Reference </reference/forms/types/file D7AE >`
19691968
* :doc:`Creating Custom Field Types </cookbook/form/create_custom_field_type>`
19701969
* :doc:`/cookbook/form/form_customization`

cookbook/controller/upload_file.rst

Lines changed: 296 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,27 @@ Now, update the template that renders the form to display the new ``brochure``
8686
field (the exact template code to add depends on the method used by your application
8787
to :doc:`customize form rendering </cookbook/form/form_customization>`):
8888

89-
.. code-block:: html+twig
89+
.. configuration-block::
9090

91-
{# app/Resources/views/product/new.html.twig #}
92-
<h1>Adding a new product</h1>
91+
.. code-block:: html+twig
9392

94-
{{ form_start() }}
95-
{# ... #}
93+
{# app/Resources/views/product/new.html.twig #}
94+
<h1>Adding a new product</h1>
9695

97-
{{ form_row(form.brochure) }}
98-
{{ form_end() }}
96+
{{ form_start(form) }}
97+
{# ... #}
98+
99+
{{ form_row(form.brochure) }}
100+
{{ form_end(form) }}
101+
102+
.. code-block:: html+php
103+
104+
<!-- app/Resources/views/product/new.html.twig -->
105+
<h1>Adding a new product</h1>
106+
107+
<?php echo $view['form']->start($form) ?>
108+
<?php echo $view['form']->row($form['brochure']) ?>
109+
<?php echo $view['form']->end($form) ?>
99110

100111
Finally, you need to update the code of the controller that handles the form::
101112

@@ -119,7 +130,7 @@ Finally, you need to update the code of the controller that handles the form::
119130
$form = $this->createForm(new ProductType(), $product);
120131
$form->handleRequest($request);
121132

122-
if ($form->isValid()) {
133+
if ($form->isSubmitted() && $form->isValid()) {
123134
// $file stores the uploaded PDF file
124135
/** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */
125136
$file = $product->getBrochure();
@@ -128,8 +139,10 @@ Finally, you need to update the code of the controller that handles the form::
128139
$fileName = md5(uniqid()).'.'.$file->guessExtension();
129140

130141
// Move the file to the directory where brochures are stored
131-
$brochuresDir = $this->container->getParameter('kernel.root_dir').'/../web/uploads/brochures';
132-
$file->move($brochuresDir, $fileName);
142+
$file->move(
143+
$this->container->getParameter('brochures_directory'),
144+
$fileName
145+
);
133146

134147
// Update the 'brochure' property to store the PDF file name
135148
// instead of its contents
@@ -146,16 +159,27 @@ Finally, you need to update the code of the controller that handles the form::
146159
}
147160
}
148161

162+
Now, create the ``brochures_directory`` parameter that was used in the
163+
controller to specify the directory in which the brochures should be stored:
164+
165+
.. code-block:: yaml
166+
167+
# app/config/config.yml
168+
169+
# ...
170+
parameters:
171+
brochures_directory: '%kernel.root_dir%/../web/uploads/brochures'
172+
149173
There are some important things to consider in the code of the above controller:
150174

151175
#. When the form is uploaded, the ``brochure`` property contains the whole PDF
152176
file contents. Since this property stores just the file name, you must set
153177
its new value before persisting the changes of the entity;
154178
#. In Symfony applications, uploaded files are objects of the
155-
:class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` class, which
179+
:class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` class. This class
156180
provides methods for the most common operations when dealing with uploaded files;
157181
#. A well-known security best practice is to never trust the input provided by
158-
users. This also applies to the files uploaded by your visitors. The ``Uploaded``
182+
users. This also applies to the files uploaded by your visitors. The ``UploadedFile``
159183
class provides methods to get the original file extension
160184
(:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getExtension`),
161185
the original file size (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientSize`)
@@ -164,15 +188,268 @@ There are some important things to consider in the code of the above controller:
164188
that information. That's why it's always better to generate a unique name and
165189
use the :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::guessExtension`
166190
method to let Symfony guess the right extension according to the file MIME type;
167-
#. The ``UploadedFile`` class also provides a :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::move`
168-
method to store the file in its intended directory. Defining this directory
169-
path as an application configuration option is considered a good practice that
170-
simplifies the code: ``$this->container->getParameter('brochures_dir')``.
171191

172-
You can now use the following code to link to the PDF brochure of an product:
192+
You can use the following code to link to the PDF brochure of a product:
193+
194+
.. configuration-block::
195+
196+
.. code-block:: html+twig
197+
198+
<a href="{{ asset('uploads/brochures/' ~ product.brochure) }}">View brochure (PDF)</a>
199+
200+
.. code-block:: html+php
201+
202+
<a href="<?php echo $view['assets']->getUrl('uploads/brochures/'.$product->getBrochure()) ?>">
203+
View brochure (PDF)
204+
</a>
205+
206+
.. tip::
207+
208+
When creating a form to edit an already persisted item, the file form type
209+
still expects a :class:`Symfony\\Component\\HttpFoundation\\File\\File`
210+
instance. As the persisted entity now contains only the relative file path,
211+
you first have to concatenate the configured upload path with the stored
212+
filename and create a new ``File`` class::
213+
214+
use Symfony\Component\HttpFoundation\File\File;
215+
// ...
216+
217+
$product->setBrochure(
218+
new File($this->getParameter('brochures_directory').'/'.$product->getBrochure())
219+
);
220+
221+
Creating an Uploader Service
222+
----------------------------
223+
224+
To avoid logic in controllers, making them big, you can extract the upload
225+
logic to a seperate service::
226+
227+
// src/AppBundle/FileUploader.php
228+
namespace AppBundle;
229+
230+
use Symfony\Component\HttpFoundation\File\UploadedFile;
231+
232+
class FileUploader
233+
{
234+
private $targetDir;
235+
236+
public function __construct($targetDir)
237+
{
238+
$this->targetDir = $targetDir;
239+
}
240+
241+
public function upload(UploadedFile $file)
242+
{
243+
$fileName = md5(uniqid()).'.'.$file->guessExtension();
244+
245+
$file->move($this->targetDir, $fileName);
246+
247+
return $fileName;
248+
}
249+
}
250+
251+
Then, define a service for this class:
252+
253+
.. configuration-block::
254+
255+
.. code-block:: yaml
256+
257+
# app/config/services.yml
258+
services:
259+
# ...
260+
app.brochure_uploader:
261+
class: AppBundle\FileUploader
262+
arguments: ['%brochures_directory%']
263+
264+
.. code-block:: xml
265+
266+
<!-- app/config/config.xml -->
267+
<?xml version="1.0" encoding="UTF-8" ?>
268+
<container xmlns="http://symfony.com/schema/dic/services"
269+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
270+
xsi:schemaLocation="http://symfony.com/schema/dic/services
271+
http://symfony.com/schema/dic/services/services-1.0.xsd"
272+
>
273+
<!-- ... -->
274+
275+
<service id="app.brochure_uploader" class="AppBundle\FileUploader">
276+
<argument>%brochures_directory%</argument>
277+
</service>
278+
</container>
279+
280+
.. code-block:: php
281+
282+
// app/config/services.php
283+
use Symfony\Component\DependencyInjection\Definition;
284+
285+
// ...
286+
$container->setDefinition('app.brochure_uploader', new Definition(
287+
'AppBundle\FileUploader',
288+
array('%brochures_directory%')
289+
));
290+
291+
Now you're ready to use this service in the controller::
292+
293+
// src/AppBundle/Controller/ProductController.php
294+
295+
// ...
296+
public function newAction(Request $request)
297+
{
298+
// ...
299+
300+
if ($form->isValid()) {
301+
$file = $product->getBrochure();
302+
$fileName = $this->get('app.brochure_uploader')->upload($file);
303+
304+
$product->setBrochure($fileName);
173305

174-
.. code-block:: html+twig
306+
// ...
307+
}
308+
309+
// ...
310+
}
311+
312+
Using a Doctrine Listener
313+
-------------------------
314+
315+
If you are using Doctrine to store the Product entity, you can create a
316+
:doc:`Doctrine listener </cookbook/doctrine/event_listeners_subscribers>` to
317+
automatically upload the file when persisting the entity::
318+
319+
// src/AppBundle/EventListener/BrochureUploadListener.php
320+
namespace AppBundle\EventListener;
321+
322+
use Symfony\Component\HttpFoundation\File\UploadedFile;
323+
use Doctrine\ORM\Event\LifecycleEventArgs;
324+
use Doctrine\ORM\Event\PreUpdateEventArgs;
325+
use AppBundle\Entity\Product;
326+
use AppBundle\FileUploader;
327+
328+
class BrochureUploadListener
329+
{
330+
private $uploader;
331+
332+
public function __construct(FileUploader $uploader)
333+
{
334+
$this->uploader = $uploader;
335+
}
336+
337+
public function prePersist(LifecycleEventArgs $args)
338+
{
339+
$entity = $args->getEntity();
340+
341+
$this->uploadFile($entity);
342+
}
343+
344+
public function preUpdate(PreUpdateEventArgs $args)
345+
{
346+
$entity = $args->getEntity();
347+
348+
$this->uploadFile($entity);
349+
}
350+
351+
private function uploadFile($entity)
352+
{
353+
// upload only works for Product entities
354+
if (!$entity instanceof Product) {
355+
return;
356+
}
357+
358+
$file = $entity->getBrochure();
359+
360+
// only upload new files
361+
if (!$file instanceof UploadedFile) {
362+
return;
363+
}
364+
365+
$fileName = $this->uploader->upload($file);
366+
$entity->setBrochure($fileName);
367+
}
368+
}
369+
370+
Now, register this class as a Doctrine listener:
371+
372+
.. configuration-block::
373+
374+
.. code-block:: yaml
375+
376+
# app/config/services.yml
377+
services:
378+
# ...
379+
app.doctrine_brochure_listener:
380+
class: AppBundle\EventListener\BrochureUploadListener
381+
arguments: ['@app.brochure_uploader']
382+
tags:
383+
- { name: doctrine.event_listener, event: prePersist }
384+
- { name: doctrine.event_listener, event: preUpdate }
385+
386+
.. code-block:: xml
387+
388+
<!-- app/config/config.xml -->
389+
<?xml version="1.0" encoding="UTF-8" ?>
390+
<container xmlns="http://symfony.com/schema/dic/services"
391+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
392+
xsi:schemaLocation="http://symfony.com/schema/dic/services
393+
http://symfony.com/schema/dic/services/services-1.0.xsd"
394+
>
395+
<!-- ... -->
396+
397+
<service id="app.doctrine_brochure_listener"
398+
class="AppBundle\EventListener\BrochureUploaderListener"
399+
>
400+
<argument type="service" id="app.brochure_uploader"/>
401+
402+
<tag name="doctrine.event_listener" event="prePersist"/>
403+
<tag name="doctrine.event_listener" event="preUpdate"/>
404+
</service>
405+
</container>
406+
407+
.. code-block:: php
408+
409+
// app/config/services.php
410+
use Symfony\Component\DependencyInjection\Reference;
411+
412+
// ...
413+
$definition = new Definition(
414+
'AppBundle\EventListener\BrochureUploaderListener',
415+
array(new Reference('brochures_directory'))
416+
);
417+
$definition->addTag('doctrine.event_listener', array(
418+
'event' => 'prePersist',
419+
));
420+
$definition->addTag('doctrine.event_listener', array(
421+
'event' => 'preUpdate',
422+
));
423+
$container->setDefinition('app.doctrine_brochure_listener', $definition);
424+
425+
This listeners 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+
$fileName = $entity->getBrochure();
447+
448+
$entity->setBrochure(new File($this->targetPath.'/'.$fileName));
449+
}
450+
}
175451

176-
<a href="{{ asset('uploads/brochures/' ~ product.brochure) }}">View brochure (PDF)</a>
452+
After adding these lines, configure the listener to also listen for the
453+
``postLoad`` event.
177454

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

0 commit comments

Comments
 (0)
0