Est-il possible d'éviter l'écueil de la configuration à rallonge de VichUploaderBundle ?
Nous pensons que oui.
Présentation du bundle
VichUploaderBundle est devenu un incontournable de l'écosystème Symfony. Il propose une mécanique efficace pour gérer l'upload de fichiers.
L'objectif n'est pas de revenir en détail sur comment configurer ce bundle (la documentation est à priori suffisante), mais de proposer un retour d'expérience sur sa mise en place et sur de "bonnes pratiques" qui découlent de l'expérience.
Cas d'usage
Prenons un cas relativement simple. 2 entités avec chacune une (ou plusieurs) propriété(s) permettant l'upload de fichier :
L'entité User gérant un avatar :
<?php use Doctrine\ORM\Mapping as ORM; use Vich\UploaderBundle\Mapping\Annotation as Vich; class User { /** * @var string|null * * @ORM\Column(type="text", nullable=true) */ private $avatar; /** * @Vich\UploadableField(mapping="...", fileNameProperty="avatar") */ private $avatarFile; }
L'entité Product gérant une picture et des instructions :
<?php use Doctrine\ORM\Mapping as ORM; use Vich\UploaderBundle\Mapping\Annotation as Vich; class Product { /** * @var string|null * * @ORM\Column(type="text", nullable=true) */ private $picture; /** * @Vich\UploadableField(mapping="...", fileNameProperty="picture") */ private $pictureFile; /** * @var string|null * * @ORM\Column(type="text", nullable=true) */ private $instructions; /** * @Vich\UploadableField(mapping="...", fileNameProperty="instructions") */ private $instructionsFile; }
Problématique
En suivant "bêtement" la documentation du bundle, on aboutit rapidement à ce genre de configuration :
vich_uploader: mappings: user_avatar: uri_prefix: /uploads/user/avatar upload_destination: '%kernel.root_dir%/../web/uploads/user/avatar' product_picture: uri_prefix: /uploads/product/picture upload_destination: '%kernel.root_dir%/../web/uploads/product/picture' product_instructions: uri_prefix: /uploads/user/instructions upload_destination: '%kernel.root_dir%/../web/uploads/product/instructions'
Ce n'est pas "mal" à proprement parler, mais cela nuit à la lisibilité de la configuration et engendre beaucoup trop de duplication.
D'autant que, pour rappel, le mapping doit également être indiqué dans la configuration de votre entité :
Dans l'entité User, on utilise user_avatar sur la propriété avatarFile :
<?php class User { /** * @Vich\UploadableField(mapping="user_avatar", ...) */ private $avatarFile; }
Dans l'entité Product, on utilise product_picture sur la propriété pictureFile
et product_instructions sur instructionsFile :
<?php class Product { /** * @Vich\UploadableField(mapping="product_picture", ...) */ private $pictureFile; /** * @Vich\UploadableField(mapping="product_instructions", ...) */ private $instructionsFile; }
Vous avez compris le principe, c'est pénible...
Mapping par convention
En bon développeur, on repère rapidement un schéma entre tous ces mappings : uploads/{entity type}/{property name}.
Et qui dit schéma, dit automatisation possible (après tout, c'est le cœur même de notre métier).
File & Directory Namers
VichUploaderBundle possède 2 composants pour vous donner la main sur le nommage et l'emplacement du fichier :
- FileNamer qui permet d'agir sur le nom du fichier. Le bundle est livré avec.
- DirectoryNamer qui permet d'agir sur le nom du sous-dossier dans lequel le fichier sera déposé.
note le bundle est d'ailleurs livré avec plusieurs namers : voir la documentation
En écrivant nous-mêmes 2 classes, on peut facilement prendre la main sur le stockage de ce fichier et factoriser notre configuration.
DirectoryNamer par convention
<?php namespace AppBundle\Upload\Namer; use Doctrine\Common\Persistence\ManagerRegistry; use Vich\UploaderBundle\Mapping\PropertyMapping; use Vich\UploaderBundle\Naming\DirectoryNamerInterface; use Vich\UploaderBundle\Util\Transliterator; class ConventionedDirectoryNamer implements DirectoryNamerInterface { /** * @var ManagerRegistry */ private $doctrine; /** * @param ManagerRegistry $doctrine */ public function __construct(ManagerRegistry $doctrine) { $this->doctrine = $doctrine; } public function directoryName($object, PropertyMapping $mapping) { return sprintf('%s/%s', $this->getShortClassName($object), $this->getIdentifier($object)); } /** * Get short class name of given object : * - AppBundle\Entity\Product : product * - AppBundle\Entity\User : user * * @param object $object * * @return string */ private function getShortClassName($object) { $fqcn = get_class($object); $classParts = explode('\\', $fqcn); return Transliterator::transliterate(array_pop($classParts)); } /** * Get identifier given object. * Use Doctrine metadata as a generic method. * * @param object $object * * @return string */ private function getIdentifier($object) { $fqcn = get_class($object); $identifiers = $this->doctrine->getManagerForClass($fqcn) ->getClassMetadata($fqcn) ->getIdentifierValues($object); return Transliterator::transliterate(reset($identifiers)); } }
Il va générer ce genre de chemin :
user/{id},product/{id}.
warning VichUploaderBundle interroge les namers lors des événements
prePersist&preUpdatede Doctrine. Si vos IDs sont gérés via unAUTO_INCREMENT(ou uneSEQUENCE) vos entités n'auront alors pas encore d'ID, l'identifiant ne peut alors pas faire partie du chemin (ou du nom) du fichier. Vous pouvez utiliser des UUID pour contourner ce problème : ramsey/uuid-doctrine
FileNamer par convention
<?php namespace AppBundle\Upload\Namer; use Vich\UploaderBundle\Mapping\PropertyMapping; use Vich\UploaderBundle\Naming\NamerInterface; use Vich\UploaderBundle\Naming\Polyfill\FileExtensionTrait; use Vich\UploaderBundle\Util\Transliterator; class ConventionedFileNamer implements NamerInterface { use FileExtensionTrait; public function name($object, PropertyMapping $mapping) { $file = $mapping->getFile($object); $name = Transliterator::transliterate($mapping->getFileNamePropertyName()); // append the file extension if there is one if ($extension = $this->getExtension($file)) { $name = sprintf('%s.%s', $name, $extension); } return uniqid() . '_' . $name; } }
Il va générer ce genre de nom de fichier :
{uniqid}_avatar.png,{uniqid}_picture.jpeg,{uniqid}_instructions.pdf.
Utilisation des namers dans la configuration
Dans la configuration, on n'a plus besoin de conserver qu'un seul mapping (default) auquel on attache nos namers :
vich_uploader: mappings: default: namer: AppBundle\Upload\Namer\ConventionedFileNamer directory_namer: AppBundle\Upload\Namer\ConventionedDirectoryNamer uri_prefix: /uploads upload_destination: '%kernel.root_dir%/../web/uploads'
note c'est bien l'ID du service que vous devez écrire dans
nameretdirectory_namer. Cet article ayant été préparé avec Symfony 3.3, la classe du namer est aussi son ID de service (quoi qu'il en soit, il est nécessaire que le service en question soit public).
Utilisation du mapping unique dans les entités
Il suffit de tout rediriger sur le mapping default.
Dans l'entité User :
<?php class User { /** * @Vich\UploadableField(mapping="default", ...) */ private $avatarFile; }
Dans l'entité Product :
<?php class Product { /** * @Vich\UploadableField(mapping="default", ...) */ private $pictureFile; /** * @Vich\UploadableField(mapping="default", ...) */ private $instructionsFile; }
Conclusion
Avec cette configuration, on obtient une façon assez simple et efficace pour gérer l'upload de nos objets.
Les chemins finaux seront donc de la forme suivante : %kernel.root_dir%/../web/uploads/{entity type}/{id}/{uniqid}_{property}.{extension}
web/
└── uploads/
├── product/
│ ├── 6e8fb2c2-5fe6-41b1-9238-3f499b5aabca/
│ │ ├── 5982cd1b131ab_picture.jpg
│ │ └── 5982cd1b1519b_instructions.pdf
│ ├── d81d4d8d-80b6-41b4-a061-1a0a8ccb54f0/
│ │ └── 5982e22cf3ad5_picture.jpg
│ └── ...
├── user/
│ ├── 1311bfee-aaef-4e7d-83f7-0f92c5db16b5/
│ │ └── 5982e21cd23de_avatar.jpg
│ ├── d3b85796-e1b8-4d2f-be51-acf6c1b382a6/
│ │ └── 5982cd012df44_avatar.png
│ └── ...
└── ...
note Si vous êtes "contraints" par l'utilisation d'identifiants en
AUTO_INCREMENT(ouSEQUENCE), l'arbre sera légèrement différent :
web/
└── uploads/
├── product/
│ ├── 5982e22cf3ad5_picture.jpg
│ ├── 5982cd1b131ab_picture.jpg
│ ├── 5982cd1b1519b_instructions.pdf
│ └── ...
├── user/
│ ├── 5982cd012df44_avatar.png
│ ├── 5982e21cd23de_avatar.jpg
│ └── ...
└── ...