Comment gérer les Uploads des fichiers avec Doctrine


Table des matières

Référence
Configuration de base
Utilisation du "Lifecycle Callbacks"
En utilisant l'id comme nom de fichier

Référence

Gestion des chargements de fichiers avec des entités Doctrine n'est pas différent que de traiter n'importe quel autre fichier de téléchargement. En d'autres termes, vous êtes libre de déplacer le fichier dans votre contrôleur après avoir manipulé un formulaire de soumission. Pour des exemples de comment faire cela, voir le fichier type référence

Si vous le souhaitez, vous pouvez aussi intégrer le fichier à télécharger dans votre entité (à savoir la création, mise à jour et la suppression) du cycle de vie. Dans ce cas, que votre entité est créée, mise à jour, et retiré de la doctrine, le téléchargement de fichiers et de traitement d'élimination se fera automatiquement (sans avoir besoin de faire quelque chose dans votre contrôleur);

Pour faire ce travail, vous aurez besoin de prendre soin d'un certain nombre de détails qui seront abordés

Configuration de base

Tout d'abord, créer une classe simple doctrine Entity pour travailler avec:

// src/Acme/DemoBundle/Entity/Document.php
namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity
 */
class Document
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank
     */
    public $name;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    public $path;

    public function getAbsolutePath()
    {
        return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->path;
    }

    public function getWebPath()
    {
        return null === $this->path ? null : $this->getUploadDir().'/'.$this->path;
    }

    protected function getUploadRootDir()
    {
        // the absolute directory path where uploaded documents should be saved
        return __DIR__.'/../../../../web/'.$this->getUploadDir();
    }

    protected function getUploadDir()
    {
        // get rid of the __DIR__ so it doesn't screw when displaying uploaded doc/image in the view.
        return 'uploads/documents';
    }
}

L'entité document a un nom et elle est associée à un fichier. Le chemin de propriété stocke le chemin d'accès relatif au dossier et est conservé dans la base de données. Le getAbsolutePath() est une méthode pratique qui renvoie le chemin absolu vers le fichier tandis que la getWebPath() est une méthode pratique qui renvoie le chemin web, qui peut être utilisé dans un modèle de faire un lien vers le fichier téléchargé.

Si vous n'avez pas déjà fait, vous devriez probablement lire le fichier de documentation pour comprendre les principes de base du processus de téléchargement.

Si vous utilisez les annotations pour spécifier vos règles de validation (comme indiqué dans cet exemple), assurez-vous que vous avez activé la validation par l'annotation (voir la configuration de validation ).

Pour gérer le fichier réel télécharger dans la form, utiliser un champ fichier "virtuel". Par exemple, si vous construisez votre formulaire directement dans un contrôleur, il pourrait ressembler à ceci:

public function uploadAction()
{
    // ...

    $form = $this->createFormBuilder($document)
        ->add('name')
        ->add('file')
        ->getForm()
    ;

    // ...
}

Ensuite, créez cette propriété sur votre classe de document et d'ajouter quelques règles de validation:

// src/Acme/DemoBundle/Entity/Document.php

// ...
class Document
{
    /**
     * @Assert\File(maxSize="6000000")
     */
    public $file;

    // ...
}

Comme vous utilisez la contrainte du fichier, Symfony2 automatiquement suppose que le champ de formulaire est un fichier d'entrée de téléchargement. C'est pourquoi vous n'avez pas à le définir explicitement lors de la création du formulaire ci-dessus (->add('file')).

Le contrôleur ci-dessous vous montre comment gérer l'ensemble du processus:

use Acme\DemoBundle\Entity\Document;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
// ...

/**
 * @Template()
 */
