-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove old File Upload article + improve the new one #6040
Changes from all commits
afc6312
d5aa913
7e255d6
97ba3ec
0eb57b7
55de61c
0739ad5
f6068c9
0c54b8a
94e21ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -86,16 +86,27 @@ Now, update the template that renders the form to display the new ``brochure`` | |
field (the exact template code to add depends on the method used by your application | ||
to :doc:`customize form rendering </cookbook/form/form_customization>`): | ||
|
||
.. code-block:: html+twig | ||
.. configuration-block:: | ||
|
||
{# app/Resources/views/product/new.html.twig #} | ||
<h1>Adding a new product</h1> | ||
.. code-block:: html+twig | ||
|
||
{{ form_start() }} | ||
{# ... #} | ||
{# app/Resources/views/product/new.html.twig #} | ||
<h1>Adding a new product</h1> | ||
|
||
{{ form_row(form.brochure) }} | ||
{{ form_end() }} | ||
{{ form_start(form) }} | ||
{# ... #} | ||
|
||
{{ form_row(form.brochure) }} | ||
{{ form_end(form) }} | ||
|
||
.. code-block:: html+php | ||
|
||
<!-- app/Resources/views/product/new.html.twig --> | ||
<h1>Adding a new product</h1> | ||
|
||
<?php echo $view['form']->start($form) ?> | ||
<?php echo $view['form']->row($form['brochure']) ?> | ||
<?php echo $view['form']->end($form) ?> | ||
|
||
Finally, you need to update the code of the controller that handles the form:: | ||
|
||
|
@@ -119,7 +130,7 @@ Finally, you need to update the code of the controller that handles the form:: | |
$form = $this->createForm(new ProductType(), $product); | ||
$form->handleRequest($request); | ||
|
||
if ($form->isValid()) { | ||
if ($form->isSubmitted() && $form->isValid()) { | ||
// $file stores the uploaded PDF file | ||
/** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */ | ||
$file = $product->getBrochure(); | ||
|
@@ -128,8 +139,10 @@ Finally, you need to update the code of the controller that handles the form:: | |
$fileName = md5(uniqid()).'.'.$file->guessExtension(); | ||
|
||
// Move the file to the directory where brochures are stored | ||
$brochuresDir = $this->container->getParameter('kernel.root_dir').'/../web/uploads/brochures'; | ||
$file->move($brochuresDir, $fileName); | ||
$file->move( | ||
$this->container->getParameter('brochures_directory'), | ||
$fileName | ||
); | ||
|
||
// Update the 'brochure' property to store the PDF file name | ||
// instead of its contents | ||
|
@@ -146,16 +159,27 @@ Finally, you need to update the code of the controller that handles the form:: | |
} | ||
} | ||
|
||
Now, create the ``brochures_directory`` parameter that was used in the | ||
controller to specify the directory in which the brochures should be stored: | ||
|
||
.. code-block:: yaml | ||
|
||
# app/config/config.yml | ||
|
||
# ... | ||
parameters: | ||
brochures_directory: '%kernel.root_dir%/../web/uploads/brochures' | ||
|
||
There are some important things to consider in the code of the above controller: | ||
|
||
#. When the form is uploaded, the ``brochure`` property contains the whole PDF | ||
file contents. Since this property stores just the file name, you must set | ||
its new value before persisting the changes of the entity; | ||
#. In Symfony applications, uploaded files are objects of the | ||
:class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` class, which | ||
:class:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile` class. This class | ||
provides methods for the most common operations when dealing with uploaded files; | ||
#. A well-known security best practice is to never trust the input provided by | ||
users. This also applies to the files uploaded by your visitors. The ``Uploaded`` | ||
users. This also applies to the files uploaded by your visitors. The ``UploadedFile`` | ||
class provides methods to get the original file extension | ||
(:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getExtension`), | ||
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: | |
that information. That's why it's always better to generate a unique name and | ||
use the :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::guessExtension` | ||
method to let Symfony guess the right extension according to the file MIME type; | ||
#. The ``UploadedFile`` class also provides a :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::move` | ||
method to store the file in its intended directory. Defining this directory | ||
path as an application configuration option is considered a good practice that | ||
simplifies the code: ``$this->container->getParameter('brochures_dir')``. | ||
|
||
You can now use the following code to link to the PDF brochure of an product: | ||
You can use the following code to link to the PDF brochure of a product: | ||
|
||
.. configuration-block:: | ||
|
||
.. code-block:: html+twig | ||
|
||
<a href="{{ asset('uploads/brochures/' ~ product.brochure) }}">View brochure (PDF)</a> | ||
|
||
.. code-block:: html+php | ||
|
||
<a href="<?php echo $view['assets']->getUrl('uploads/brochures/'.$product->getBrochure()) ?>"> | ||
View brochure (PDF) | ||
</a> | ||
|
||
.. tip:: | ||
|
||
When creating a form to edit an already persisted item, the file form type | ||
still expects a :class:`Symfony\\Component\\HttpFoundation\\File\\File` | ||
instance. As the persisted entity now contains only the relative file path, | ||
you first have to concatenate the configured upload path with the stored | ||
filename and create a new ``File`` class:: | ||
|
||
use Symfony\Component\HttpFoundation\File\File; | ||
// ... | ||
|
||
$product->setBrochure( | ||
new File($this->getParameter('brochures_directory').'/'.$product->getBrochure()) | ||
); | ||
|
||
Creating an Uploader Service | ||
---------------------------- | ||
|
||
To avoid logic in controllers, making them big, you can extract the upload | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if I really like this service, it's small doesn't do much and doesn't seem to make things simpler in any way. However, it felt like a nice step between a big controller and using a Doctrine listener. |
||
logic to a seperate service:: | ||
|
||
// src/AppBundle/FileUploader.php | ||
namespace AppBundle; | ||
|
||
use Symfony\Component\HttpFoundation\File\UploadedFile; | ||
|
||
class FileUploader | ||
{ | ||
private $targetDir; | ||
|
||
public function __construct($targetDir) | ||
{ | ||
$this->targetDir = $targetDir; | ||
} | ||
|
||
public function upload(UploadedFile $file) | ||
{ | ||
$fileName = md5(uniqid()).'.'.$file->guessExtension(); | ||
|
||
$file->move($this->targetDir, $fileName); | ||
|
||
return $fileName; | ||
} | ||
} | ||
|
||
Then, define a service for this class: | ||
|
||
.. configuration-block:: | ||
|
||
.. code-block:: yaml | ||
|
||
# app/config/services.yml | ||
services: | ||
# ... | ||
app.brochure_uploader: | ||
class: AppBundle\FileUploader | ||
arguments: ['%brochures_directory%'] | ||
|
||
.. code-block:: xml | ||
|
||
<!-- app/config/config.xml --> | ||
<?xml version="1.0" encoding="UTF-8" ?> | ||
<container xmlns="http://symfony.com/schema/dic/services" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://symfony.com/schema/dic/services | ||
http://symfony.com/schema/dic/services/services-1.0.xsd" | ||
> | ||
<!-- ... --> | ||
|
||
<service id="app.brochure_uploader" class="AppBundle\FileUploader"> | ||
<argument>%brochures_directory%</argument> | ||
</service> | ||
</container> | ||
|
||
.. code-block:: php | ||
|
||
// app/config/services.php | ||
use Symfony\Component\DependencyInjection\Definition; | ||
|
||
// ... | ||
$container->setDefinition('app.brochure_uploader', new Definition( | ||
'AppBundle\FileUploader', | ||
array('%brochures_directory%') | ||
)); | ||
|
||
Now you're ready to use this service in the controller:: | ||
|
||
// src/AppBundle/Controller/ProductController.php | ||
|
||
// ... | ||
public function newAction(Request $request) | ||
{ | ||
// ... | ||
|
||
if ($form->isValid()) { | ||
$file = $product->getBrochure(); | ||
$fileName = $this->get('app.brochure_uploader')->upload($file); | ||
|
||
$product->setBrochure($fileName); | ||
|
||
.. code-block:: html+twig | ||
// ... | ||
} | ||
|
||
// ... | ||
} | ||
|
||
Using a Doctrine Listener | ||
------------------------- | ||
|
||
If you are using Doctrine to store the Product entity, you can create a | ||
:doc:`Doctrine listener </cookbook/doctrine/event_listeners_subscribers>` to | ||
automatically upload the file when persisting the entity:: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wondering if we should show code + config, or just link to the other article and just mentioning this as a little tip. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like how you've handled it here: with code+config (it's really not too long). |
||
|
||
// src/AppBundle/EventListener/BrochureUploadListener.php | ||
namespace AppBundle\EventListener; | ||
|
||
use Symfony\Component\HttpFoundation\File\UploadedFile; | ||
use Doctrine\ORM\Event\LifecycleEventArgs; | ||
use Doctrine\ORM\Event\PreUpdateEventArgs; | ||
use AppBundle\Entity\Product; | ||
use AppBundle\FileUploader; | ||
|
||
class BrochureUploadListener | ||
{ | ||
private $uploader; | ||
|
||
public function __construct(FileUploader $uploader) | ||
{ | ||
$this->uploader = $uploader; | ||
} | ||
|
||
public function prePersist(LifecycleEventArgs $args) | ||
{ | ||
$entity = $args->getEntity(); | ||
|
||
$this->uploadFile($entity); | ||
} | ||
|
||
public function preUpdate(PreUpdateEventArgs $args) | ||
{ | ||
$entity = $args->getEntity(); | ||
|
||
$this->uploadFile($entity); | ||
} | ||
|
||
private function uploadFile($entity) | ||
{ | ||
// upload only works for Product entities | ||
if (!$entity instanceof Product) { | ||
return; | ||
} | ||
|
||
$file = $entity->getBrochure(); | ||
|
||
// only upload new files | ||
if (!$file instanceof UploadedFile) { | ||
return; | ||
} | ||
|
||
$fileName = $this->uploader->upload($file); | ||
$entity->setBrochure($fileName); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add the preUpdate too? That would be more useable |
||
} | ||
|
||
Now, register this class as a Doctrine listener: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a question about this. In the popular VichUploaderBundle, they warn you about uploading files in entities which haven't modified any other property. In that case, it won't work and the file will be lost. That's why you need to fake some changes (e.g. updating the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, will have to look into this. I think it might is (as doctrine doesn't discover the entity changed) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I understand the code correctly, the file name will always change when uploaded thus leading to changes in the entity which means that Doctrine is able to detect the change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created a test project and this is not a problem with this code. |
||
|
||
.. configuration-block:: | ||
|
||
.. code-block:: yaml | ||
|
||
# app/config/services.yml | ||
services: | ||
# ... | ||
app.doctrine_brochure_listener: | ||
class: AppBundle\EventListener\BrochureUploadListener | ||
arguments: ['@app.brochure_uploader'] | ||
tags: | ||
- { name: doctrine.event_listener, event: prePersist } | ||
- { name: doctrine.event_listener, event: preUpdate } | ||
|
||
.. code-block:: xml | ||
|
||
<!-- app/config/config.xml --> | ||
<?xml version="1.0" encoding="UTF-8" ?> | ||
<container xmlns="http://symfony.com/schema/dic/services" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://symfony.com/schema/dic/services | ||
http://symfony.com/schema/dic/services/services-1.0.xsd" | ||
> | ||
<!-- ... --> | ||
|
||
<service id="app.doctrine_brochure_listener" | ||
class="AppBundle\EventListener\BrochureUploaderListener" | ||
> | ||
<argument type="service" id="app.brochure_uploader"/> | ||
|
||
<tag name="doctrine.event_listener" event="prePersist"/> | ||
<tag name="doctrine.event_listener" event="preUpdate"/> | ||
</service> | ||
</container> | ||
|
||
.. code-block:: php | ||
|
||
// app/config/services.php | ||
use Symfony\Component\DependencyInjection\Reference; | ||
|
||
// ... | ||
$definition = new Definition( | ||
'AppBundle\EventListener\BrochureUploaderListener', | ||
array(new Reference('brochures_directory')) | ||
); | ||
$definition->addTag('doctrine.event_listener', array( | ||
'event' => 'prePersist', | ||
)); | ||
$definition->addTag('doctrine.event_listener', array( | ||
'event' => 'preUpdate', | ||
)); | ||
$container->setDefinition('app.doctrine_brochure_listener', $definition); | ||
|
||
This listeners is now automatically executed when persisting a new Product | ||
entity. This way, you can remove everything related to uploading from the | ||
controller. | ||
|
||
.. tip:: | ||
|
||
This listener can also create the ``File`` instance based on the path when | ||
fetching entities from the database:: | ||
|
||
// ... | ||
use Symfony\Component\HttpFoundation\File\File; | ||
|
||
// ... | ||
class BrochureUploadListener | ||
{ | ||
// ... | ||
|
||
public function postLoad(LifecycleEventArgs $args) | ||
{ | ||
$entity = $args->getEntity(); | ||
|
||
$fileName = $entity->getBrochure(); | ||
|
||
$entity->setBrochure(new File($this->targetPath.'/'.$fileName)); | ||
} | ||
} | ||
|
||
<a href="{{ asset('uploads/brochures/' ~ product.brochure) }}">View brochure (PDF)</a> | ||
After adding these lines, configure the listener to also listen for the | ||
``postLoad`` event. | ||
|
||
.. _`VichUploaderBundle`: https://github.com/dustin10/VichUploaderBundle |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes more sense in
config.yml
- it's not a value that would normally need to be changed on a machine-by-machine basis, and devs wouldn't/shouldn't need to worry about configuring it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And actually, we're dependent on it later when we make the paths to it in the Twig template.