From 20ae21b5adc0d62c34ec0a956800840b033afb1e Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Wed, 10 Jun 2015 11:02:47 +0200 Subject: [PATCH 1/2] Added a new cookbook about file uploading --- cookbook/controller/index.rst | 1 + cookbook/controller/upload_file.rst | 161 ++++++++++++++++++++++++++++ cookbook/map.rst.inc | 1 + 3 files changed, 163 insertions(+) create mode 100644 cookbook/controller/upload_file.rst diff --git a/cookbook/controller/index.rst b/cookbook/controller/index.rst index fc4041abf25..9ee8ca56a17 100644 --- a/cookbook/controller/index.rst +++ b/cookbook/controller/index.rst @@ -6,3 +6,4 @@ Controller error_pages service + upload_file diff --git a/cookbook/controller/upload_file.rst b/cookbook/controller/upload_file.rst new file mode 100644 index 00000000000..ab4f4987d5b --- /dev/null +++ b/cookbook/controller/upload_file.rst @@ -0,0 +1,161 @@ +.. index:: + single: Controller; Upload; File + +How to Upload Files +=================== + +.. note:: + + Instead of handling file uploading yourself, you may consider using the + `VichUploaderBundle`_ community bundle. This bundle provides all the common + operations (such as file renaming, saving and deleting) and it's tightly + integrated with Doctrine ORM, MongoDB ODM, PHPCR ODM and Propel. + +Imagine that you have a ``Product`` entity in your application and you want to +add a PDF brochure for each product. To do so, add a new property called ``brochure`` +in the ``Product`` entity:: + + // src/AppBundle/Entity/Product.php + namespace AppBundle\Entity; + + use Doctrine\ORM\Mapping as ORM; + use Symfony\Component\Validator\Constraints as Assert; + + class Product + { + // ... + + /** + * @ORM\Column(type="string") + * + * @Assert\NotBlank(message="Please, upload the product brochure as a PDF file.") + * @Assert\File(mimeTypes={ "application/pdf" }) + */ + private $brochure; + + public function getBrochure() + { + return $this->brochure; + } + + public function setBrochure($brochure) + { + $this->brochure = $brochure; + + return $this; + } + } + +Note that the type of the ``brochure`` column is ``string`` instead of ``binary`` +or ``blob`` because it just stores the PDF file name instead of the file contents. + +Then, add a new ``brochure`` field to the form that manages ``Product`` entities:: + + // src/AppBundle/Form/ProductType.php + namespace AppBundle\Form; + + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\FormBuilderInterface; + + class ProductType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + // ... + ->add('brochure', 'file', array('label' => 'Brochure (PDF file)')) + // ... + ; + } + + // ... + } + +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 `): + +.. code-block:: html+jinja + + {# app/Resources/views/product/new.html.twig #} +

Adding a new product

+ + {{ form_start() }} + {# ... #} + + {{ form_row(form.brochure) }} + {{ form_end() }} + +Finally, you need to update the code of the controller that handles the form:: + + // src/AppBundle/Controller/ProductController.php + namespace AppBundle\ProductController; + + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Component\HttpFoundation\Request; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; + use AppBundle\Entity\Product; + + class ProductController extends Controller + { + /** + * @Route("/product/new", name="app_product_new") + */ + public function newAction(Request $request) + { + //... + + if ($form->isValid()) { + // $file stores the uploaded PDF file + $file = $product->getBrochure() + + // Generate a unique name for the file before saving it + $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); + + // Update the 'brochure' property to store the PDF file name instead of its contents + $product->setBrochure($filename); + + // ... + + return $this->redirect($this->generateUrl('app_product_list')); + } + + return $this->render('product/new.html.twig', array( + 'form' => $form->createView() + )); + } + } + +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 + 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`` + 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::getSize()`) + and the original file name (:method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::getClientOriginalName()`). + However, they are considered *not safe* because a malicious user could tamper + 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: + +.. code-block:: html+jinja + + View brochure (PDF) + +.. _`VichUploaderBundle`: https://github.com/dustin10/VichUploaderBundle diff --git a/cookbook/map.rst.inc b/cookbook/map.rst.inc index 2c4d8eb94f6..6c37de6d5f7 100644 --- a/cookbook/map.rst.inc +++ b/cookbook/map.rst.inc @@ -50,6 +50,7 @@ * :doc:`/cookbook/controller/error_pages` * :doc:`/cookbook/controller/service` + * :doc:`/cookbook/controller/upload_file` * **Debugging** From 4a7709bd789bcf687fb1b65ca2cdbb59dc4aa4ac Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 19 Jun 2015 20:08:53 +0200 Subject: [PATCH 2/2] Fixed all the issues spotted by Ryan --- cookbook/controller/upload_file.rst | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/cookbook/controller/upload_file.rst b/cookbook/controller/upload_file.rst index ab4f4987d5b..d374e1473f0 100644 --- a/cookbook/controller/upload_file.rst +++ b/cookbook/controller/upload_file.rst @@ -49,13 +49,14 @@ in the ``Product`` entity:: Note that the type of the ``brochure`` column is ``string`` instead of ``binary`` or ``blob`` because it just stores the PDF file name instead of the file contents. -Then, add a new ``brochure`` field to the form that manages ``Product`` entities:: +Then, add a new ``brochure`` field to the form that manage the ``Product`` entity:: // src/AppBundle/Form/ProductType.php namespace AppBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; class ProductType extends AbstractType { @@ -68,7 +69,17 @@ Then, add a new ``brochure`` field to the form that manages ``Product`` entities ; } - // ... + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => 'AppBundle\Entity\Product', + )); + } + + public function getName() + { + return 'product'; + } } Now, update the template that renders the form to display the new ``brochure`` @@ -91,10 +102,11 @@ Finally, you need to update the code of the controller that handles the form:: // src/AppBundle/Controller/ProductController.php namespace AppBundle\ProductController; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use AppBundle\Entity\Product; + use AppBundle\Form\ProductType; class ProductController extends Controller { @@ -103,10 +115,13 @@ Finally, you need to update the code of the controller that handles the form:: */ public function newAction(Request $request) { - //... + $product = new Product(); + $form = $this->createForm(new ProductType(), $product); + $form->handleRequest($request); if ($form->isValid()) { // $file stores the uploaded PDF file + /** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */ $file = $product->getBrochure() // Generate a unique name for the file before saving it @@ -116,10 +131,11 @@ Finally, you need to update the code of the controller that handles the form:: $brochuresDir = $this->container->getParameter('kernel.root_dir').'/../web/uploads/brochures'; $file->move($brochuresDir, $fileName); - // Update the 'brochure' property to store the PDF file name instead of its contents + // Update the 'brochure' property to store the PDF file name + // instead of its contents $product->setBrochure($filename); - // ... + // persist the $product variable or any other work... return $this->redirect($this->generateUrl('app_product_list')); }