55
votes

I am still learning Symfony2 and don't understand how to upload a file.

Don't worry, I've already checked the documentation. It's really good, but my problem isn't explained in any tutorial.

I am looking for guidance on how to upload a file with Symfony2 but with all the thing everybody needs (such as constraint of extension, rename of the file based on the id and stuff, store the path in the db, etc...)

I found good tutorials, tried to mixed them but with no success. Each time a different problem appears: file re-uploads on every submit on the form (even if the file field is empty), guessExtension impossible to used, tmp path stored in the database instead of the right path, file not moved, impossible to used the id in the rename because the id is on auto-increment and so not yet generated).

So, I'll put an 'standard' entity, let say: Photo.php

/**
 * Photo
 *
 * @ORM\Table(name="photo")
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Photo
{
    // Annotation for the id and auto increment etc
    private $id;

    /**
     * @var string
     * @Assert\File( maxSize = "3072k", mimeTypesMessage = "Please upload a valid Image")
     * @ORM\Column(name="image", type="string", length=245, nullable=false)
     */
    private $image

    private $title

    private $description

    // all the function get, set for the 4 previous variables
}

and the controller:

public function addPhotoAction()
{
    $add_photo = new Photo;
    $formBuilderPhoto = $this->createFormBuilder($add_photo);
    $formBuilderPhoto
        ->add('title','text',array('label'  => 'Title of the photo', 'required' => true))
        ->add('image','file', array('required' => true, 'data_class' => null))
        ->add('description','textarea',array('label' => 'Description of your photo', 'required' => false))
    ;

    $form_photo = $formBuilderPhoto->getForm();

    if ($request->getMethod() == 'POST') {
        $form_photo->bind($request);
        if ($form_photo->isValid()) {
            // ...
        }
    }
    return $this->render('MyBundle:frontend:photo.html.twig',
        array('form_photo' => $form_photo->createView())
    );
}

Do you know now what are the 'important' function to add to be able to upload the photo and rename it ?

How do you check the extension to see if the upload is possible?

What is your actual way of doing such a thing with Symfony2? I know there is a lot of Bundle that do all those things for you but I want to learn to do it and understand the process.

What is the 'classic' way to implement a file upload form and rename function with Symfony2?

4

4 Answers

109
votes

Do you know now what are the 'important' function to add to be able to upload the photo and rename it?

See the official documentation on how to do this. There are good working examples for a simple file upload. Also check the doctrine documentation for lifecycle callbacks.

How do you check the extension to see if the upload is possible?

There is some HTML-Form validation in each browser. See this question for the HTML accept="" attribute in input elements. Also in Symfony2 you can specify the MIME-type of an uploaded file using this annotation:

/**
 * @Assert\File(
 *     maxSize = "1024k",
 *     mimeTypes = {"application/pdf", "application/x-pdf"},
 *     mimeTypesMessage = "Please upload a valid PDF"
 * )
 */

Even though you did not want to use any bundles I'll have to recommend you the KnpDoctrineBehavioursBundle which makes file uploading way easier.


Step-by-step:

Because you read the documentation already I'll give you a step by step code-example.

First of all you need an entity. Let's call it Image:

/**
 * Class Image
 *
 * @ORM\Entity()
 * @ORM\HasLifecycleCallbacks
 */
class Image extends BaseEntity
{

Note the @ORM\HasLifecycleCallbacks annotation. It is very important and you need it later. We create all the basic fields like ID and what not. Also we need a field to store the file path in:

    /**
     * Image path
     *
     * @var string
     *
     * @ORM\Column(type="text", length=255, nullable=false)
     */
    protected $path;
    

And one for the Image itself. Here we also define the Validation for the images. In my example it has to be 5M big and of one of the defined mimeTypes. It should be self-explanatory. Otherwise the official docs help as always.

