Skip to Content

Chapitre 4.6 — Les commandes console

Partie 4 : Cœur HTTP — Chapitre 6/6 Version de référence : Symfony 7.4 (composant Console).

⏱️ TL;DR

  • bin/console est l’autre porte d’entrée de votre application : pas de HTTP, mais du CLI (tâches d’admin, imports, nettoyage, jobs planifiés).
  • Une commande = une classe avec l’attribut #[AsCommand(name: '...')] qui implémente __invoke() (ou étend Command).
  • On génère le squelette avec make:command, on définit arguments (positionnels) et options (--flag), et on écrit la sortie via SymfonyStyle (titres, tableaux, barres de progression, couleurs).
  • Une commande est un service : elle reçoit ses dépendances par injection (comme un contrôleur).
  • Elle renvoie un code de sortie : Command::SUCCESS (0) ou Command::FAILURE (1).
  • Pour un dev Next.js : c’est votre « script node scripts/xxx.mjs », mais intégré au framework (accès aux services, à la base, à la config, testable).

🎯 Objectifs

  • Créer une commande console avec make:command.
  • Définir arguments et options, et lire leurs valeurs.
  • Produire une sortie soignée avec SymfonyStyle.
  • Comprendre quand une commande est le bon outil (vs un contrôleur).

1. Pourquoi des commandes console

Toute application a des tâches hors requête HTTP : importer un CSV, purger des données, envoyer des rappels quotidiens, ré-indexer une recherche, créer un compte admin. Le composant Console de Symfony fournit un cadre pour ça. Vous en avez déjà utilisé beaucoup : php bin/console make:*, doctrine:migrations:migrate, cache:clear, debug:router… Ce sont des commandes. Vous allez écrire les vôtres.

L’atout clé : une commande accède aux mêmes services que vos contrôleurs. Un BookingReminder (service métier) peut être appelé depuis une commande planifiée et depuis un contrôleur — zéro duplication.

2. Créer une commande

php bin/console make:command app:send-reminders

Le Maker génère src/Command/SendRemindersCommand.php. Voici une version typique, avec injection de service et SymfonyStyle :

// src/Command/SendRemindersCommand.php declare(strict_types=1); namespace App\Command; use App\Service\BookingReminder; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( name: 'app:send-reminders', description: 'Envoie les rappels aux clients ayant une réservation demain.', )] final class SendRemindersCommand extends Command { public function __construct( private readonly BookingReminder $reminder, // injection, comme un contrôleur ) { parent::__construct(); } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $io->title('Envoi des rappels'); $count = $this->reminder->sendForTomorrow(); $io->success(sprintf('%d rappel(s) envoyé(s).', $count)); return Command::SUCCESS; // 0 (échec : Command::FAILURE = 1) } }

On la lance :

php bin/console app:send-reminders

Depuis Symfony 5 : deux évolutions. (1) L’attribut #[AsCommand] remplace les propriétés statiques $defaultName/$defaultDescription de l’ère 5 — plus propre, et la commande est auto-enregistrée comme service. (2) Symfony 7.3+ permet même une commande sans hériter de Command, via une méthode __invoke() avec des arguments résolus par attributs — mais l’héritage de Command reste parfaitement idiomatique et clair.

