@@ -86,16 +86,27 @@ Now, update the template that renders the form to display the new ``brochure``
86
86
field (the exact template code to add depends on the method used by your application
87
87
to:doc: `customize form rendering </cookbook/form/form_customization >`):
88
88
89
- ..code -block ::html+twig
89
+ ..configuration -block ::
90
90
91
- {# app/Resources/views/product/new.html.twig #}
92
- <h1>Adding a new product</h1>
91
+ ..code-block ::html+twig
93
92
94
- {{ form_start() } }
95
- {# ... #}
93
+ {# app/Resources/views/product/new.html.twig # }
94
+ <h1>Adding a new product</h1>
96
95
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) ?>
99
110
100
111
Finally, you need to update the code of the controller that handles the form::
101
112
@@ -119,7 +130,7 @@ Finally, you need to update the code of the controller that handles the form::
119
130
$form = $this->createForm(new ProductType(), $product);
120
131
$form->handleRequest($request);
121
132
122
- if ($form->isValid()) {
133
+ if ($form->isSubmitted() && $form-> isValid()) {
123
134
// $file stores the uploaded PDF file
124
135
/** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */
125
136
$file = $product->getBrochure();
@@ -128,8 +139,10 @@ Finally, you need to update the code of the controller that handles the form::
128
139
$fileName = md5(uniqid()).'.'.$file->guessExtension();
129
140
130
141
// 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
+ );
133
146
134
147
// Update the 'brochure' property to store the PDF file name
135
148
// instead of its contents
@@ -146,16 +159,27 @@ Finally, you need to update the code of the controller that handles the form::
146
159
}
147
160
}
148
161
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
+
149
173
There are some important things to consider in the code of the above controller:
150
174
151
175
#. When the form is uploaded, the ``brochure `` property contains the whole PDF
152
176
file contents. Since this property stores just the file name, you must set
153
177
its new value before persisting the changes of the entity;
154
178
#. 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
156
180
provides methods for the most common operations when dealing with uploaded files;
157
181
#. 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 ``
159
183
class provides methods to get the original file extension
160
184
(:method: `Symfony\\ Component\\ HttpFoundation\\ File\\ UploadedFile::getExtension `),
161
185
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:
164
188
that information. That's why it's always better to generate a unique name and
165
189
use the:method: `Symfony\\ Component\\ HttpFoundation\\ File\\ UploadedFile::guessExtension `
166
190
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') ``.
171
191
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);
173
305
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
+ }
175
451
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.
177
454
178
455
.. _`VichUploaderBundle` :https://github.com/dustin10/VichUploaderBundle