8000 Merge branch '2.7' into 2.8 · symfony/symfony-docs@b5c09b3 · GitHub
[go: up one dir, main page]

Skip to content

Commit b5c09b3

Browse files
committed
Merge branch '2.7' into 2.8
* 2.7: Remove old File Upload article + improve the new one Conflicts: book/forms.rst cookbook/doctrine/file_uploads.rst
2 parents d96ed74 + fa92eac commit b5c09b3

File tree

8 files changed

+311
-619
lines changed

8 files changed

+311
-619
lines changed

book/forms.rst

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

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

19621961
Learn more from the Cookbook
19631962
----------------------------
19641963

1965-
* :doc:`/cookbook/doctrine/file_uploads`
1966-
* :doc:`FileType Reference </reference/forms/types/file>`
1964+
* :doc:`/cookbook/controller/upload_file`
1965+
* :doc:`File Field Reference </reference/forms/types/file>`
19671966
* :doc:`Creating Custom Field Types </cookbook/form/create_custom_field_type>`
19681967
* :doc:`/cookbook/form/form_customization`
19691968
* :doc:`/cookbook/form/dynamic_form_modification`

cookbook/controller/upload_file.rst

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

85-
.. code-block:: html+twig
85+
.. configuration-block::
8686

87-
{# app/Resources/views/product/new.html.twig #}
88-
<h1>Adding a new product</h1>
87+
.. code-block:: html+twig
8988

90-
{{ form_start() }}
91-
{# ... #}
89+
{# app/Resources/views/product/new.html.twig #}
90+
<h1>Adding a new product</h1>
9291

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

96107
Finally, you need to update the code of the controller that handles the form::
97108

@@ -115,7 +126,7 @@ Finally, you need to update the code of the controller that handles the form::
115126
$form = $this->createForm(ProductType::class, $product);
116127
$form->handleRequest($request);
117128

118-
if ($form->isValid()) {
129+
if ($form->isSubmitted() && $form->isValid()) {
119130
// $file stores the uploaded PDF file
120131
/** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */
121132
$file = $product->getBrochure();
@@ -124,8 +135,10 @@ Finally, you need to update the code of the controller that handles the form::
124135
$fileName = md5(uniqid()).'.'.$file->guessExtension();
125136

126137
// Move the file to the directory where brochures are stored
127-
$brochuresDir = $this->container->getParameter('kernel.root_dir').'/../web/uploads/brochures';
128-
$file->move($brochuresDir, $fileName);
138+
$file->move(
139+
$this->container->getParameter('brochures_directory'),
140+
$fileName
141+
);
129142

130143
// Update the 'brochure' property to store the PDF file name
131144
// instead of its contents
@@ -142,16 +155,27 @@ Finally, you need to update the code of the controller that handles the form::
142155
}
143156
}
144157

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

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

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

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

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

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

0 commit comments

Comments
 (0)
0