Architecture Console et cycle de vie d'une commande
Un Command Symfony suit un cycle de vie en 4 phases — configure, initialize, interact, execute — et s'intègre dans une Application qui gère le routing et l'I/O.
Le composant Console Symfony est plus qu'une simple bibliothèque de parsing — c'est un mini-framework
avec une Application qui route par nom, gère les erreurs, affiche les listes de commandes
et délègue l'exécution à des objets Command.
Point d'entrée du binaire (
bin/console). Elle enregistre toutes les commandes via
add(Command), résout le nom saisi et orchestre le cycle d'exécution.
Dans Symfony full-stack, le Kernel l'alimente automatiquement.
Cycle de vie d'une commande
Chaque commande traverse exactement quatre méthodes, dans l'ordre. Ne jamais les mélanger :
| Méthode | Rôle | I/O |
|---|---|---|
configure() |
Déclarer les arguments, options, description, help | Non |
initialize() |
Bootstrap sans interaction — charger fichiers, valider env | Non |
interact() |
Poser des questions interactives pour compléter les entrées manquantes | Oui |
execute() |
Logique principale — retourner SUCCESS/FAILURE/INVALID | Oui |
execute() est la seule méthode obligatoire à implémenter.
Les trois autres sont optionnelles — surcharge-les seulement si tu en as besoin.
Codes de retour
La valeur retournée par execute() devient le code de sortie du processus :
// Constantes de la classe Command Command::SUCCESS // 0 — tout s'est bien passé Command::FAILURE // 1 — erreur métier ou technique Command::INVALID // 2 — arguments/options invalides
#[AsCommand]
L'attribut remplace la constante protected static $defaultName héritée :
#[AsCommand( name: 'app:users:create', description: 'Crée un utilisateur en base', aliases: ['app:user:new'], hidden: false, )] final class CreateUserCommand extends Command { // ... }
help accepte une longue description affichée avec --help.
Utilise-le pour documenter les cas d'usage avancés — il supporte les balises de formatage.
Arguments et options
Les arguments sont positionnels et obligatoires ou optionnels — les options commencent par -- et peuvent être des flags, des scalaires ou des tableaux.
Arguments et options sont les deux façons d'alimenter une commande depuis la ligne de commande. Ils ont des sémantiques très différentes — les confondre nuit à l'ergonomie de la CLI.
- Positionnels — l'ordre compte
- Syntaxe :
command valeur - Modes : REQUIRED, OPTIONAL, IS_ARRAY
- Nommées — l'ordre ne compte pas
- Syntaxe :
--nom=valeurou--flag - Modes : VALUE_NONE, VALUE_REQUIRED, VALUE_OPTIONAL, VALUE_IS_ARRAY, VALUE_NEGATABLE
Arguments — addArgument()
$this->addArgument( 'username', InputArgument::REQUIRED, // 1 — obligatoire 'Nom de l\'utilisateur', null, // valeur par défaut (OPTIONAL seulement) ['alice', 'bob'] // suggestedValues pour l'autocomplétion ); $this->addArgument('roles', InputArgument::IS_ARRAY); // Usage : command alice ROLE_USER ROLE_ADMIN // $input->getArgument('roles') retourne ['ROLE_USER', 'ROLE_ADMIN']
| Constante | Valeur | Comportement |
|---|---|---|
REQUIRED | 1 | Doit être fourni — erreur sinon |
OPTIONAL | 2 | Peut être omis — retourne la valeur par défaut |
IS_ARRAY | 4 | Capture tous les arguments restants dans un tableau |
Options — addOption()
$this->addOption( 'format', // nom long : --format 'f', // raccourci : -f InputOption::VALUE_REQUIRED, // 2 — --format=json obligatoire 'Format de sortie', 'text', // valeur par défaut ['text', 'json', 'csv'] // suggestedValues ); // VALUE_NONE : flag booléen // --dry-run : $input->getOption('dry-run') === true // absent : $input->getOption('dry-run') === false $this->addOption('dry-run', null, InputOption::VALUE_NONE); // VALUE_NEGATABLE : --format / --no-format // --format : true, --no-format : false, absent : null $this->addOption('format', null, InputOption::VALUE_NEGATABLE);
| Constante | Valeur | Syntaxe CLI | Retour getOption() |
|---|---|---|---|
VALUE_NONE | 1 | --flag | true / false |
VALUE_REQUIRED | 2 | --opt=val | string |
VALUE_OPTIONAL | 4 | --opt ou --opt=val | string ou null |
VALUE_IS_ARRAY | 8 | --opt=a --opt=b | string[] |
VALUE_NEGATABLE | 16 | --opt / --no-opt | true / false / null |
IS_ARRAY | REQUIRED (opérateur bitwise PHP).
Change le type et le mode pour voir le comportement correspondant.
OutputInterface et verbosité
OutputInterface est le canal de sortie — il contrôle les niveaux de verbosité et les tags de formatage.
OutputInterface est l'abstraction sur laquelle s'appuient toutes les sorties Console.
Elle n'est pas liée au terminal — tu peux la substituer par un BufferedOutput ou
un NullOutput sans changer une ligne de code.
Niveaux de verbosité
Cinq niveaux, chacun correspondant à un drapeau CLI. Le niveau minimal déclenche l'affichage de tout le contenu en-dessous de lui :
| Constante | Valeur | Drapeau | Usage |
|---|---|---|---|
VERBOSITY_QUIET | 16 | -q | Aucune sortie sauf erreurs fatales |
VERBOSITY_NORMAL | 32 | (rien) | Sorties utilisateur standard |
VERBOSITY_VERBOSE | 64 | -v | Détails d'exécution |
VERBOSITY_VERY_VERBOSE | 128 | -vv | Informations de debug de haut niveau |
VERBOSITY_DEBUG | 256 | -vvv | Tout — SQL, requêtes HTTP, traces internes |
Écriture conditionnelle
// Méthode directe : passage du niveau en 3e argument de writeln() $output->writeln('Message verbose', OutputInterface::VERBOSITY_VERBOSE); // Méthode conditionnelle : garde explicite if ($output->isVerbose()) { $output->writeln('Connexion à la base : ' . $dsn); } if ($output->isDebug()) { $output->writeln('Requête SQL : ' . $sql); }
Tags de formatage
La Console interprète des balises XML légères pour colorier la sortie :
$output->writeln('<info>Succès</info>'); // vert $output->writeln('<comment>Attention</comment>'); // jaune $output->writeln('<question>Confirmer ?</question>'); // cyan $output->writeln('<error>Erreur critique</error>'); // blanc sur rouge $output->writeln('<fg=magenta>Personnalisé</>'); $output->writeln('<fg=white;bg=blue;options=bold>Surligné</>');
write() n'ajoute pas de saut de ligne — utile pour une progression sur une seule ligne.
writeln() termine toujours par \n. Les deux acceptent un tableau de chaînes.
Implémentation de
OutputInterface qui absorbe silencieusement toute écriture.
Parfait pour les tests qui ne se soucient pas de la sortie, ou pour forcer le mode silencieux
dans un contexte non-interactif.
ConsoleOutput sépare stdout et stderr
(accessible via getErrorOutput()). Préfère écrire les erreurs sur stderr pour
permettre la redirection indépendante (2>/dev/null).
SymfonyStyle — la boîte à outils I/O
SymfonyStyle est une façade sur Output qui fournit des blocs visuels structurés — titres, sections, tables, notifications.
SymfonyStyle emballe un InputInterface et un OutputInterface pour
offrir une API de haut niveau. Elle impose une cohérence visuelle — espaces, couleurs, encadrés —
sans que tu aies à construire les séquences ANSI toi-même.
use Symfony\Component\Console\Style\SymfonyStyle; protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $io->title('Importation des utilisateurs'); // ... return Command::SUCCESS; }
Blocs structurants
$io->title('Import CSV'); // === titre === en jaune $io->section('Étape 1 : lecture'); // --- section --- en bleu $io->text('Fichier : import.csv'); // ligne de texte simple $io->text(['Ligne 1', 'Ligne 2']); // tableau = plusieurs lignes $io->listing(['Alice', 'Bob']); // liste à puces $io->newLine(2); // 2 lignes vides
Notifications
$io->success('42 utilisateurs importés'); // bloc vert [OK] $io->error('Fichier introuvable'); // bloc rouge [ERROR] $io->warning('Encodage non UTF-8 détecté'); // bloc orange [WARNING] $io->note('Aucun doublon trouvé'); // bloc bleu [NOTE] $io->info('Durée : 2.3s'); // bloc cyan [INFO] $io->caution('Opération irréversible !'); // bloc rouge [CAUTION] // Variantes outline — même couleur, sans fond plein $io->outlineSuccess('Terminé'); $io->outlineError('Échec partiel');
Tableaux
// Tableau vertical classique $io->table( ['ID', 'Nom', 'Email'], [ [1, 'Alice', 'alice@example.com'], [2, 'Bob', 'bob@example.com'], ] ); // Tableau horizontal — headers à gauche, valeurs à droite $io->horizontalTable( ['Symfony', 'PHP', 'Doctrine'], [['8.1', '8.4', '3.x']] ); // Liste de définitions — clé : valeur sur une ligne $io->definitionList( ['Slug' => 'symfony-console'], ['Sections' => 10], ['Statut' => 'publié'], );
OutputInterface est attendu — pratique pour transmettre l'objet $io
à des méthodes privées qui écrivent elles aussi.
Questions et interactions
Les helpers de questions permettent d'obtenir des données interactives — avec validation, autocomplétion et gestion des erreurs.
Les commandes interactives posent des questions à l'utilisateur pour collecter des données manquantes. L'objectif est de faire marcher la commande en mode non-interactif (scripts, CI) et de guider l'utilisateur humain lorsqu'il oublie un argument.
Symfony appelle
interact() avant execute(), mais seulement si
l'entrée est interactive. C'est l'endroit idéal pour compléter des arguments optionnels
avec des valeurs saisies — sans polluer la logique de execute().
Les quatre helpers SymfonyStyle
// Texte libre avec valeur par défaut $name = $io->ask('Nom d\'utilisateur', 'alice'); // Texte masqué — mot de passe, token $password = $io->askHidden('Mot de passe'); // Confirmation booléenne if (!$io->confirm('Êtes-vous sûr ?', false)) { return Command::SUCCESS; } // Choix dans une liste $format = $io->choice('Format de sortie', ['json', 'csv', 'text'], 'text'); // Sélection multiple (4e argument = true) $roles = $io->choice('Rôles', ['ADMIN', 'USER', 'API'], null, true);
Validation et autocomplétion
use Symfony\Component\Console\Question\Question; $question = new Question('Email : '); $question->setValidator(function (?string $value): string { if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { throw new \RuntimeException('Email invalide'); } return $value; }); $question->setMaxAttempts(3); // exception après 3 essais $question->setAutocompleterValues([ // tab-complétion 'alice@example.com', 'bob@example.com' ]); $helper = $this->getHelper('question'); $email = $helper->ask($input, $output, $question);
$input->isInteractive() avant tout
ask(). En CI, Symfony passe automatiquement en mode non-interactif quand l'entrée
n'est pas un TTY — mais c'est une bonne pratique de le vérifier explicitement dans
interact() pour les tests unitaires.
null — l'utilisateur peut boucler indéfiniment. Toujours fixer
une limite dans les scripts automatisés.
ProgressBar — retour visuel
ProgressBar est le composant de progression visuelle — il affiche des barres, des pourcentages et des estimations de temps.
ProgressBar transforme une boucle silencieuse en retour visuel — sans avoir à
calculer les pourcentages ou gérer les séquences d'échappement ANSI.
C'est l'un des composants Console les plus utiles pour les commandes longues.
Via SymfonyStyle — la voie rapide
$io->progressStart(100); // total = 100 éléments foreach ($items as $item) { // traitement… $io->progressAdvance(); // avance de 1 } $io->progressFinish(); // affiche 100% et passe à la ligne
Via ProgressBar directe — le contrôle total
use Symfony\Component\Console\Helper\ProgressBar; $bar = new ProgressBar($output, 50); $bar->setFormat('verbose'); // format prédéfini $bar->setBarCharacter('█'); $bar->setEmptyBarCharacter('░'); $bar->setProgressCharacter('▶'); $bar->setBarWidth(40); $bar->start(); foreach ($items as $item) { $bar->setMessage('Traitement : ' . $item->name); // traitement… $bar->advance(); } $bar->finish();
Formats prédéfinis
| Format | Rendu type |
|---|---|
normal | 0/50 [>--] 0% |
verbose | 0/50 [>--] 0% 1 sec |
very_verbose | 0/50 [>--] 0% 1 sec/est. 50 secs |
debug | 0/50 [>--] 0% 1 sec/est. 50 secs 1.0 MB |
Placeholders
Un format personnalisé peut utiliser ces placeholders :
$bar->setFormat('[%bar%] %percent:3s%% %current%/%max% %elapsed:6s%/%estimated:-6s% %memory:6s% %message%');
| Placeholder | Description |
|---|---|
%current% | Étape courante |
%max% | Total |
%bar% | Barre graphique |
%percent% | Pourcentage (0–100) |
%elapsed% | Temps écoulé |
%remaining% | Temps restant estimé |
%estimated% | Durée totale estimée |
%memory% | Mémoire courante |
%message% | Message défini par setMessage() |
progressIterate() — la voie la plus concise
// Wrap automatique — aucune gestion start/advance/finish foreach ($io->progressIterate($items, count($items)) as $item) { // traitement… }
0
— la barre affichera une animation en cours sans pourcentage.
Table et rendu structuré
Table génère des tableaux ASCII stylisés — avec des séparateurs, des styles prédéfinis et des titres.
Table génère des tableaux alignés automatiquement dans le terminal.
Elle gère les largeurs de colonnes, les séparateurs et plusieurs styles visuels.
Usage de base
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableSeparator; $table = new Table($output); $table ->setHeaderTitle('Utilisateurs') ->setHeaders(['ID', 'Nom', 'Rôle']) ->setRows([ [1, 'Alice', 'ADMIN'], [2, 'Bob', 'USER'], new TableSeparator(), [3, 'Carol', 'API'], ]) ->setFooterTitle('3 utilisateurs') ->render();
Styles visuels
$table->setStyle('box'); // ┌─┬─┐ / │ │ │ / └─┴─┘ $table->setStyle('box-double'); // ╔═╦═╗ / ║ ║ ║ / ╚═╩═╝ $table->setStyle('compact'); // sans bordures externes $table->setStyle('default'); // +---+---+ (ASCII pur)
| Style | Caractères | Cas d'usage |
|---|---|---|
default | ASCII + - | | Compatibilité maximale |
box | Unicode box-drawing | Terminaux modernes |
box-double | Double trait Unicode | Titres importants |
compact | Espaces seuls | Logs, sortie parsée |
symfony-style-guide | Mixte | Outil officiel Symfony |
Contrôle des largeurs
// Largeur fixe d'une colonne (index 0-based) $table->setColumnWidth(1, 30); // colonne 1 = 30 chars // Largeur maximale — tronque avec "…" si dépassée $table->setColumnMaxWidth(2, 20);
setRows() remplace toutes les lignes existantes.
addRow() en ajoute une seule à la fin.
Pour construire un tableau ligne par ligne dans une boucle, utilise addRow().
new TableCell('texte', ['colspan' => 2]).
Utile pour les en-têtes de groupe couvrant plusieurs colonnes.
Autocomplétion et LazyCommand
Console supporte l'autocomplétion native du shell — et LazyCommand diffère l'instanciation pour des applications avec beaucoup de commandes.
Deux fonctionnalités avancées qui améliorent l'ergonomie et les performances des CLI complexes :
l'autocomplétion native du shell, et LazyCommand qui diffère l'instanciation
des commandes jusqu'à leur invocation.
Autocomplétion — complete()
Pour qu'une commande propose des suggestions au shell (bash/zsh/fish), implémente la méthode
complete(). Elle est appelée par la sous-commande interne _complete :
use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; public function complete( CompletionInput $input, CompletionSuggestions $suggestions ): void { // Suggestion pour un argument if ($input->mustSuggestArgumentValuesFor('username')) { $suggestions->suggestValues(['alice', 'bob', 'carol']); return; } // Suggestion pour une option if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues(['json', 'csv', 'text']); return; } // Suggestion d'options supplémentaires if ($input->getCompletionType() === CompletionInput::TYPE_OPTION_NAME) { $suggestions->suggestOptions($this->getDefinition()->getOptions()); } }
| Type CompletionInput | Contexte |
|---|---|
TYPE_ARGUMENT_VALUE | L'utilisateur tape la valeur d'un argument |
TYPE_OPTION_VALUE | L'utilisateur tape la valeur d'une option (--opt=…) |
TYPE_OPTION_NAME | L'utilisateur tape un nom d'option (--…) |
TYPE_NONE | Aucun contexte précis déterminé |
LazyCommand — instanciation différée
Dans une application avec des centaines de commandes, instancier chaque Command
(et donc ses dépendances) au démarrage est coûteux. LazyCommand résout ce problème :
use Symfony\Component\Console\Command\LazyCommand; // Dans l'Application ou un CompilerPass : $application->add(new LazyCommand( name: 'app:heavy:process', aliases: ['app:process'], description: 'Traitement lourd avec beaucoup de dépendances', isHidden: false, commandFactory: fn() => new HeavyProcessCommand($dep1, $dep2), ));
name, aliases, description, isHidden sont
lus sans instancier la commande — ils servent pour la liste et le routing.
La commandFactory n'est appelée que lorsque la commande est effectivement exécutée.
console.command
sont automatiquement enveloppées dans LazyCommand par le framework. Tu bénéficies
du mécanisme sans rien configurer.
Tester ses commandes
Symfony fournit CommandTester pour tester les commandes sans instancier l'Application — avec simulation d'inputs et capture d'output.
Tester une commande Console ne doit pas nécessiter de lancer un processus — CommandTester
capture la sortie et simule les entrées en mémoire, rendant les tests rapides et isolés.
Structure d'un test
use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; class CreateUserCommandTest extends KernelTestCase { public function testExecute(): void { // Instancier la commande avec ses dépendances mockées $command = new CreateUserCommand( $this->createMock(UserRepository::class) ); $tester = new CommandTester($command); // Exécuter avec des arguments et options $tester->execute([ 'username' => 'alice', // argument '--format' => 'json', // option '--dry-run' => true, // flag ], [ 'decorated' => false, // désactive les couleurs ANSI ]); // Assertions $tester->assertCommandIsSuccessful(); $this->assertStringContainsString('alice', $tester->getDisplay()); } }
Tester les questions interactives
// setInputs() simule les réponses clavier dans l'ordre $tester->setInputs(['alice', 'motdepasse', 'yes']); $tester->execute([]);
API complète de CommandTester
| Méthode | Description |
|---|---|
execute(input, options) | Lance la commande, retourne le code de sortie |
getDisplay(normalize) | Sortie stdout (normalize = normalise les retours chariot) |
getErrorOutput() | Sortie stderr |
getStatusCode() | Code de retour (0, 1, 2) |
setInputs(array) | Simule les réponses aux questions interactives |
assertCommandIsSuccessful() | Assert que le code de retour est 0 |
Dans des cas avancés où la commande vérifie
$input->isInteractive(),
appelle $tester->execute([], ['interactive' => false]) pour simuler
un environnement non-interactif.
Si la commande doit résoudre des services du container, utilise
Application::find(name) sur une Application bootstrappée
depuis le Kernel de test — puis passe la commande trouvée à CommandTester.
Synthèse — guide de choix et anti-patterns
Le guide de choix et les anti-patterns de Console Symfony.
Après neuf sections, voici les guides de décision qui s'appliquent à la construction d'une interface CLI robuste avec Symfony Console.
Guide de choix : argument ou option ?
— La valeur est obligatoire et positionnelle (ex. : nom de fichier, username)
— L'ordre d'entrée est naturel pour l'utilisateur
— Il n'y a pas d'ambiguïté sémantique
— Le paramètre est optionnel ou modifie le comportement par défaut
— C'est un flag booléen (
--dry-run, --verbose)— Tu veux permettre
--no-maprefix (VALUE_NEGATABLE)— La valeur peut être omise avec un défaut implicite
Guide de choix : quelle question poser ?
| Besoin | Méthode |
|---|---|
| Texte libre, une réponse | ask() |
| Mot de passe / secret | askHidden() |
| Oui / Non | confirm() |
| Choix parmi une liste finie | choice() |
| Texte avec validation + autocomplétion | Question + setValidator() |
Guide de choix : ProgressBar
| Situation | Solution |
|---|---|
| Itérable connu, pas de logique complexe | progressIterate() |
| Boucle avec logique entre les steps | progressStart/Advance/Finish via $io |
| Format personnalisé, placeholders spécifiques | ProgressBar directe |
| Total inconnu | max = 0 → animation sans pourcentage |
Récapitulatif des modes
| InputArgument | Valeur |
|---|---|
REQUIRED | 1 |
OPTIONAL | 2 |
IS_ARRAY | 4 |
| InputOption | Valeur | Drapeau CLI |
|---|---|---|
VALUE_NONE | 1 | --flag |
VALUE_REQUIRED | 2 | --opt=val |
VALUE_OPTIONAL | 4 | --opt ou --opt=val |
VALUE_IS_ARRAY | 8 | --opt=a --opt=b |
VALUE_NEGATABLE | 16 | --opt / --no-opt |
| VERBOSITY_* | Valeur | Drapeau |
|---|---|---|
QUIET | 16 | -q |
NORMAL | 32 | (rien) |
VERBOSE | 64 | -v |
VERY_VERBOSE | 128 | -vv |
DEBUG | 256 | -vvv |
4 anti-patterns à éviter
La commande est une couche Infrastructure. Elle doit déléguer à un Handler ou Service de domaine — jamais implémenter elle-même les règles métier.
Injecter
OutputInterface ou SymfonyStyle dans un Handler de domaine
crée un couplage fort et rend les tests unitaires pénibles.
Utilise des events, des retours ou des callbacks.
Si
interact() pose des questions et que tu ne passes pas setInputs(),
le test se bloque indéfiniment en attendant une saisie clavier.
Vérifie toujours $input->isInteractive() ou passe ['interactive' => false].
Ne jamais terminer
execute() sans return Command::SUCCESS (ou FAILURE/INVALID).
Sans return explicite, PHP retourne null, qui vaut 0 — masquant les erreurs réelles.