3. Arguments et options

  • Un argument est positionnel (app:import fichier.csv).
  • Une option est nommée (--dry-run, --limit=100), avec ou sans valeur.
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; protected function configure(): void { $this ->addArgument('file', InputArgument::REQUIRED, 'Chemin du fichier CSV') ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simuler sans écrire') ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Nombre max de lignes', 1000); } protected function execute(InputInterface $input, OutputInterface $output): int { $file = $input->getArgument('file'); $dryRun = $input->getOption('dry-run'); // bool $limit = (int) $input->getOption('limit'); // ... return Command::SUCCESS; }

Usage : php bin/console app:import bookings.csv --dry-run --limit=500.

4. Une sortie soignée avec SymfonyStyle

SymfonyStyle ($io) offre une API riche pour un rendu pro :

$io->title('Import des réservations'); $io->section('Validation'); $io->text('Lecture du fichier…'); $io->note('Astuce : utilisez --dry-run pour tester.'); $io->warning('3 lignes ignorées (email manquant).'); $io->success('Import terminé : 497 réservations créées.'); $io->error('Fichier introuvable.'); // tableau $io->table(['ID', 'Email', 'Statut'], [ [1, 'a@b.c', 'confirmed'], [2, 'd@e.f', 'pending'], ]); // barre de progression $io->progressStart(1000); foreach ($rows as $row) { // ... traiter $io->progressAdvance(); } $io->progressFinish(); // interactions (questions) $email = $io->ask('Email de l’administrateur ?'); $confirm = $io->confirm('Confirmer la suppression ?', false);

💡 Pour un dev Next.js : SymfonyStyle ≈ un mélange de chalk + ora (spinners/progress) + inquirer (questions), mais intégré et cohérent. Vous n’assemblez pas 4 packages : tout vient avec le composant Console.

5. Commandes et tâches planifiées

Les commandes sont la cible des tâches planifiées : un cron système peut appeler php bin/console app:send-reminders chaque matin. Mais Symfony offre mieux — le composant Scheduler (Partie 12) — qui planifie l’exécution dans l’application (déclenchant des messages/handlers), sans dépendre du cron de l’OS. On construira les rappels de BookLab ainsi.

⚠️ Piège : une commande longue (traitement de milliers d’éléments) doit gérer la mémoire (utiliser des générateurs, vider l’EntityManager par lots avec clear()), sinon elle gonfle jusqu’au crash. On verra les patterns de traitement par lot en Partie 6 (Doctrine) et Partie 12 (Messenger).

6. Quand une commande plutôt qu’un contrôleur ?

BesoinOutil
Répondre à une requête d’un utilisateur/navigateur/APIContrôleur (HTTP)
Tâche d’administration, import/export, batchCommande console
Job récurrent (rappels, nettoyage)Commande + Scheduler (Partie 12)
Travail déclenché par une requête mais différéMessenger (Partie 12), pas une commande synchrone

✏️ Exercices

Exercice 1. Créez une commande app:create-admin qui prend un argument email (requis) et une option --name, et affiche un message de succès. (Supposez un service AdminCreator::create(string $email, ?string $name): void.)

✅ Solution

#[AsCommand(name: 'app:create-admin', description: 'Crée un compte administrateur.')] final class CreateAdminCommand extends Command { public function __construct(private readonly AdminCreator $creator) { parent::__construct(); } protected function configure(): void { $this ->addArgument('email', InputArgument::REQUIRED, 'Email de l’admin') ->addOption('name', null, InputOption::VALUE_REQUIRED, 'Nom affiché'); } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $this->creator->create($input->getArgument('email'), $input->getOption('name')); $io->success('Administrateur créé.'); return Command::SUCCESS; } }

Usage : php bin/console app:create-admin admin@booklab.test --name="Alex".

Exercice 2. Pourquoi une commande peut-elle réutiliser exactement la même logique qu’un contrôleur ?

✅ Solution

Parce que la logique métier vit dans des services (contrôleurs fins, chapitre 4.3), et qu’une commande, comme un contrôleur, reçoit ces services par injection. Le même BookingReminder (ou AdminCreator) est appelé depuis l’un ou l’autre point d’entrée — zéro duplication. HTTP et CLI sont deux portes vers le même cœur applicatif.

Exercice 3. Une commande d’import de 1 million de lignes fait planter la mémoire. Deux pistes de correction ?

✅ Solution

(1) Lire le fichier avec un générateur (yield, chapitre 2.7) au lieu de tout charger en mémoire. (2) Traiter par lots et vider l’EntityManager régulièrement ($em->flush(); $em->clear(); tous les N éléments) pour ne pas accumuler les entités gérées. Éventuellement, déléguer à Messenger pour paralléliser (Partie 12).

🧠 Quiz de révision

1. Qu’est-ce que bin/console ?

L’entrée CLI de l’application : un point d’exécution hors HTTP pour les tâches d’admin, imports, jobs, avec accès aux mêmes services que les contrôleurs.

2. Quel attribut déclare une commande, et comment la générer ?

#[AsCommand(name: '...')] (auto-enregistrée comme service). On génère le squelette avec php bin/console make:command.

3. Différence entre un argument et une option ?

Un argument est positionnel (app:import fichier.csv) ; une option est nommée (--dry-run, --limit=100).

4. Que renvoie execute() et quelles valeurs ?

Un code de sortie entier : Command::SUCCESS (0) ou Command::FAILURE (1).

5. Comment obtient-on une sortie soignée (titres, tableaux, progression) ?

Avec SymfonyStyle ($io = new SymfonyStyle($input, $output)) : title(), success(), table(), progressStart/Advance/Finish(), ask(), etc.


Fin de la Partie 4. Vous maîtrisez le cœur HTTP : le pipeline du noyau, le routing, les contrôleurs fins, la résolution d’arguments (entités, DTO validés), les sessions/flash, et les commandes console. BookLab a désormais de vraies portes d’entrée.

Prochaine étape : Partie 5 — Injection de dépendances & Services — le conteneur, l’autowiring, et le mécanisme qui irrigue tout Symfony.