📖 Introduction
Gérer des médias dans Symfony, c’est souvent réinventer la roue : upload de fichiers, redimensionnement d’images, stockage, optimisation… JoliMediaBundle développé par JoliCode met fin à cette répétition en offrant une solution clé en main, testée et maintenue.
Dans ce tutoriel, nous allons créer un système d’articles avec gestion d’images en intégrant JoliMediaBundle avec EasyAdmin. À la fin, vous aurez une médiathèque complète et une interface d’administration professionnelle.
🎯 Ce que nous allons construire
Un back-office complet avec :
- ✅ Une entité
Articleavec titre, contenu et image - ✅ Une interface EasyAdmin pour gérer les articles
- ✅ Une médiathèque pour uploader et organiser vos images
- ✅ Des miniatures générées automatiquement
- ✅ Un stockage dans le filesystem (local)
🚀 Création du Projet
1. Initialisation du Projet Symfony
Si vous partez de zéro, créez un nouveau projet Symfony :
symfony new JoliBundleTest --webapp
cd JoliBundleTest
symfony serve -d
docker compose up -d
On utilise le
compose.ymlde base pour démarrer une instance de PostgreSQL.
📦 Installation des Bundles
1. Installation de JoliMediaBundle
composer require jolicode/media-bundle
Ce bundle installe automatiquement ses dépendances, notamment :
- Flysystem pour la gestion du stockage
- Symfony UX Twig Component pour les composants visuels
2. Installation d’EasyAdmin
composer require easycorp/easyadmin-bundle
3. Installation d’Imagine
JoliMediaBundle nécessite la bibliothèque Imagine pour la manipulation d’images :
composer require imagine/imagine
Configuration des Bundles
1. Enregistrement des Bundles
Vérifiez que les bundles sont bien enregistrés dans config/bundles.php :
<?php
return [
// ... autres bundles
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
JoliCode\MediaBundle\JoliMediaBundle::class => ['all' => true],
JoliCode\MediaBundle\Bridge\EasyAdmin\JoliMediaEasyAdminBundle::class => ['all' => true],
];
Explication :
JoliMediaBundle: Le bundle principalJoliMediaEasyAdminBundle: L’intégration avec EasyAdmin
2. Configuration de Flysystem
Ajoutez la configuration Flysystem dans config/services.yaml :
services:
# ... configuration par défaut
# Configuration Flysystem pour JoliMediaBundle
filesystem.original.adapter:
class: League\Flysystem\Local\LocalFilesystemAdapter
arguments:
$location: '%kernel.project_dir%/public/media/original'
filesystem.original.storage:
class: League\Flysystem\Filesystem
arguments:
$adapter: '@filesystem.original.adapter'
filesystem.cache.adapter:
class: League\Flysystem\Local\LocalFilesystemAdapter
arguments:
$location: '%kernel.project_dir%/public/media/cache'
filesystem.cache.storage:
class: League\Flysystem\Filesystem
arguments:
$adapter: '@filesystem.cache.adapter'
Explication :
filesystem.original.*: Services pour stocker les fichiers originauxfilesystem.cache.*: Services pour stocker les variations (thumbnails, etc.)- Nous utilisons
LocalFilesystemAdapterpour un stockage sur le système de fichiers - En production, vous pourriez utiliser S3, Azure Blob Storage, etc.
3. Configuration du JoliMediaBundle
📚 Pourquoi cette configuration ?
Avant de plonger dans le fichier de configuration, comprenons les concepts clés :
| Concept | Rôle | Exemple |
|---|---|---|
| Library | Conteneur de stockage | default, products, avatars |
| Original | Fichiers uploadés (non modifiés) | photo.jpg |
| Cache | Fichiers transformés/optimisés | photo-thumbnail.jpg |
| Variations | Différentes versions d’un média | thumbnail, medium, large |
| Processor | Moteur de traitement d’image | GD, Imagick |
Le principe : Vous uploadez UN fichier original, le bundle génère automatiquement PLUSIEURS variations selon vos besoins (miniature, moyenne résolution, haute résolution…).
💡 Astuce : Séparer original et cache permet de regénérer facilement toutes les variations si vous changez les paramètres.
Créez le fichier config/packages/joli_media.yaml :
joli_media:
# Bibliothèque par défaut
default_library: default
# Configuration des processeurs d'images
processors:
imagine:
driver: gd # Utiliser GD au lieu d'Imagick
options:
jpeg_quality: 85
png_quality: 80
quality: 85
# Configuration des bibliothèques de médias
libraries:
default:
# Configuration du stockage original
original:
flysystem: "filesystem.original.storage"
url_generator:
strategy: folder
path: /media/original/
# Configuration du stockage cache (variations)
cache:
flysystem: "filesystem.cache.storage"
url_generator:
strategy: folder
path: /media/cache/
must_store_when_generating_url: true
# Désactiver la conversion automatique en WebP pour simplifier
enable_auto_webp: false
# Définition des variations d'images
variations:
# Vignette pour les listes
thumbnail:
transformers:
resize:
width: 200
height: 200
mode: inside
allow_upscale: false
# Format moyen pour l'affichage
medium:
transformers:
resize:
width: 800
height: 600
mode: inside
# Grande image
large:
transformers:
resize:
width: 1920
height: 1080
mode: inside
Explication détaillée :
- default_library : Nom de la bibliothèque par défaut à utiliser
- processors.imagine : Configuration du processeur d’images
driver: gd: Utilise GD (généralement disponible par défaut avec PHP)jpeg_quality,png_quality: Qualité de compression des images
- libraries.default.original : Configuration du stockage des fichiers originaux
flysystem: Service Flysystem à utiliserurl_generator.path: Chemin public pour accéder aux fichiers
- libraries.default.cache : Configuration du stockage des variations
must_store_when_generating_url: true: Génère le fichier lors de la création de l’URL
- variations : Définit les différentes tailles d’images disponibles
thumbnail: Petite image (200x200px max)medium: Image moyenne (800x600px max)large: Grande image (1920x1080px max)mode: inside: L’image est redimensionnée pour tenir dans les dimensions sans déformation
🔑 Points clés à retenir
| Point | Ce qu’il faut comprendre |
|---|---|
| Séparation original/cache | Les fichiers originaux restent intacts, seules les variations sont générées dans le cache |
| Variations à la demande | Avec must_store_when_generating_url: true, les variations sont créées lors du premier accès |
Mode inside | L’image est réduite proportionnellement pour tenir dans les dimensions sans rogner ni déformer |
| Qualité d’image | jpeg_quality: 85 est un bon compromis entre qualité et taille de fichier |
| Driver GD vs Imagick | GD est inclus avec PHP, Imagick offre plus de fonctionnalités mais nécessite une extension |
⚠️ Piège courant : Si vous changez les paramètres des variations, pensez à vider le cache : symfony console cache:clear && rm -rf public/media/cache/*
4. Configuration de l’Intégration EasyAdmin
Créez le fichier config/packages/joli_media_easy_admin.yaml :
joli_media_easy_admin:
# Configuration de l'upload
upload:
max_files: 10
max_file_size: 20 # En Mo
accepted_files:
- image/*
- video/*
- application/pdf
# Visibilité des fonctionnalités
visibility:
show_variations_stored: true
show_variations_action_regenerate: true
show_html_code: true
show_markdown_code: true
Explication :
- upload : Limites pour l’upload de fichiers
- visibility : Fonctionnalités visibles dans l’interface
show_variations_stored: Affiche les variations généréesshow_variations_action_regenerate: Bouton pour régénérer les variationsshow_html_code: Affiche le code HTML pour intégrer l’imageshow_markdown_code: Affiche le code Markdown
5. Configuration des Routes
Créez le fichier config/routes/joli_media.yaml :
# Routes pour JoliMediaBundle
_joli_media:
resource: "@JoliMediaBundle/config/routes.php"
# Routes pour l'intégration EasyAdmin du MediaBundle
_joli_media_easy_admin:
resource: "@JoliMediaEasyAdminBundle/src/Controller/"
prefix: /admin/media
Explication :
- La première section importe les routes du bundle principal
- La seconde section importe les routes de l’intégration EasyAdmin avec le préfixe
/admin/media
📝 Création de l’Entité Article
1. Création de l’Entité
Créez le fichier src/Entity/Article.php :
<?php
namespace App\Entity;
use App\Repository\ArticleRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use JoliCode\MediaBundle\Doctrine\Types as MediaTypes;
use JoliCode\MediaBundle\Model\Media;
use JoliCode\MediaBundle\Validator\Media as MediaConstraint;
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $content = null;
#[ORM\Column(type: MediaTypes::MEDIA, nullable: true)]
#[MediaConstraint(
allowedTypes: ['image'],
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
)]
private ?Media $image = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private ?\DateTimeInterface $createdAt = null;
public function __construct()
{
$this->createdAt = new \DateTime();
}
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): static
{
$this->content = $content;
return $this;
}
public function getImage(): ?Media
{
return $this->image;
}
public function setImage(?Media $image): static
{
$this->image = $image;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
}
Points clés à comprendre :
- Type personnalisé :
MediaTypes::MEDIAest un type Doctrine personnalisé qui stocke le chemin du média en base de données - Objet Media : En PHP, vous manipulez un objet
Mediaqui contient toutes les informations du fichier - Validation : L’attribut
#[MediaConstraint]valide automatiquement :- Le type de média (seulement des images)
- Les formats acceptés (JPEG, PNG, GIF, WebP)
2. Création du Repository
Créez le fichier src/Repository/ArticleRepository.php :
<?php
namespace App\Repository;
use App\Entity\Article;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Article>
*/
class ArticleRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Article::class);
}
}
🎨 Configuration d’EasyAdmin
1. DashboardController
Créez le fichier src/Controller/Admin/DashboardController.php :
<?php
namespace App\Controller\Admin;
use App\Entity\Article;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DashboardController extends AbstractDashboardController
{
#[Route('/admin', name: 'admin')]
public function index(): Response
{
return $this->render('admin/dashboard.html.twig');
}
public function configureDashboard(): Dashboard
{
return Dashboard::new()
->setTitle('JoliMediaBundle - Tutorial')
->setFaviconPath('favicon.ico');
}
public function configureMenuItems(): iterable
{
yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home');
yield MenuItem::linkToCrud('Articles', 'fas fa-newspaper', Article::class);
yield MenuItem::linkToUrl('Médiathèque', 'fas fa-images', '/admin?dashboardControllerFqcn=App%5CController%5CAdmin%5CDashboardController&routeName=joli_media_easy_admin_explore');
}
}
Explication :
- configureMenuItems() : Définit le menu de l’admin avec un lien vers la médiathèque
- L’URL de la médiathèque doit inclure le paramètre
dashboardControllerFqcnpour que le bundle ait le contexte EasyAdmin nécessaire - Le paramètre
routeName=joli_media_easy_admin_exploreindique quelle page de la médiathèque afficher
- L’URL de la médiathèque doit inclure le paramètre
- Pas de configureAssets() : Le JoliMediaBundle gère automatiquement ses propres assets (CSS et JS). Il n’est pas nécessaire de les ajouter manuellement dans le DashboardController, cela causerait même une erreur de duplication
2. ArticleCrudController
Créez le fichier src/Controller/Admin/ArticleCrudController.php :
<?php
namespace App\Controller\Admin;
use App\Entity\Article;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use JoliCode\MediaBundle\Bridge\EasyAdmin\Field\MediaChoiceField;
class ArticleCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Article::class;
}
public function configureCrud(Crud $crud): Crud
{
return parent::configureCrud($crud)
// Ajouter le thème de formulaire pour le MediaBundle
->addFormTheme('@JoliMediaEasyAdmin/form/form_theme.html.twig')
->setEntityLabelInSingular('Article')
->setEntityLabelInPlural('Articles')
->setPageTitle('index', 'Liste des articles')
->setPageTitle('new', 'Créer un article')
->setPageTitle('edit', 'Modifier un article');
}
public function configureFields(string $pageName): iterable
{
yield IdField::new('id')->hideOnForm();
yield TextField::new('title', 'Titre')
->setRequired(true)
->setHelp('Le titre de votre article');
yield TextEditorField::new('content', 'Contenu')
->setRequired(true)
->setHelp('Le contenu complet de votre article');
yield MediaChoiceField::new('image', 'Image')
->setRequired(false);
yield DateTimeField::new('createdAt', 'Date de création')
->hideOnForm();
}
}
Points importants :
- addFormTheme() : Active le thème de formulaire du JoliMediaBundle
- Sans cela, le champ
MediaChoiceFieldne s’afficherait pas correctement
- Sans cela, le champ
- MediaChoiceField : Le champ magique fourni par le bundle
- Permet de sélectionner un média existant OU d’en uploader un nouveau
- Affiche un aperçu de l’image sélectionnée
- Optionnel avec
setRequired(false)pour permettre des articles sans image
3. Template du Dashboard
Créez le fichier templates/admin/dashboard.html.twig :
{% extends '@EasyAdmin/page/content.html.twig' %}
{% block content_title %}
<h1>Bienvenue dans l'administration</h1>
{% endblock %}
{% block main %}
<div class="row">
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-newspaper"></i> Gestion des Articles
</h5>
<p class="card-text">Créez et gérez vos articles avec leurs images.</p>
<a href="/admin/article" class="btn btn-primary">
Voir les articles
</a>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-images"></i> Médiathèque
</h5>
<p class="card-text">Gérez vos images et médias avec JoliMediaBundle.</p>
<a href="/admin?dashboardControllerFqcn=App%5CController%5CAdmin%5CDashboardController&routeName=joli_media_easy_admin_explore" class="btn btn-primary">
Accéder à la médiathèque
</a>
</div>
</div>
</div>
</div>
<div class="alert alert-info mt-4">
<h4 class="alert-heading">À propos de ce projet</h4>
<p>
Ce projet est un tutoriel pédagogique démontrant l'intégration du
<strong>JoliMediaBundle</strong> avec <strong>EasyAdmin</strong> dans Symfony.
</p>
<hr>
<p class="mb-0">
Le bundle JoliMediaBundle facilite la gestion, le stockage et l'optimisation
des médias dans vos applications Symfony.
</p>
</div>
{% endblock %}
Explication :
ea_url(): Fonction Twig fournie par EasyAdmin pour générer des URLs vers les pages d’administration.setController(): Spécifie le contrôleur CRUD cible (échapper les backslashes avec\\\\).setAction('index'): Définit l’action à exécuter (ici, afficher la liste)
- Cette méthode est plus fiable que
path()car elle gère automatiquement tous les paramètres EasyAdmin nécessaires
🗄️ Migration de Base de Données
1. Créer la Migration
symfony console make:migration
Cette commande génère un fichier de migration dans le dossier migrations/.
2. Exécuter la Migration
symfony console doctrine:migrations:migrate
Cela crée la table article dans votre base de données PostgreSQL.
📁 Préparation des Dossiers Médias
Créons les dossiers de stockage des médias.
mkdir -p public/media/original public/media/cache
Installation des Assets
symfony console assets:install
Cette commande copie les assets publics des bundles (CSS, JS, images) dans public/bundles/.
Test de l’Application
1. Démarrer le Serveur
symfony serve -d
2. Accéder à l’Administration
Ouvrez votre navigateur à l’adresse : https://localhost:8000/admin et tada ! ✨
✅ Conclusion
JoliMediaBundle transforme la gestion de médias dans Symfony d’un casse-tête en une fonctionnalité clé en main. En quelques minutes de configuration, vous obtenez une médiathèque prête à l’emploi.
Loading comments...