Skip to content
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

Added cookbook to show how to make a simple upload #4018

Closed
wants to merge 2 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 270 additions & 0 deletions cookbook/controller/upload_file.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
.. index::
single: Controller; Upload; File

How to upload files
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be "How to Upload Files" (all words should be capitialized, except from closed class words)

===================

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should immediately say that there's a great bundle called VichUploaderBundle that makes uploading a breeze if you're using Doctrine or Propel. If you want to learn about how to handle uploads manually, this post is for you.

Let's begin with the creation of an entity Product having a document property to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never use the first person in the ddocumentation. You should replace "Let's" here. E.g. "First of all, you need to create a Product entity that has a document property which will contain the description of the product."

which will contain the description of that product. We'll also indicate the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: "You'll also want to indicate [...]"

validation needed for each properties of the entity.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[...] each property [...]


So let's say we have a product with a name, a price and a document which must be
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Assume you want a product with a name, a price and a PDF document."

You get the trick now, I think. You should remove all first persons in the rest of the article :)

a PDF file::

// src/Vendor/ShopBundle/Entity/Product.php
namespace Vendor\ShopBundle\Entity;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use Acme instead of Vendor to be consistent.


use Symfony\Component\Validator\Constraints as Assert;

class Product
{
/**
* @Assert\NotBlank(message="You must indicate a name to your product.")
*/
private $name;

/**
* @Assert\NotBlank(message="You must indicate a price to your product.")
* @Assert\Type(type="float", message="Amount must be a valid number.")
*/
private $price;

/**
* @Assert\NotBlank(message="You must upload a description with a PDF file.")
* @Assert\File(mimeTypes={ "application/pdf" })
*/
private $document;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we only really need to show the document property. If we remove the others, it'll focus on what's important.


public function getName()
{
return $this->name;
}

public function setName($name)
{
$this->name = $name;

return $this;
}

public function getPrice()
{
return $this->price;
}

public function setPrice($price)
{
$this->price = $price;

return $this;
}

public function getDocument()
{
return $this->document;
}

public function setDocument($document)
{
$this->document = $document;

return $this;
}
}

We also made sure that the user will have to indicate information to each fields.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a bit unclear for me. What about using "The NonBlank constraints made sure all fields have a value."?

To know more about validation, take a look at the :doc:`validation book </book/validation>`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put this in a .. seealso:: directive

chapter.

You have now to create the ``ProductType`` with those three fields as following::

// src/Vendor/ShopBundle/Form/ProductType.php
namespace Vendor\ShopBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'text', array('label' => 'Name:'))
->add('price', 'money', array('label' => 'Price:'))
->add('document', 'file', array('label' => 'Upload description (PDF file):'))
->add('submit', 'submit', array('label' => 'Create!'))
;
}

public function getName()
{
return 'product';
}
}

Now, make it as a service so it can be used anywhere easily::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The double colon is a shortcut for:

This is some PHP code:

.. code-block:: php

    echo 'hello';

That's not what you wanted here, so you should use a single colon instead.


.. configuration-block::

.. code-block:: yaml

# src/Vendor/ShopBundle/Resources/config/services.yml
services:
vendor.form.product_type:
class: Vendor\ShopBundle\Form\ProductType
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove all spaces except one between the key and the value.

tags:
- { name: form.type }

