@@ -86,16 +86,27 @@ Now, update the template that renders the form to display the new ``brochure``
8686field (the exact template code to add depends on the method used by your application
8787to: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
100111Finally, 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