Playing with a Custom Console Command

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

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

Login Subscribe

Let's make our new console command sing! Start by giving it a better description: "Cast a random spell!":

... lines 1 - 11
class RandomSpellCommand extends Command
{
... lines 14 - 15
protected function configure()
{
$this
->setDescription('Cast a random spell!')
... lines 20 - 21
;
}
... lines 24 - 41
}

For the arguments and options, these describe what you can pass to the command. Like, if we configured two arguments, then we could pass two things, like foo and bar after the command. The order of arguments is important. Options are things that start with --. Some have values and some don't.

Configuring Arguments & Options

Anyways, let's add one argument called your-name. The argument name is an internal key: you won't see that when using the command. Give it some docs: this is "your name". Let's also add an option called yell so that users can type --yell if they want us to scream the spell at them in uppercase:

... lines 1 - 11
class RandomSpellCommand extends Command
{
... lines 14 - 15
protected function configure()
{
$this
->setDescription('Cast a random spell!')
->addArgument('your-name', InputArgument::OPTIONAL, 'Your name')
->addOption('yell', null, InputOption::VALUE_NONE, 'Yell?')
;
}
... lines 24 - 41
}

There are more ways to configure this stuff - like you can make an argument optional or required or allow the --yell flag to have a value... but you get the idea.

Executing the Command

When someone calls our command, Symfony will run execute(). Let's rename the variable to $yourName and read out the your-name argument:

... lines 1 - 11
class RandomSpellCommand extends Command
{
... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$yourName = $input->getArgument('your-name');
... lines 29 - 40
}
}

So if the user passes a first argument, we're going to get it here and then, if we have a name, let's say Hi! and then $yourName:

... lines 1 - 11
class RandomSpellCommand extends Command
{
... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$yourName = $input->getArgument('your-name');
if ($yourName) {
$io->note(sprintf('Hi %s!', $yourName));
}
... lines 33 - 40
}
}

Cool! For the random spell part, I'll paste some code to get it:

... lines 1 - 11
class RandomSpellCommand extends Command
{
... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$yourName = $input->getArgument('your-name');
if ($yourName) {
$io->note(sprintf('Hi %s!', $yourName));
}
$spells = [
'alohomora',
'confundo',
'engorgio',
'expecto patronum',
'expelliarmus',
'impedimenta',
'reparo',
];
$spell = $spells[array_rand($spells)];
... lines 45 - 52
}
}

Let's check to see if the user passed a --yell flag: if we have a yell option, then $spell = strtoupper($spell):

... lines 1 - 11
class RandomSpellCommand extends Command
{
... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$yourName = $input->getArgument('your-name');
if ($yourName) {
$io->note(sprintf('Hi %s!', $yourName));
}
$spells = [
... lines 35 - 41
];
$spell = $spells[array_rand($spells)];
if ($input->getOption('yell')) {
$spell = strtoupper($spell);
}
... lines 49 - 52
}
}

Finally, we can use the $io variable to output the spell. This is an instance of SymfonyStyle: it's basically a set or shortcuts for rendering things in a nice way, asking the user questions, printing tables and a lot more. Let's say $io->success($spell):

... lines 1 - 11
class RandomSpellCommand extends Command
{
... lines 14 - 24
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$yourName = $input->getArgument('your-name');
if ($yourName) {
$io->note(sprintf('Hi %s!', $yourName));
}
$spells = [
... lines 35 - 41
];
$spell = $spells[array_rand($spells)];
if ($input->getOption('yell')) {
$spell = strtoupper($spell);
}
$io->success($spell);
return 0;
}
}

Done! Let's try it! Back at your terminal, start by running the command, but with a --help option:

php bin/console app:random-spell --help

This tells us everything about our command: the argument, --yell option and a bunch of other options that are built into every command. Try the command with no flags:

php bin/console app:random-spell

Hello random spell! Try with a name argument:

php bin/console app:random-spell Ryan

Hi command! And of course we can pass --yell

php bin/console app:random-spell Ryan --yell

to tell it to scream at us.

There are many more fun things you can do with a command, like printing lists, progress bars, asking users questions with auto-complete and more. You'll have no problems figuring that stuff out.

Commands are Services

Oh, but I do want to mention one more thing: a command is a good normal, boring service. I love that! So what if we needed to access another service from within a command? Like a database connection? Well... it's the same process as always. Let's log something.

Add a public function __construct with one argument LoggerInterface $logger:

... lines 1 - 4
use Psr\Log\LoggerInterface;
... lines 6 - 12
class RandomSpellCommand extends Command
{
... lines 15 - 17
public function __construct(LoggerInterface $logger)
{
... lines 20 - 22
}
... lines 24 - 64
}

I'll use my new PhpStorm shortcut - actually I need to hit Escape first - then press Alt+Enter and go to "Initialize properties" to create that property and set it:

... lines 1 - 12
class RandomSpellCommand extends Command
{
... line 15
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
... lines 21 - 22
}
... lines 24 - 64
}

But there is one unique thing with commands. The parent Command class has its own constructor, which we need to call. Call parent::__construct(): we don't need to pass any arguments to it:

... lines 1 - 12
class RandomSpellCommand extends Command
{
... lines 15 - 17
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
parent::__construct();
}
... lines 24 - 64
}

I can't think of any other part of Symfony where this is required - it's a quirk of the command system. Anyways, right before we print the success message, say $this->logger->info() with: "Casting spell" and then $spell:

... lines 1 - 12
class RandomSpellCommand extends Command
{
... lines 15 - 33
protected function execute(InputInterface $input, OutputInterface $output): int
{
... lines 36 - 52
$spell = $spells[array_rand($spells)];
if ($input->getOption('yell')) {
$spell = strtoupper($spell);
}
$this->logger->info('Casting spell: '.$spell);
... lines 60 - 63
}
}

Let's make sure this works! Run the command again.

php bin/console app:random-spell Ryan --yell

Cool, that still works. To see if it logged, we can check the log file directly:

tail var/log/dev.log

I'll move this up a bit... there it is!

Next, let's "make" one more thing with MakerBundle. We're going to create our own Twig filter so we can parse markdown through our caching system.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.8", // 1.8.1
        "sensio/framework-extra-bundle": "^5.5", // v5.5.4
        "sentry/sentry-symfony": "^3.4", // 3.4.4
        "symfony/asset": "5.0.*", // v5.0.8
        "symfony/console": "5.0.*", // v5.0.8
        "symfony/debug-bundle": "5.0.*", // v5.0.8
        "symfony/dotenv": "5.0.*", // v5.0.8
        "symfony/flex": "^1.3.1", // v1.9.10
        "symfony/framework-bundle": "5.0.*", // v5.0.8
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/profiler-pack": "*", // v1.0.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/var-dumper": "5.0.*", // v5.0.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.7.3
        "symfony/yaml": "5.0.*" // v5.0.8
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.15", // v1.15.0
        "symfony/profiler-pack": "^1.0" // v1.0.4
    }
}