Ce qui se passe
Les applications console Symfony groupent leurs commandes avec des deux-points
— cache:clear, messenger:consume. Docker et Git ont retenu l'autre
convention : des espaces, où chaque niveau analyse ses propres options
(docker compose up).
Symfony 8.2 permet la seconde forme, sans retirer la première. Les deux invocations suivantes lancent la même commande :
php bin/console tenant:users:import customers.csv --dry-run
php bin/console tenant users import customers.csv --dry-runContribution de Nicolas Grekas, PR #65825 pour les sous-commandes et #65853 pour les groupes.
Ce que la forme espacée change vraiment
Ce n'est pas un alias. Dans la forme espacée, chaque niveau enregistré analyse ses propres options, et seule la dernière commande s'exécute :
php bin/console tenant --name=acme users import customers.csv --dry-runAvec les deux-points, tout est analysé d'un bloc, à la fin.
Trois conséquences à connaître :
- Tous les niveaux n'ont pas besoin d'une commande. Si seul
tenant:users:importest enregistré,usersest un nœud implicite. Cela fonctionne donc sur n'importe quel namespace existant, sans rien écrire. - Une commande sans code liste ses sous-commandes et sort avec le code
1— exactement comme un namespace nu aujourd'hui. - Les sous-commandes ont toujours la priorité sur les arguments. Avant
8.2,
deploy rollbackpassaitrollbackcomme valeur de l'argumenttarget. Pour retrouver ce comportement, il faut--:
php bin/console deploy -- rollback--help, help, list et l'autocomplétion comprennent l'arbre :
tenant users import --help affiche l'aide de import, et
tenant users im<TAB> complète.
Lire l'entrée du parent
Une sous-commande n'hérite pas des options de ses parents. Elle accède à la
chaîne des commandes résolues via l'objet CommandChain, injectable comme
n'importe quel service de console :
public function __invoke(
CommandChain $chain,
#[Argument] string $file,
): int {
$tenantName = $chain->getInput('tenant')?->getOption('name');
// getInput(TenantCommand::class) fonctionne aussi
}L'opérateur nullsafe n'est pas décoratif : le parent ne fait partie de la chaîne
que dans la forme espacée. Lancée directement, tenant:users:import renvoie
null sur getInput('tenant').
Hors de la commande — dans un écouteur d'événement, par exemple —
Application::getCommandChain() rend le même objet pendant l'exécution.
Les options de l'application (-v, --env) restent acceptées à n'importe quel
niveau. Côté tests, ApplicationTester suit, parce que ArrayInput accepte
désormais des valeurs positionnelles qui remplissent les arguments dans l'ordre.
Un arbre entier dans une classe
Symfony 8.1 avait déjà permis de définir des commandes comme méthodes d'une classe. La 8.2 complète le dispositif :
#[AsCommand('tenant', description: 'Manages tenants', options: [
new InputOption('name', null, InputOption::VALUE_REQUIRED, 'The tenant name'),
])]
class TenantCommands
{
#[AsCommand('users:import', description: 'Imports users from a CSV file')]
public function importUsers(
CommandChain $chain,
#[Argument] string $file,
#[Option] bool $dryRun = false,
): int { }
}Cette classe enregistre deux commandes : le groupe tenant, sans code, dont la
description et l'option --name viennent de l'attribut, et
tenant:users:import.
L'entrée options: n'est d'ailleurs pas réservée aux groupes : une commande
invocable peut désormais déclarer ses options là plutôt qu'en paramètres de
méthode avec #[Option], et les lire depuis l'objet d'entrée comme dans une
classe Command classique.
À retenir
Le gain est pour l'outillage interne : un CLI d'administration à trois niveaux redevient lisible, avec l'aide et la complétion à chaque étage. Symfony 8.2 n'est pas encore sortie — c'est la série des nouveautés annoncées pendant son développement.