Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Fun with Commands

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

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

... 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":

... 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:

... 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:

... 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:

... 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:

... 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?

... 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:

... 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:

... 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:

... 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):

... 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:

... 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:

... 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!

Leave a comment!

11
Login or Register to join the conversation
Peter-K Avatar
Peter-K Avatar Peter-K | posted 5 years ago

So far good course.

I hope that next course/tutorial will cover audit log, action logs, form dropdown dependency, producing pdfs from templates if possible, testing of controllers and whole site (shifting logic from controllers to services??) updating/overwriting of build-in templates, creating global variables for templates, authorization, role hierarchy etc.

Cant wait for the next video.

Can I find somewhere what is on your todo list with some dates?

1 Reply

Hey Peter,

Thanks for your suggestions, most of them we're going to cover in the upcoming Symfony 4 screencasts, but probably not all of them, at least in the nearest future.

Yes, you can check out upcoming screencasts on this page: https://knpuniversity.com/c... but unfortunately we do not have any dates, but we're trying to stick the order of those upcoming screencasts.

Cheers!

Reply

Thank you ! Small question : For a long time command, is it recommended to set `set_time_limit(0);` ?

Reply

Hey Ahmedbhs,

It might be a good idea, and most probably you already have such config for the php-cli. Not a good idea to do this for php-fpm config, but it might depend on your specific user case.

Though, I'd not recommend you to run your long-time commands too long if we're talking about PHP, because PHP is far from perfect for this, it has a lot of memory leaks, etc. And also performance on long-time commands will be reduced most probably too. Usually, PHP devs make commands that are safe to be run more than once, i.e. the business logic in it will skip already handled cases and continue from the place where it stopped.

I hope this helps!

Cheers!

Reply
Default user avatar

The command works when it is in src/command but it seems to not autowire when i put the command in a vendor package like vendor/name/package/src/Command/MyCommand any idea ?

Reply

Yo Frogg!

You're 100% correct! The src/ directory is special because of some code in the beginning of your config/services.yaml file:


# config/services.yaml
services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/*'
        exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'

Those lines of code are responsible for (A) auto-registering everything in src/ as a service (that's what the second section does) and activating autowiring (and autoconfiguration) for all services registered in this file (that's the first part). That's why, if you put something anywhere outside of src/, this won't work. As a best-practice, if you're building a reusable bundle, you should wire your services manually. Here are some details about that https://symfonycasts.com/screencast/symfony-bundle/bundle-services

I hope that helps! Excellent question!

Cheers!

1 Reply
Default user avatar
Default user avatar Frogg | weaverryan | posted 3 years ago | edited

Thanks for your answer, it works now.
I created a Bundle as you said in your other post:
src/Bundle.php<br />src/Resources/config/services.xml<br />src/DependencyInjection/Extension.php<br />src/Command/Command.php<br />
but it didn't add automatically my bundle in the symfony config/bundle.php
So i checked how FOS bundles did, and i figured out there is a configuration missing to achieve it:

in the bundle composer.json i added:
`{

"name": "froggdev/behat-installer",
"type": "symfony-bundle",
...

}`

And now the command works when the bundle is installed !
Thanks a lot.

1 Reply
Dung L. Avatar
Dung L. Avatar Dung L. | posted 3 years ago

Hello team,

in the "->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'text')" can you please explain the second argument "null" is short hand of the first argument "format" and that one can write short hand "f" such as "->addOption('format', 'f', InputOption::VALUE_REQUIRED, 'The output format', 'text')"

I had to read original doc https://symfony.com/doc/cur... to understand. Thank you for all your works!

Reply

Hey Dung L.!

Sorry for my slow reply! Thanks for posting the tip here :). I don't often use that 2nd argument (the "shortcut"), but it's a really nice one to know.

Cheers!

Reply
Dung L. Avatar

Yep, for learning. Thanks!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.1.4
        "symfony/asset": "^4.0", // v4.0.4
        "symfony/console": "^4.0", // v4.0.14
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.0.14
        "symfony/lts": "^4@dev", // dev-master
        "symfony/twig-bundle": "^4.0", // v4.0.4
        "symfony/web-server-bundle": "^4.0", // v4.0.4
        "symfony/yaml": "^4.0" // v4.0.14
    },
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/maker-bundle": "^1.0", // v1.0.2
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.4
    }
}
userVoice