Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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!

8
Login or Register to join the conversation
Jakub Avatar

Can u tell me few ideas how can i use commands in my project? I cannot imagine how can I really use them

Reply

Hey HudyWeas,

Well, if you don't have any ideas how to use them - probably you don't need them ;) And it's ok, not every project have to use custom command. Well, to get some ideas - you can run bin/console list to see the list of all the available commands out of the box, try to look what they are and maybe you will get with some custom ideas. Also, command are very useful when you need to run some tasks in Cron - you write a command, and tell the Cron job to run it every hour, or day, or month, e.g. you can generate some PDF reports nightly and them to your inbox, or create a custom command migration that you will run on production and that will migrate some heavy data in the background, or even a command that will find all the customers that have active subscription which will be renewed in 7 days and send them email notifications about they will be charged soon, etc.

But once again, you don't have to use them in your project if you don't need :)

Cheers!

Reply
Jakub Avatar

Thanks for your answer. Im not doing any project right now. By "my" I meant a project hypothetically. My point is that it's hard to get possible ideas how I can use commands if I don't know real potential of them. This example from video was really easy, it shows bases of commands but I missed some real app examples how really commands may be used

Reply

Hey HudyWeas,

Ah, yes, I see... I hope a few my examples above helps ;)

Cheers!

Reply
Antoine R. Avatar
Antoine R. Avatar Antoine R. | posted 2 years ago

Note : in commands, the default logger is $consoleLogger. You can check this by throwing an error in your command and look at which channel is used. So if you inject a logger in your command, it is better to use LoggerInterface $consoleLogger

Reply

Hey Antoine R.!

An, that's an excellent point! For others reading this, the importance of that is that you may have your "logger" channel logger configured to save those logs in different ways than you main logger :).

Cheers!

Reply
Default user avatar
Default user avatar Boran Alsaleh | posted 2 years ago

Hi, is the Console component a Command design pattern? If yes, is there ability to undo command in that component?

Reply

Hey Boran Alsaleh

That's a really good question. In my opinion, yes, the Console component implements the command pattern. All the console commands you create are handled the same.

About the undo action. I believe you can create an abstract AbstractUndoCommand class which will force all of its derivatives to implement the undo() method, it should also check for a flag to determine if you want to execute the undo method or just run the command. So, whenever you create a command that it's "undoable", it should extend from that abstract class and implement both method execute and undo

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "sentry/sentry-symfony": "^4.0", // 4.0.3
        "symfony/asset": "5.0.*", // v5.0.11
        "symfony/console": "5.0.*", // v5.0.11
        "symfony/debug-bundle": "5.0.*", // v5.0.11
        "symfony/dotenv": "5.0.*", // v5.0.11
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.0.*", // v5.0.11
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/profiler-pack": "*", // v1.0.5
        "symfony/routing": "5.1.*", // v5.1.11
        "symfony/twig-pack": "^1.0", // v1.0.1
        "symfony/var-dumper": "5.0.*", // v5.0.11
        "symfony/webpack-encore-bundle": "^1.7", // v1.8.0
        "symfony/yaml": "5.0.*" // v5.0.11
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.15", // v1.23.0
        "symfony/profiler-pack": "^1.0" // v1.0.5
    }
}
userVoice