    /**
     * Image file
     *
     * @var File
     *
     * @Assert\File(
     *     maxSize = "5M",
     *     mimeTypes = {"image/jpeg", "image/gif", "image/png", "image/tiff"},
     *     maxSizeMessage = "The maxmimum allowed file size is 5MB.",
     *     mimeTypesMessage = "Only the filetypes image are allowed."
     * )
     */
    protected $file;

Add all the Getters & Setters and update your database schema with this command:

php app/console doctrine:schema:update --force

Next we need the lifecycles. They are methods in the Entity that are called on certain events. For example the @ORM\PreUpdate() annotation before a method says that this method is being called right before the entity gets updated.

/**
 * Called before saving the entity
 * 
 * @ORM\PrePersist()
 * @ORM\PreUpdate()
 */
public function preUpload()
{   
    if (null !== $this->file) {
        // do whatever you want to generate a unique name
        $filename = sha1(uniqid(mt_rand(), true));
        $this->path = $filename.'.'.$this->file->guessExtension();
    }
}

Before the entity gets stored or updated this method is called. You can use it to e.g. generate a unique file name.

/**
 * Called before entity removal
 *
 * @ORM\PreRemove()
 */
public function removeUpload()
{
    if ($file = $this->getAbsolutePath()) {
        unlink($file); 
    }
}

Called before the entity gets removed. This gives you time to delete the image from your folders or log a message if you want to.

/**
 * Called after entity persistence
 *
 * @ORM\PostPersist()
 * @ORM\PostUpdate()
 */
public function upload()
{
    // The file property can be empty if the field is not required
    if (null === $this->file) {
        return;
    }

    // Use the original file name here but you should
    // sanitize it at least to avoid any security issues

    // move takes the target directory and then the
    // target filename to move to
    $this->file->move(
        $this->getUploadRootDir(),
        $this->path
    );

    // Set the path property to the filename where you've saved the file
    //$this->path = $this->file->getClientOriginalName();

    // Clean up the file property as you won't need it anymore
    $this->file = null;
}

This is the important part where your file is actually moved to the right directory. Note that I have used some additional methods. You can all get them from the official docs.

Next thing you need is a form. The form class itself is very simple. Just make sure you set the default data_class like this:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(
        array(
            'data_class' => 'FSchubert\SiyabongaBundle\Entity\Image',
       )
    );
}

A file upload field can be created very easily in the buildForm() method:

$builder->add('file', 'file');

The methods for your Controller are a little long for just pasting them here and IMHO it's not part of answering your question. There are countless examples out there for writing a proper Controller Action for your purpose.


More things you have to keep in mind:

  • You need to give your app writing permissions to the folders you upload the files to. Although it seems obvious it can be annoying if you have multiple servers you run the application on.
  • There is an Image Constraint for your entity as well. You can find it here. But since you were talking about a file upload I used the File Constraint instead.
  • As I mentioned in the top of this post, there are many Bundles that handle all these things for you. Check them out if you want an easy life.

Edit:

  • Changed from DoctrineExtensionsBundle to DoctrineBehaviours since development on the old one stopped in favour of the DoctrineBehaviours bundle.
12
votes
6
votes

The VichUploaderBundle is also easy to use for uploading files:

https://github.com/dustin10/VichUploaderBundle

0
votes

I recommend VichUploader bundle and this code with the bundle implanted in the entity and the FormType.

composer require vich/uploader-bundle

Admission.php

/**
 * @ORM\Entity(repositoryClass=AdmissionRepository::class)
 * @Vich\Uploadable
 */
class Admission
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $cin;

    /**
     * @Vich\UploadableField(mapping="product_image", fileNameProperty="cin")
     * @var File
     */
    private $cinFile;


    public function getId(): ?int
    {
        return $this->id;
    }

    public function getCin(): ?string
    {
        return $this->cin;
    }

    public function setCin(string $cin): self
    {
        $this->cin = $cin;

        return $this;
    }
}

AdmissionType.php

   class AdmissionType extends AbstractType
      {
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('cinFile', VichFileType::class);

    }

vich_uploader.yaml

vich_uploader:
    db_driver: orm

    mappings:
        product_image:
            uri_prefix: /uploads
            upload_destination: '%kernel.project_dir%/public/uploads'
            inject_on_load: false
            delete_on_update: true
            delete_on_remove: true
            namer: vich_uploader.namer_origname