# Import the services.yml file of your bundle in your config.yml
imports:
- { resource: "@VendorShopBundle/Resources/config/services.yml" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe people will understand this. You should put it in a seperate code block and also document XML and PHP.


.. code-block:: xml

<!-- src/Vendor/ShopBundle/Resources/config/services.xml -->
<services>
<service id="vendor.form.product_type" class="Vendor\ShopBundle\Form\ProductType">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To comply to the standards, this should be vendor_shop.form.product_type

<tag name="form.type" alias="product" />
</service>
</services>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to have a complete XML document:

<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services">
    <services>
        <!-- ... the service definition -->
    <services>
</container>


.. code-block:: php

// src/Vendor/ShopBundle/DependencyInjection/VendorShopExtension.php
use Symfony\Component\DependencyInjection\Definition;

//…
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always use 3 normal dots instead of the (correct) ellipsis.


$definition = new Definition('Vendor\ShopBundle\Form\ProductType');
$container->setDefinition('vendor.form.product_type', $definition);
$definition->addTag('form.type');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move this line before the $container->setDefinition(...); line


If you never dealt with services before, take some time to read the
:doc:`book Service </book/service_container>` chapter.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put this in a .. seealso:: directive too. And "the book Service chapter" should be "the Service Container chapter of the book."



Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should remove one empty line

We must display the form to our users. To do that, create the controller as
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say "The form has to be shown to the users. To do that [...]"

following::

// src/Vendor/ShopBundle/Controller/ProductController.php
namespace Vendor\ShopBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Vendor\ShopBundle\Entity\Product;

class ProductController extends Controller
{
/**
* @Route("/product/new", name="vendor_product_new")
* @Template()
* @Method({"GET", "POST"})
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like @Route, because it makes the whole tutorial easier to follow. But I don't like @Template - it only potentially makes things more confusing if they're not familiar with it.

public function newAction(Request $request)
{
$product = new Product();
$form = $this->createForm('product', $product);
$form->handleRequest($request);

return array('form' => $form->createView());
}
}

Create the corresponding template as following::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have to remove one of the colons so that the following lines are not treated as a PHP code block.


.. code-block:: html+jinja

{# src/Vendor/ShopBundle/Resources/views/Product/new.html.twig #}
{% form_theme form _self %}

<h1>Creation of a new Product</h1>

<form action="{{ path('vendor_product_new') }}" method="POST" {{ form_enctype(form) }}>
{{ form_widget(form) }}
</form>

{% block form_row %}
{% spaceless %}
<fieldset>
<legend>{{ form_label(form) }}</legend>
{{ form_errors(form) }}

{{ form_widget(form) }}
</fieldset>
{% endspaceless %}
{% endblock form_row %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also need to show the PHP template.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should show the form_row form theme - it doesn't add anything about uploading.


We added some sugar by adapting our form with a form theme (take a look at the
:doc:`form themes </cookbook/form/form_customization#what-are-form-themes>` to
know more about the subject).

We now have our form displayed. Let's complete our action to deal with the
upload of our document::

// src/Vendor/ShopBundle/Controller/ProductController.php

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should add // ... after this line

class ProductController extends Controller
{
/**
* @Route("/product/new", name="vendor_product_new")
* @Template()
* @Method({"GET", "POST"})
*/
public function newAction(Request $request)
{
//…
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use 3 dots here


if ($form->isValid()) {

$file = $product->getDocument()

// Compute the name of the file.
$name = md5(uniqid()).'.'.$file->guessExtension();

$file = $file->move(__DIR__.'/../../../../web/uploads', $name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use $this->container->getParameter('kernel.root_dir').'/../web/uploads.

$product->setDocument($filename);

// Perform some persistance
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// ... perform some persistance


$this->getSession()->getFlashBag()->add('notice', 'The upload has been well uploaded.');

return $this->redirect($this->generateUrl('vendor_product_new'));
}

return array('form' => $form->createView());
}
}

The :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::guessExtension()`
returns the extension of the file the user just uploaded.

Note the :method:`Symfony\\Component\\HttpFoundation\\File\\UploadedFile::move`
method allowing movement of the file

We must display the flash message in our template::

.. code-block:: html+jinja
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the indent here


{# src/Vendor/ShopBundle/Resources/views/Product/new.html.twig #}

{# … #}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use normal three dots here and below

{% for flashes in app.session.flashbag.all %}
{% for flashMessage in flashes %}
<ul>
<li>{{ flashMessage }}</li>
</ul>
{% endfor %}
{% endfor %}
{# … #}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should also include a PHP example


The file is now uploaded in the folder ``web/upload`` of your project.

.. note::

For the sake of testability and maintainability, it is recommended to put the
logic inherent to the upload in a dedicated service. You could even make the
path to the upload folder as a configuration parameter injected to your service.
That way, you make the upload feature more flexible.