Aujourd’hui, nous allons voir comment créer nos propres commandes console pour Symfony2. Si vous utilisez déjà ce framework, vous en avez probablement déjà utilisé quelques unes. S’il vous est arrivé de lancer une console et d’y taper par exemple
<br />
php app/console doctrine:schema:update<br />
ou bien
<br />
php app/console cache:clear<br />
alors vous avez utilisé le système de commandes console de Symfony2. Ces commandes sont très utiles, et permettent de simplifier pas mal de choses. La première commande permet de mettre à jour une entité qui a été modifié du côté de la base de données, et la seconde permet de vider le cache. Mais comment faire pour écrire nos propres commandes si l’on souhaite nous aussi automatiser des tâches pénibles, récurrentes ou difficiles à réaliser à la main ?
Dans l’exemple qui va suivre, nous allons écrire une commande console pour automatiser la création d’un contrôleur. La structure est en effet toujours la même, seul change, à peu de choses prêt, le nom du contrôleur avant de pouvoir réellement commencer à coder, bref on souhaite éviter d’écrire toujours la même chose et va écrire une commande pour s’en occuper pour nous.
Dans cet exemple donc, nous allons automatiser la création d’un contrôlleur très simple, qui va dépendre de 3 paramètres :
Rentrons dans le vif du sujet. Commençons par créer le bundle qui va accueillir notre commande, par une commande console dont nous allons par la suite nous inspirer du principe :
<br />
php app/console generate:bundle --namespace=KeiruaProd/CommandBundle<br />
Une fois le bundle créé, ajoutez dedans un répertoire Command et placez-y un fichier ControllerGeneratorCommand.php. Ce fichier va accueillir le code de notre nouvelle commande. Commençons par y placer le minimum vital pour une commande.
<br />
<?php
namespace KeiruaProd\CommandBundle\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
class ControllerGeneratorCommand extends ContainerAwareCommand { protected function configure() { $this->setName(‘keiruaprod:generate’);<br /> }</p> <p> protected function interact(InputInterface $input, OutputInterface $output)<br /> {<br /> }</p> <p> protected function execute(InputInterface $input, OutputInterface $output)<br /> {<br /> }<br /> }<br /> </code>
On crée une classe ControllerGeneratorCommand, héritée de ContainerAwareCommand, qui contient 3 méthodes. La première, configure, permet de configurer les informations sur la commande. Pour le moment, seul son nom est renseigné, mais c’est la dedans que nous allons par la suite configurer les paramètres d’entrée de notre fonction, sa description, ainsi qu’un message d’aide.
La seconde, interact, permet d’interagir avec l’utilisateur. Cela permet de demander à l’utilisateur des informations sur les champs s’il ne pas pas fait directement en lançant la commande avec des arguments, ou bien de lui demander plus d’informations. Les paramètres $input et $output servent à réaliser les entrées/sorties avec la console.
La dernière méthode, execute, sert à réaliser l’action qu’est censée réaliser votre commande une fois qu’elle dispose de toutes les informations.
A partir de cela, lancez une console et placez-vous dans votre application Symfony, puis faites
<br />
php app/console<br />
Cette commande permet de lister les commandes disponibles. Dans la liste, vous pouvez voir que la commande keiruaprod:generate est disponible. Vous pouvez même l’exécuter, mais évidemment, il ne se passera rien, car les méthodes interact et execute sont vides. Avant de les remplir, commençons par donner plus d’informations dans la méthode configure.
<br />
protected function configure()<br />
{<br />
$this->setName('keiruaprod:generate')<br />
->setDefinition(array(<br />
new InputOption('controller', '', InputOption::VALUE_REQUIRED, 'Le nom du controller a creer'),<br />
new InputOption('bundle', '', InputOption::VALUE_REQUIRED, 'Le bundle dans lequel creer le controlleur'),<br />
new InputOption('basecontroller', '', InputOption::VALUE_REQUIRED, 'S\'il faut ou non heriter du controlleur de base de Symfony2')<br />
))<br />
->setDescription('Genere le code de base pour commencer a utiliser un controlleur')<br />
->setHelp('Cette commande vous permet de facilement generer le code necessaire pour commencer a travailler avec un controlleur. N\'hesitez pas a vous en servir quand vous avez besoin d\'en creer un !')<br />
;<br />
}<br />
Nous avons laissé setName, et nous avons appelé d’autres méthodes. setDefinition nous permet de definir quelles sont les options de notre commande, leur importance, ainsi qu’une description.
setDescription nous permet de specificier le texte à afficher à côté de notre commande dans la liste fournie par php app/console. Quand à setHelp, je vous laisse le découvrir en tapant dans votre invite de commande
<br />
php app/console keiruaprod:generate --help<br />
He oui, notre commande possede un peu plus d’explications désormais, et les paramètres utilisés sont décrits à l’utilisateur. Sympa non ? Bon, le truc pénible quand même, c’est que pour le moment, notre commande ne fait rien.
Créons le modèle des fichiers que nous allons générer dans le fichier Resources/views/ControllerCommande/Controller.php.twig
<br />
<?php
namespace \Controller;
class Controller { } </code></p> <p>Le template est très simple, il ne contient même pas de méthodes à l’intérieur. Ce sera à nous de les ajouter par la suite. Le but désormais va être de générer ce fichier à partir des informations fournies par l’utilisateur. Une bonne partie sont donnés directement en paramètres de la commande, mais ils nous faut par exemple faire quelques opérations pour obtenir le namespace à partir du nom du bundle. Rien de bien compliqué toutefois.</p> <p>Avant d’écrire le code de génération, il nous faut lire les informations. En effet, l’utilisateur ne rentre pas nécessairement tous les paramètres directement en arguments de la commande. Nous allons concevoir un générateur intéractif, où l’utilisateur peut, au cours de l’exécution de la commande, spécifier ses paramètres.<br /> Commençons par ajouter les espaces de noms nécessaires au sommet du fichier :<br /> <code lang=”php”><br /> use Symfony\Component\Console\Command\Command;<br /> use Symfony\Component\Console\Input\InputOption;<br /> use Symfony\Component\Console\Input\InputArgument;<br /> use Symfony\Component\Console\Input\InputInterface;<br /> use Symfony\Component\Console\Output\OutputInterface;<br /> use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;<br /> use Sensio\Bundle\GeneratorBundle\Command\Helper\DialogHelper;<br /> </code>
puis mettez à jour le code de la fonction interact :
<br />
protected function interact(InputInterface $input, OutputInterface $output)<br />
{<br />
// On affiche quelques infos<br />
$dialog = $this->getDialogHelper ();<br />
$output->writeln(array(<br />
'',<br />
' Bienvenue dans le generateur de controlleurs',<br />
'',<br />
'Cet outil va vous permettre de generer rapidement votre controlleur',<br />
'',<br />
));</p>
<p> // On récupère les informations de l'utilisateur<br />
$controller = $dialog->ask(<br />
$output,<br />
$dialog->getQuestion('Nom du controlleur', $input->getOption('controller')),<br />
$input->getOption('controller')<br />
);</p>
<p> $basecontroller = $input->getOption('basecontroller');<br />
if (!$basecontroller && !$dialog->askConfirmation($output, $dialog->getQuestion('Voulez vous que le bundle etende le controlleur de base de Symfony2 ?', 'yes', '?'), true)) {<br />
$basecontroller = false;<br />
}</p>
<p> $bundleName = $dialog->ask(<br />
$output,<br />
$dialog->getQuestion('bundle', $input->getOption('bundle')),<br />
$input->getOption('bundle')<br />
);</p>
<p> // On sauvegarde les paramètres<br />
$input->setOption('controller', $controller);<br />
$input->setOption('basecontroller', $basecontroller);<br />
$input->setOption('bundle', $bundleName);<br />
}</p>
<p>protected function getDialogHelper()<br />
{<br />
$dialog = $this->getHelperSet()->get('dialog');<br />
if (!$dialog || get_class($dialog) !== 'Sensio\Bundle\GeneratorBundle\Command\Helper\DialogHelper') {<br />
$this->getHelperSet()->set($dialog = new DialogHelper());<br />
}</p>
<p> return $dialog;<br />
}<br />
La méthode getDialogHelper nous permet d’utiliser le DialogHelper du GeneratorBundle pour utiliser la méthode getQuestion. Si vous avez déjà regardé le code de ce générateur, vous verrez sans doute d’où vient une bonne partie de cet article, car la méthodologie utilisée est en effet la même que pour nous. Les informations obtenues sont stockées dans les options d’entrée ($input->setOption(…)) afin d’être récupérées par la suite dans execute. Nous aurions pû faire autrement (en utilisant une variable membre de la classe par exemple), mais cela permet de centraliser les informations d’entrée.
Il est maintenant temps d’écrire la méthode execute, qui va utiliser les informations fournies par l’utilisateur pour construire le fichier du contrôleur à générer et le sauvegarder au bon endroit dans le bundle spécifié.
<br />
protected function execute(InputInterface $input, OutputInterface $output)<br />
{<br />
$dialog = $this->getDialogHelper();</p>
<p> if ($input->isInteractive()) {<br />
if (!$dialog->askConfirmation($output, $dialog->getQuestion('Do you confirm generation', 'yes', '?'), true)) {<br />
$output->writeln('<error>Command aborted</error>');</p>
<p> return 1;<br />
}<br />
}<br />
// On recupere les options<br />
$controller = $input->getOption('controller');<br />
$basecontroller = $input->getOption('basecontroller');<br />
$bundleName = $input->getOption('bundle');</p>
<p> // On recupere les infos sur le bundle nécessaire à la génération du controller<br />
$kernel = $this->getContainer()->get('kernel');<br />
$bundle = $kernel->getBundle ($bundleName);<br />
$namespace = $bundle->getNamespace();<br />
$path = $bundle->getPath();<br />
$target = $path.'/Controller/'.$controller.'Controller.php';</p>
<p> // On génère le contenu du controlleur<br />
$twig = $this->getContainer()->get ('templating');</p>
<p> $controller_code = $twig->render ('KeiruaProdCommandBundle:ControllerCommand:Controller.php.twig',<br />
array (<br />
'controller' => $controller,<br />
'basecontroller' => $basecontroller,<br />
'namespace' => $namespace<br />
)<br />
);</p>
<p> // On crée le fichier<br />
if (!is_dir(dirname($target))) {<br />
mkdir(dirname($target), 0777, true);<br />
}<br />
file_put_contents($target, $controller_code);</p>
<p> return 0;<br />
}<br />
Dans cette méthode, on vérifie que que l’utilisateur souhaite bien générer un contrôleur. Si c’est le cas, à partir du nom du bundle et grâce au composant HttpKernel (obtenu grâce au service ‘kernel’), on obtient les informations sur le namespace du bundle et le chemin dans lequel stocker le fichier que nous allons générer. Ces informations sont fournies au service de template qui se charge de générer le contenu du fichier à l’aide du modèle que nous avons évoqué plus haut. Cela fonctionne comme si nous voulions générer une page HTML avec Twig depuis un controlleur, sauf que pour accéder à Twig nous devons ici utiliser le container de services au lieu de nous en servir directement comme c’est souvent le cas.
Le code du controlleur est enfin sauvegardé dans le fichier adéquat, le répertoire le contenant étant créé si c’est nécessaire.
On a terminé avec le code, plus qu’à vérifier que ça marche.
Maintenant, nous pouvons lancer notre commande depuis une console. Si l’utilisateur ne fournit pas de paramètres, il lui est demandé de préciser les 3 élémentes nécessaires. Par contre, si certaines valeur sont précisés, il lui est demandé de valider que c’est bien les paramètres qu’il souhaite utiliser.
Lancez la commande suivante pour tester que notre commande fait bien ce que souhaitons :
<br />
php app/console keiruaprod:generate --controller=Article --basecontroller=yes --bundle=AcmeDemoBundle<br />
Après execution, vous pouvez vérifier dans src/Acme/DemoBundle/Controller qu’un nouveau fichier ArticleController.php est désormais présent, et que ce controlleur étend bien la classe Controller du FrameworkBundle.
Et voila ! C’est finalement relativement simple, le code de notre commande est très simpliste et vous pouvez désormais le compléter pour ajouter la possibilité de créer dynamiquement des méthodes, pour créer les routes associées…. quoi qu’il en soit nous avons utilisé plusieurs notions que nous n’avions pas encore abordées sur ce blog, comme le container de services, sur lesquelles je reviendrais plus en détail dans les prochains articles.