@@ -82,16 +82,27 @@ Now, update the template that renders the form to display the new ``brochure``
8282field (the exact template code to add depends on the method used by your application
8383to :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
96107Finally, 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 UploadedFile) {
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