If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLet'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.
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.
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.
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.
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!
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
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
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!
Hi, is the Console component a Command design pattern? If yes, is there ability to undo command in that component?
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!
// 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
}
}
Can u tell me few ideas how can i use commands in my project? I cannot imagine how can I really use them