public function uploadAction()
{
    $document = new Document();
    $form = $this->createFormBuilder($document)
        ->add('name')
        ->add('file')
        ->getForm()
    ;

    if ($this->getRequest()->getMethod() === 'POST') {
        $form->bindRequest($this->getRequest());
        if ($form->isValid()) {
            $em = $this->getDoctrine()->getEntityManager();

            $em->persist($document);
            $em->flush();

            $this->redirect($this->generateUrl('...'));
        }
    }

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

Le contrôleur précédente sera automatiquement persistent l'entité document avec le nom soumis, mais il ne fera rien sur le fichier et le chemin de propriété sera vide.

Un moyen facile de gérer le fichier upload est de le déplacer juste avant l'entité est conservé et puis définissez la propriété chemin en conséquence. Commencez par appeler une nouvelle méthode upload () sur la classe Document, que vous allez créer dans un moment de traiter le fichier de téléchargement:

<h1>Upload File</h1>

<form action="#" method="post" {{ form_enctype(form) }}>
    {{ form_widget(form) }}

    <input type="submit" value="Upload Document" />
</form>

La méthode upload() sera de profiter de l'objet UploadedFile, qui est ce qui revient après qu'un champ fichier est soumis:

public function upload()
{
    // the file property can be empty if the field is not required
    if (null === $this->file) {
        return;
    }

    // we 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->file->getClientOriginalName());

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

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

Utilisation du "Lifecycle Callbacks"

Même si cette mise en œuvre fonctionne, elle souffre d'un défaut majeur: Que faire s'il y a un problème lorsque l'entité est persistente? Le dossier aurait déjà déménagé à son emplacement final, même si la propriété chemin de l'entité n'a pas persisté correctement.

Pour éviter ces problèmes, vous devriez changer la mise en œuvre de telle sorte que l'implémentation en database et le déplacement du fichier devienne atomique: s'il y a un problème persistant de l'entité ou si le fichier ne peut pas être déplacé, alors rien ne devrait se produire.

Pour ce faire, vous devez déplacer le fichier de droite comme doctrine persiste à l'entité de la base de données. Ceci peut être accompli par accrochage de l'entité dans un "Lifecycle Callbacks"

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Document
{
}

Ensuite, un refactoring de la classe Document pour profiter de ces rappels:

use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Document
{
    /**
     * @ORM\PrePersist()
     * @ORM\PreUpdate()
     */
    public function preUpload()
    {
        if (null !== $this->file) {
            // do whatever you want to generate a unique name
            $this->path = uniqid().'.'.$this->file->guessExtension();
        }
    }

    /**
     * @ORM\PostPersist()
     * @ORM\PostUpdate()
     */
    public function upload()
    {
        if (null === $this->file) {
            return;
        }

        // if there is an error when moving the file, an exception will
        // be automatically thrown by move(). This will properly prevent
        // the entity from being persisted to the database on error
        $this->file->move($this->getUploadRootDir(), $this->path);

        unset($this->file);
    }

    /**
     * @ORM\PostRemove()
     */
    public function removeUpload()
    {
        if ($file = $this->getAbsolutePath()) {
            unlink($file);
        }
    }
}

La classe fait maintenant tout ce dont vous avez besoin: elle génère un nom de fichier unique avant persistant, déplace le fichier après la persistance, et supprime le fichier si l'entité est déjà supprimé.

Le @ORM\PrePersist() et @ORM\PostPersist() rappels d'événements sont déclenchés avant et après que l'entité est conservé dans la base de données. D'autre part, l'ORM@\PreUpdate() et @ORM\PostUpdate() rappels d'événements sont appelés lorsque l'entité est mis à jour.

Le PreUpdate et les rappels ne sont PostUpdate déclenché s'il ya un changement dans l'un des champs de l'entité qui sont conservées. Cela signifie que, par défaut, si vous modifiez uniquement la propriété du fichier $, ces événements ne sera pas déclenchée, comme la propriété elle-même n'est pas directement persisté via Doctrine. Une solution serait d'utiliser un champ mis à jour qui a persisté à la doctrine, et de le modifier manuellement lorsque vous modifiez le fichier.

En utilisant l'id comme nom de fichier

Si vous voulez utiliser l'identifiant comme le nom de fichier, la mise en œuvre est légèrement différente car vous avez besoin pour sauver l'extension en vertu de la propriété path, au lieu du nom de fichier:

use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Document
{
    /**
     * @ORM\PrePersist()
     * @ORM\PreUpdate()
     */
    public function preUpload()
    {
        if (null !== $this->file) {
            $this->path = $this->file->guessExtension();
        }
    }

    /**
     * @ORM\PostPersist()
     * @ORM\PostUpdate()
     */
    public function upload()
    {
        if (null === $this->file) {
            return;
        }

        // you must throw an exception here if the file cannot be moved
        // so that the entity is not persisted to the database
        // which the UploadedFile move() method does
        $this->file->move($this->getUploadRootDir(), $this->id.'.'.$this->file->guessExtension());

        unset($this->file);
    }

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

    public function getAbsolutePath()
    {
        return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->id.'.'.$this->path;
    }
}