Buy Access to Course
19.

Fun with Commands

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Time to make our command a bit more fun! Give it a description: "Returns some article stats":

47 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 15
protected function configure()
{
$this
->setDescription('Returns some article stats!')
// ... lines 20 - 21
;
}
// ... lines 24 - 45
}

Each command can have arguments - which are strings passed after the command and options, which are prefixed with --, like --option1:

php bin/console article:stats arg1 arg2 --option1 --opt2=khan

Rename the argument to slug, change it to InputArgument::REQUIRED - which means that you must pass this argument to the command, and give it a description: "The article's slug":

47 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 15
protected function configure()
{
$this
->setDescription('Returns some article stats!')
->addArgument('slug', InputArgument::OPTIONAL, 'The article\'s slug')
// ... line 21
;
}
// ... lines 24 - 45
}

Rename the option to format: I want to be able to say --format=json to get the article stats as JSON. Change this to VALUE_REQUIRED: instead of just --format, this means we need to say --format=something. Update its description, and give it a default value: text:

47 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 15
protected function configure()
{
$this
->setDescription('Returns some article stats!')
->addArgument('slug', InputArgument::OPTIONAL, 'The article\'s slug')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'text')
;
}
// ... lines 24 - 45
}

Perfect! We're not using these options yet, but we can already go back and run the command with a --help flag:

php bin/console article:stats --help

Actually, you can add --help to any command to get all the info about it - like the description, arguments and options... including a bunch of options that apply to all commands.

Customizing our Command

Ok, so the configure() method is where we set things up. But execute() is where the magic happens. We can do whatever we want here!

To get the argument value, update the getArgument() call to slug and rename the variable too:

47 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$slug = $input->getArgument('slug');
// ... lines 29 - 44
}
}

Let's just invent some article "data": give this array a slug key and, how about, hearts set to a random number between 10 and 100:

47 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$slug = $input->getArgument('slug');
$data = [
'slug' => $slug,
'hearts' => rand(10, 100),
];
// ... lines 34 - 44
}
}

Clear out the rest of the code, and then add a switch statement on $input->getOption('format'). Here's the plan: we're going to support two different formats: text - don't forget the break - and json:

47 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$slug = $input->getArgument('slug');
$data = [
'slug' => $slug,
'hearts' => rand(10, 100),
];
switch ($input->getOption('format')) {
case 'text':
// ... line 37
break;
case 'json':
// ... line 40
break;
// ... lines 42 - 43
}
}
}

If someone tries to use a different format, yell at them!

What kind of crazy format is that?

47 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$slug = $input->getArgument('slug');
$data = [
'slug' => $slug,
'hearts' => rand(10, 100),
];
switch ($input->getOption('format')) {
case 'text':
// ... line 37
break;
case 'json':
// ... line 40
break;
default:
throw new \Exception('What kind of crazy format is that!?');
}
}
}

Printing Things

Notice that execute() has two arguments: $input and $output:

47 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 6
use Symfony\Component\Console\Input\InputInterface;
// ... line 8
use Symfony\Component\Console\Output\OutputInterface;
// ... lines 10 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output)
{
// ... lines 27 - 44
}
}

Input lets us read arguments and options. And, you can even use it to ask questions interactively. $output is all about printing things. To make both of these even easier to use, we have a special SymfonyStyle object that's full of shortcut methods:

47 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
// ... lines 28 - 44
}
}

For example, to print a list of things, just say $io->listing() and pass the array:

47 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
// ... lines 28 - 34
switch ($input->getOption('format')) {
case 'text':
$io->listing($data);
break;
// ... lines 39 - 43
}
}
}

For json, to print raw text, use $io->write() - then json_encode($data):

47 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
// ... lines 28 - 34
switch ($input->getOption('format')) {
case 'text':
$io->listing($data);
break;
case 'json':
$io->write(json_encode($data));
break;
// ... lines 42 - 43
}
}
}

And... we're done! Let's try this out! Find your terminal and run:

php bin/console article:stats khaaaaaan

Nice! And now pass --format=json:

php bin/console article:stats khaaaaaan --format=json

Woohoo!

Printing a Table

But... this listing isn't very helpful: it just prints out the values, not the keys. The article has 88... what?

Instead of using listing, let's create a table.

Start with an empty $rows array. Now loop over the data as $key => $val and start adding rows with $key and $val:

51 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
// ... lines 28 - 34
switch ($input->getOption('format')) {
case 'text':
$rows = [];
foreach ($data as $key => $val) {
$rows[] = [$key, $val];
}
// ... line 41
break;
// ... lines 43 - 47
}
}
}

We're doing this because the SymfonyStyle object has an awesome method called ->table(). Pass it an array of headers - Key and Value, then $rows:

51 lines | src/Command/ArticleStatsCommand.php
// ... lines 1 - 5
use Symfony\Component\Console\Input\InputArgument;
// ... lines 7 - 11
class ArticleStatsCommand extends Command
{
// ... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
// ... lines 28 - 34
switch ($input->getOption('format')) {
case 'text':
$rows = [];
foreach ($data as $key => $val) {
$rows[] = [$key, $val];
}
$io->table(['Key', 'Value'], $rows);
break;
// ... lines 43 - 47
}
}
}

Let's rock! Try the command again without the --format option:

php bin/console article:stats khaaaaaan

Yes! So much better! And yea, that $io variable has a bunch of other features, like interactive questions, a progress bar and more. Not only are commands fun, but they're super easy to create thanks to MakerBundle.

Oh my gosh, you did it! You made it through Symfony Fundamentals! This was serious work that will seriously unlock you for everything else you do with Symfony! We now understand the configuration system and - most importantly - services. Guess what? Commands are services. So if you needed your SlackClient service, you would just add a __construct() method and autowire it!

Tip

When you do this, you need to call parent::__construct(). Commands are a rare case where there is a parent constructor!

With our new knowledge, let's keep going and start mastering features, like the Doctrine ORM, form system, API stuff and a lot more.

Alright guys, seeya next time!