@@ -82,16 +82,27 @@ Now, update the template that renders the form to display the new ``brochure``
82
82
field (the exact template code to add depends on the method used by your application
83
83
to :doc: `customize form rendering </cookbook/form/form_customization >`):
84
84
85
- .. code -block :: html+twig
85
+ .. configuration -block ::
86
86
87
- {# app/Resources/views/product/new.html.twig #}
88
- <h1>Adding a new product</h1>
87
+ .. code-block :: html+twig
89
88
90
- {{ form_start() } }
91
- {# ... #}
89
+ {# app/Resources/views/product/new.html.twig # }
90
+ <h1>Adding a new product</h1>
92
91
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) ?>
95
106
96
107
Finally, you need to update the code of the controller that handles the form::
97
108
@@ -115,7 +126,7 @@ Finally, you need to update the code of the controller that handles the form::
115
126
$form = $this->createForm(ProductType::class, $product);
116
127
$form->handleRequest($request);
117
128
118
- if ($form->isValid()) {
129
+ if ($form->isSubmitted() && $form-> isValid()) {
119
130
// $file stores the uploaded PDF file
120
131
/** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */
121
132
$file = $product->getBrochure();
@@ -124,8 +135,10 @@ Finally, you need to update the code of the controller that handles the form::
124
135
$fileName = md5(uniqid()).'.'.$file->guessExtension();
125
136
126
137
// 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
+ );
129
142
130
143
// Update the 'brochure' property to store the PDF file name
131
144
// instead of its contents
@@ -142,16 +155,27 @@ Finally, you need to update the code of the controller that handles the form::
142
155
}
143
156
}
144
157
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
+
145
169
There are some important things to consider in the code of the above controller:
146
170
147
171
#. When the form is uploaded, the ``brochure `` property contains the whole PDF
148
172
file contents. Since this property stores just the file name, you must set
149
173
its new value before persisting the changes of the entity;
150
174
#. 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
152
176
provides methods for the most common operations when dealing with uploaded files;
153
177
#. 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 ``
155
179
class provides methods to get the original file extension
156
180
(:method: `Symfony\\ Component\\ HttpFoundation\\ File\\ UploadedFile::getExtension `),
157
181
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:
160
184
that information. That's why it's always better to generate a unique name and
161
185
use the :method: `Symfony\\ Component\\ HttpFoundation\\ File\\ UploadedFile::guessExtension `
162
186
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') ``.
167
187
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);
169
301
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
+ }
171
447
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.
173
450
174
451
.. _`VichUploaderBundle` : https://github.com/dustin10/VichUploaderBundle
0 commit comments