My Symfony command template

Published on 2019-11-24 • Modified on 2019-11-24

You can generate a command with the maker bundle (php bin/console make:command). I use a slightly different template where the help text is dynamically generated with constants. These are also used as parameters to set the various options, so you don't have duplicated code. Remove the constructor parameter as it is specific to this blog (It's a parameter bound in services.yaml). To run the command, I will use a CommandTester this time instead of using the Process component.


<?php

declare(strict_types=1);

// src/Command/ShowVersionCommand.php

namespace App\Command;

use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * Displays the current application version. This is also the template I copy/paste
 * when having to write a new command.
 */
final class ShowVersionCommand extends BaseCommand
{
    public const CMD = 'version';

    // BaseCommand is an abstract class extending Symfony\Component\Console\Command\Command
    // and contains the project main namespace

    protected static $defaultName = self::NAMESPACE.':'.self::CMD;
    protected static $defaultDescription = 'Shows the current application version number.'; // Symfony 5.3 only
    private string $appVersion;

    public function __construct(string $appVersion)
    {
        $this->appVersion = $appVersion;
        parent::__construct();
    }

    protected function configure(): void
    {
        [$desc, $class] = [self::$defaultDescription, self::class];
        $this
            // InputArgument::REQUIRED / OPTIONAL / IS_ARRAY (3)
            ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description')

            // InputOption::VALUE_NONE / VALUE_REQUIRED / VALUE_OPTIONAL / VALUE_IS_ARRAY / VALUE_NEGATABLE (5)
            ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description')
            ->setHelp(
                <<<EOT
$desc

COMMAND:
<comment>$class</comment>

DEV:
<info>%command.full_name% -vv</info>

PROD:
<info>%command.full_name% --env=prod --no-debug</info>
EOT
            );
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
            $io->note('This note will only be displayed when using at least the verbose mode option for the command "-v"');
        }
        $output->writeln('This line will only be displayed when using at least the verbose mode option for the command "-v"', OutputInterface::VERBOSITY_VERBOSE);
        // snippet L62+9 used in templates/snippet/code/_164.html.twig

        $io->section('Version');
        $io->note(BaseCommand::NAMESPACE.' '.$this->appVersion);
        $io->newLine();
        $io->section('Let test the SymfonyStyle for CLI now, this is a section');
        $io->title('This is a title');
        $io->block('This is a block');
        $io->newLine(2);
        $io->text('This is a text (two new lines before)');
        $io->comment('This is a comment');
        $io->caution('This is a caution');
        $io->error('This is an error');
        $io->info('This is an info');
        $io->note('This is a note');
        $io->listing(['this', 'is', 'a', 'listing']);
        $io->table(
            ['This', 'is', 'a', 'table'],
            [
                ['Cell 1-1', 'Cell 1-2', 'Cell 1-3', 'Cell 1-4'],
                ['Cell 2-1', 'Cell 2-2', 'Cell 2-3', 'Cell 2-4'],
                ['Cell 3-1', 'Cell 3-2', 'Cell 2-3', 'Cell 3-4'],
            ]
        );
        $io->horizontalTable(
            ['This is an', 'horizontal Table'],
            [
                ['Cell 1-1', 'Cell 1-2'],
                ['Cell 2-1', 'Cell 2-2'],
                ['Cell 3-1', 'Cell 3-2'],
            ]
        );

        $io->definitionList(
            'This is a definition list',
            ['foo1' => 'bar1'],
            ['foo2' => 'bar2'],
            ['foo3' => 'bar3'],
            new TableSeparator(),
            'with keys => values',
            ['foo4' => 'bar4']
        );

        $io->success('This is a success');
        $io->section("That's all folks!");

        return self::SUCCESS;
    }
}
Bonus, the snippet to run this code: 🎉
<?php

declare(strict_types=1);

namespace App\Controller\Snippet;

use App\Command\ShowVersionCommand;
use SensioLabs\AnsiConverter\AnsiToHtmlConverter;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Process\Process;

/**
 * I am using a PHP trait to isolate each snippet in a file.
 * This code should be called from a Symfony controller extending AbstractController (as of Symfony 4.2)
 * or Symfony\Bundle\FrameworkBundle\Controller\Controller (Symfony <= 4.1).
 * Services are injected in the main controller constructor.
 *
 * @property KernelInterface    $kernel
 * @property ShowVersionCommand $showVersionCommand
 */
trait Snippet58Trait
{
    public function snippet58(): void
    {
        $process = new Process([
            'php',
            $this->kernel->getProjectDir().'/bin/console',
            'strangebuzz:version',
            '--ansi',
        ]);
        $process->run();

        echo (new AnsiToHtmlConverter())->convert($process->getOutput()); // That's it! 😁
    }
}

 Run this snippet  ≪ this.showUnitTest ? this.trans.hide_unit_test : this.trans.show_unit_test ≫  More on Stackoverflow   Read the doc  Random snippet

<?php

declare(strict_types=1);

namespace App\Tests\Controller\Snippets;

use App\Command\BaseCommand;
use App\Command\SendSlackNotificationCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

/**
 * @covers Snippet58Trait
 */
final class Snippet58Test extends KernelTestCase
{
    private ?SendSlackNotificationCommand $sendSlackNotificationCommand;

    protected function setUp(): void
    {
        self::bootKernel();
        $this->sendSlackNotificationCommand = self::$kernel->getContainer()->get(SendSlackNotificationCommand::class);
    }

    /**
     * @covers Snippet58Trait::snippet58
     */
    public function testSnippet58(): void
    {
        $app = new Application(self::$kernel);
        if (!$this->sendSlackNotificationCommand instanceof SendSlackNotificationCommand) {
            throw new \RuntimeException('Commande not found.');
        }
        $app->add($this->sendSlackNotificationCommand);
        $command = $app->find(BaseCommand::NAMESPACE.':'.SendSlackNotificationCommand::CMD);
        $commandTester = new CommandTester($command);
        $message = 'Hello World!';
        $commandTester->execute([
            'command' => $command->getName(),
            'message' => $message,
        ]);
        self::assertStringContainsStringIgnoringCase($message, $commandTester->getDisplay());
    }
}