Tutoriel Vidéo Symfony Annotation et Upload de fichiers

Télécharger la vidéo Télécharger les sources

Dans ce tutoriel je vous propose d'explorer l'utilisation des annotations à travers la création d'un système d'upload de fichiers "facile à configurer". Le principe est d'être capable de "marquer" certains champs de nos entités comme "Uploadable".

Créer une annotation

Pour créer une annotation il suffit de créer une simple classe. On précise que notre classe est une annotation gràce à @Annotation et on précise la cible gràce à @Target.

<?php
namespace Grafikart\UploadBundle\Annotation;

use Doctrine\Common\Annotations\Annotation\Target;

/**
 * @Annotation
 * @Target("PROPERTY")
 */
class UploadableField {

    private $filename;
    private $path;

    public function __construct(array $options)
    {
        if (empty($options['filename'])) {
            throw new \InvalidArgumentException("L'annotation UplodableField doit avoir un attribut 'filename'");
        }

        if (empty($options['path'])) {
            throw new \InvalidArgumentException("L'annotation UplodableField doit avoir un attribut 'path'");
        }

        $this->filename = $options['filename'];
        $this->path = $options['path'];
    }

    public function getFilename()
    {
        return $this->filename;
    }

    public function getPath()
    {
        return $this->path;
    }

}

Cette classe permettra donc de "décorer" nos propriétés :

<?php

use Grafikart\UploadBundle\Annotation\UploadableField;  

class Post {

    // ...

    /**
     * @UploadableField(filename="filename", path="uploads")
     * @Assert\Image(maxWidth="2000", maxHeight="2000")
     */
    private $file;

    // ....
}

Lire nos annotations

Il faut ensuite être capable de lire ces annotations afin de les utiliser pour effectuer un traitement spécifique. Afin de séparer notre code on va créer une classe dédiée à la lecture des Annotations.

On injectera une instance de Doctrine\Common\Annotations\AnnotationReader qui permettra, comme son nom l'indique, de lire les annotations.

<?php
namespace Grafikart\UploadBundle\Annotation;

use Doctrine\Common\Annotations\AnnotationReader;

class UploadAnnotationReader {

    /**
     * @var AnnotationReader
     */
    private $reader;

    public function __construct(AnnotationReader $reader)
    {
        $this->reader = $reader;
    }

    /**
     * Liste les champs uploadable d'une entité (sous forme de tableau associatif)
     */
    public function getUploadableFields($entity): array {
        $reflection = new \ReflectionClass(get_class($entity));
        $properties = [];
        foreach($reflection->getProperties() as $property) {
            $annotation = $this->reader->getPropertyAnnotation($property, UploadableField::class);
            if ($annotation !== null) {
                $properties[$property->getName()] = $annotation;
            }
        }
        return $properties;
    }

}

Utiliser les annotations

Maintenant on peut "interpréter" les annotations et appliquer un comportement spécifique en utilisant un EventSubscriber pour détecter la persistence d'une entité en base de données.

<?php
namespace Grafikart\UploadBundle\Listener;

use Doctrine\Common\EventArgs;
use Doctrine\Common\EventSubscriber;
use Grafikart\UploadBundle\Annotation\UploadAnnotationReader;
use Grafikart\UploadBundle\Handler\UploadHandler;

class UploadSubscriber implements EventSubscriber {

    /**
     * @var UploadAnnotationReader
     */
    private $reader;

    /**
     * @var UploadHandler
     */
    private $handler;

    public function __construct(UploadAnnotationReader $reader, UploadHandler $handler)
    {
        $this->reader = $reader;
        $this->handler = $handler;
    }

    public function getSubscribedEvents()
    {
        return [
            'prePersist',
            'preUpdate',
            'postLoad',
            'postRemove'
        ];
    }

    public function prePersist(EventArgs $event) {
        $this->preEvent($event);
    }

    public function preUpdate(EventArgs $event) {
        $this->preEvent($event);
    }

    private function preEvent(EventArgs $event) {
        $entity = $event->getEntity();
        foreach ($this->reader->getUploadableFields($entity) as $property => $annotation) {
            $this->handler->uploadFile($entity, $property, $annotation);
        }
    }

    public function postLoad(EventArgs $event) {
        $entity = $event->getEntity();
        foreach ($this->reader->getUploadableFields($entity) as $property => $annotation) {
            $this->handler->setFileFromFilename($entity, $property, $annotation);
        }
    }

    public function postRemove(EventArgs $event) {
        $entity = $event->getEntity();
        foreach ($this->reader->getUploadableFields($entity) as $property => $annotation) {
            $this->handler->removeFile($entity, $property);
        }
    }
}

Afin de ne pas rendre le code de ce subscriber trop conséquent on va séparer la partie logique (upload / suppression de fichier) dans sa classe dédiée UploadHandler

<?php
namespace Grafikart\UploadBundle\Handler;

use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\PropertyAccess\PropertyAccess;

class UploadHandler {

    private $accessor;

    public function __construct() {
        $this->accessor = PropertyAccess::createPropertyAccessor();
    }

    public function uploadFile($entity, $property, $annotation) {
        $file = $this->accessor->getValue($entity, $property);
        if ($file instanceof UploadedFile) {
            $this->removeOldFile($entity, $annotation);
            $filename = $file->getClientOriginalName();
            $file->move($annotation->getPath(), $filename);
            $this->accessor->setValue($entity, $annotation->getFilename(), $filename);
        }
    }

    public function setFileFromFilename($entity, $property, $annotation)
    {
        $file = $this->getFileFromFilename($entity, $annotation);
        $this->accessor->setValue($entity, $property, $file);
    }

    public function removeOldFile($entity, $annotation)
    {
        $file = $this->getFileFromFilename($entity, $annotation);
        if ($file !== null) {
            @unlink($file->getRealPath());
        }
    }

    public function removeFile($entity, $property)
    {
        $file = $this->accessor->getValue($entity, $property);
        if ($file instanceof File) {
            @unlink($file->getRealPath());
        }
    }

    private function getFileFromFilename ($entity, $annotation) {
        $filename = $this->accessor->getValue($entity, $annotation->getFilename());
        if (empty($filename)) {
            return null;
        } else {
            return new File($annotation->getPath() . DIRECTORY_SEPARATOR . $filename, false);
        }
    }

}

Conclusion

Comme vous le voyez les annotations permettent de greffer un comportement "avancé" sur nos entités très facilement. Si vous cherchez une librairie testée et plus complète pour gérer l'upload de fichiers vous pouvez utiliser la librairie VichUploaderBundle qui utilise le même principe que celui que l'on vient de voir. Si vous voulez redimensionner vos images après coup vous pouvez aussi utiliser LiipImagineBundle.