Commentaires
Bien penser ses commandes Symfony2
Qu'est-ce qu'une commande ?
Simple question rhétorique ? Pas tant que ça ! Sur Wikipedia il existe 2 définitions du mot Commande rien que dans le domaine de l'informatique. La première est celle d'un design pattern et la seconde nous est plus familière:
on entend par commande un mot qui décrit de manière mnémonique un nom de tâche, qu'il est possible de faire suivre par des paramètres.
Notez bien les mots en gras et gardez-les bien à l'esprit, autant que ce qu'ils impliquent (on va y revenir).
Mais qui Commande ? Et par quel Moyen ?
Ok, remettons-nous dans le contexte au lieu de philosopher. Nous sommes clairement aux manettes de toute Commande donc faisons le maximum pour rendre leur utilisation agréable et efficace :)
Question moyens, du côté de Symfony c'est noël avant l'heure, car depuis la version 2.3 le composant Console a bénéficié de nombreuses améliorations.
C'est l'intention qui compte
Revendiquons notre statut de first class citizens pour chacune de nos commandes.
Mesurer les progrès
$progress->setEmptyBarCharacter('.');
$progress->setBarCharacter("\xf0\x9f\x8e\x84");
$progress->setProgressCharacter("\xf0\x9f\x8e\x85");
$progress->setFormat('<info>%current%</info> <info>[</info>%bar%<info>]</info> <comment>Elapsed: %elapsed%</comment>');
C'est le moment de passer à table !
$table = $this->getHelperSet()->get('table');
$christmasTreeEmoji = "\xf0\x9f\x8e\x84";
$santaEmoji = "\xf0\x9f\x8e\x85";
$table->setCrossingChar($christmasTreeEmoji);
$table->setHorizontalBorderChar($christmasTreeEmoji);
$table->setVerticalBorderChar($christmasTreeEmoji);
$table->setHeaders(array('Person', 'Present'));
$table->addRow(array('Oliver', 'Twist lessons'));
$table->addRow(array('Hugo', 'Lin'));
$table->addRow(array('Blanket', 'Neverland'));
$table->addRow(array('Jean Marc', 'Cigar'));
$table->render($output);
$output->writeln("\n");
$table->setLayout(TableHelper::LAYOUT_BORDERLESS);
$table->setHorizontalBorderChar($santaEmoji);
$table->render($output);
$output->writeln("\n");
$table->setLayout(TableHelper::LAYOUT_COMPACT);
$table->render($output);
Des questions ?
Forcément... et nous avons le droit d'en poser à l'utilisateur de notre commande. Le DialogHelper
est déjà disponible depuis Symfony 2.2 mais tournons-nous vers l'avenir... je vous invite à lire les slides de la SymfonyCon de Fabien au sujet du futur QuestionHelper
.
Être de sortie... et bavard (ou pas)
Profiter du logger
Depuis Symfony 2.4 le logger est parfaitement intégré dans les commandes, de sorte que, selon le niveau de verbosité passé en argument à la commande, si:
OutputInterface::VERBOSITY_NORMAL
alors tous les logs de niveau WARNING et supérieur s'affichent,
OutputInterface::VERBOSITY_VERBOSE
(-v) les logs de niveau NOTICE et supérieur s'affichent,
OutputInterface::VERBOSITY_VERY_VERBOSE
(-vv) les logs de niveau INFO et supérieur s'affichent,
OutputInterface::VERBOSITY_DEBUG
(-vvv) les logs de niveau DEBUG et supérieur s'affichent, en l'occurence tous les logs.
Par exemple le code suivant:
$logger = $this->getContainer()->get('logger');
$presents = array('presents' => array('cigar', 'lin', 'Twist Lessons', 'Neverland'));
$logger->debug('This is a debug message', $present);
$logger->info('This is an info message', $presents);
$logger->notice('This is an notice message', $presents);
$logger->warn('This is a warning message', $presents);
$logger->err('This is an error message', $presents);
$logger->crit('This is a critical message', $presents);
$logger->alert('This is an alert message', $presents);
$logger->emerg('This is an emergency message', $presents);
Produira en étant invoqué ainsi:
$ ./app/console namespace:command -vvv
La sortie suivante :
Remarque: Si le formateur de logs proposé par défaut ne vous convient pas, vous pouvez bien entendu configurer Monolog pour utiliser le votre. C'est expliqué avec deux approches différentes ici et ici.
Sorties personnalisées en fonction du niveau de verbosité
Vous disposez désormais de 4 méthodes explicites. Un exemple vaut mieux que de longs discours:
if ($output->isVerbose()) {
// ...
}
if ($output->isQuiet()) {
// ...
}
if ($output->isVeryVerbose()) {
// ...
}
if ($output->isDebug()) {
// ...
}
Remarque: À priori vous n'aurez pas souvent besoin d'encadrer des blocs de code par ce genre de conditions au sein d'une commande. Si c'est le cas alors posez-vous la question de la quantité de code que votre commande contient. Tout comme un contrôleur, celle-ci a vocation à se servir de toute la logique métier écrite et la manipuler. Une commande doit donc faire peu de lignes de code.
La bien nommée
Nommer une commande est toujours un casse tête. Une simple invocation de la console Symfony nous le prouve et sème déjà la confusion dans nos esprits.
La première bonne pratique, vu que les commandes sont organisées grâce à un système d'espace de nommage, consiste à regrouper toutes les commandes de notre application sous un namespace commun, par exemple acme
.
Cela signifie aussi que vous pouvez faire le choix de créer une console dédiée à votre application et qui encapsule uniquement les commandes dont le namespace commence par acme
. De cette manière votre commande s'appelle déjà acme et vous listez uniquement les commandes utiles à votre application, la valeur ajoutée en quelque sorte.
Ignorons donc désormais ce préfixe acme
pour nous concentrer sur la suite. La première chose à savoir en matière de nom de commande:
Il n'existe pas de règle universelle !
Cela dit... 2 pistes s'offrent à vous pour avoir une certaine cohérence. Vous pouvez mixer les deux (c'est le cas dans la console Symfony).
Action first
Avec parcimonie
Cette approche part de l'intention comme c'est le cas de beaucoup de commandes UNIX (cut, grep, ls, cd, ...). C'est ainsi que dans la console Symfony nous avons init:acl
et toutes les commandes generate:*
. Au premier niveau de namespace elles doivent cependant rester peu nombreuses. Prenez pour exemple le ruban de commandes de vos applications graphiques: vous avez tous remarqué que toutes les entrées de menu étaient des noms pratiquement ("File", "View", "Help", ...), sauf généralement une qui est "Edit" ? :)
Sans être borné
Bien entendu si votre application manipule beaucoup de données de manière abstraite, vous faites partie de l'exception et personne ne vous en voudra d'avoir uniquement des verbes en premier car l'action prime sur la donnée manipulée.
Subject first
Un vague lien de parenté ?
Ici le sujet est au cœur de nos préoccupations. Si nous avons un client dans notre application alors c'est très simple toutes les commandes liées à ce client doivent être dans le namespace acme:customer:*
.
Ce qui est très intéressant avec cette méthode de nommage, c'est que l'on peut adopter un mode de fonctionnement orienté ressource. Ça ne vous rappelle rien ?
Ha oui mais bien sûr, REST ! Si vos commandes sont un shell pour des API REST cette approche est particulièrement intéressante.
Des possibilités intéressantes offertes
Avec certes un peu de travail on peut facilement mettre en place une console consciente du contexte (ou plutôt du namespace), à la manière du shell des routeurs CISCO™ par exemple. Vous allez me dire "cisse quoi" ? Bon ok un exemple vous parlera plus:
C'est très utile lorsqu'on commence à avoir un arbre de commandes fourni car tout est question de contexte: pourquoi s'encombrer l'esprit à lister une tonne de commandes qui n'ont rien à avoir avec notre intention initiale ?
Je suis persuadé que ce concept ferait adopter le mode shell de la console Symfony (./app/console -s
) à beaucoup d'entre nous ;)
Rendons-nous service
Depuis Symfony 2.4 vous pouvez enregistrer vos commandes via l'injection de dépendance !
Il suffit de la déclarer en tant que service taggé. Le tag est console.command
.
Heureux événements
Depuis Symfony 2.3 il existe désormais 3 événements sur lesquels nous pouvons nous brancher pour par exemple décorer notre sortie:
ConsoleEvents::COMMAND
ConsoleEvents::TERMINATE
ConsoleEvents::EXCEPTION
Vous pouvez vous brancher sur ces événements très facilement en suivant ce guide.
The future is now!
Nous avons balayé les killer features des commandes Symfony ainsi que les bonnes pratiques. Mettez à profit ces fêtes de fin d'année pour les adopter si ce n'est pas déjà le cas.
Vu que Noël approche c'est le moment de faire votre liste. Qu'aimeriez-vous avoir dans vos commandes en 2014 ? Pas d'idées ? Aller j'ouvre la voie, je me pose moi-même quelques questions que j'aimerais partager avec vous.
Universal shell ?
Ne serait-il pas intéressant de toujours développer un set de commandes cohérentes adossé aux interfaces de nos projets web ? Nous le faisons tous plus ou moins, mais combien parmi nous peuvent se vanter de pouvoir contrôller aussi bien via un shell que via une interface web 100% du métier de notre projet applicatif ?
Découverte automatique de commandes ?
Cette question pourrait être liée à la précédente... imaginons que nous avons une API REST, comme on en voit partout de nos jours. À partir du moment où elle expose ses services (via WADL ?, Github Like ?, un ESB ?, ...), ne pourrions-nous pas construire une console capable de consommer ces services... et surtout qui à la manière du modèle de distribution web serait en permanence à jour puisque chaque nouvelle évolution est délivrée côté serveur uniquement ?
Une console dédiée, sans les commandes symfony ?
Aviez-vous déjà lu cette recette ? :)
Profiler ?
Jusqu'ici le profiler Symfony est réservé aux controleurs. Ne serait-il pas pratique de se brancher sur le profiler afin d'apprécier les performances des commandes que nous écrivons ?