Understanding the Symfony Console Component

Petre Pătraşc

@Cegeka

All code samples and slides available after presentation

PHP 7  Symfony 3

Components

  • Dependency Injection
  • Event Dispatcher
  • Console
  • etc...

Symfony Console

  • Structure of a Command
  • Input
  • Output
  • Visualizing the Role
  • Case Study

Talk is cheap, show me the code

Hello World Command

petre$ bin/console demo:hello
Hello World!

petre$ bin/console demo:hello "Symfony Bucharest"
Hello Symfony Bucharest!
namespace SymfonyBucharest\UnderstandingConsoleBundle\Command;

use ...

/**
 * Greet a person or group of people.
 */
class HelloWorldCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('demo:hello')
            ->setDescription('Greet a person or group of people')
            ->addArgument(
                 'who', 
                 InputArgument::OPTIONAL, 
                 'The person or group to greet', 
                 'World'
            );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $who = $input->getArgument('who');

        $output->writeln("Hello {$who}!");
    }
}

Writing a Command

  1. Create a PHP class under the Command namespace
  2. Extend ContainerAwareCommand
  3. Overwrite configure()
  4. Overwrite execute($input, $output)

Handling Input

  • Input Arguments
    • doctrine:mig:mig prev
  • Input Options
    • cache:clear -e=prod

Input Arguments

  • Feed input into Command
  • Can have multiple Arguments
  • Some Arguments can be of type array

Input Arguments

$this
    ->setName('demo:hello')
    ->setDescription('Greet a person or group of people')
    ->addArgument(
        'who', # Argument Identifier
        InputArgument::OPTIONAL, # Required/Optional/Array
        'The person or group to greet', # Description
        'World' # Default Value
    );

Input Arguments

petre$ bin/console demo:hello --help
Usage:
  demo:hello [<who>]

Arguments:
  who                   The person or group to greet [default: "World"]

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -e, --env=ENV         The Environment name. [default: "dev"]
      --no-debug        Switches off debug mode.
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Help:
 Greet a person or group of people

Input Options

  • Define multiple flags/modifiers
  • Can use shortcut notation
$this->addOption(
    'greeting', # Option Identifier
    'g',  # Option Shortcut
    InputOption::VALUE_OPTIONAL, # Required/Optional/Array
    'The greeting to use', # Description
    'Hello' # Default Value
);
$who = $input->getArgument('who');
$greeting = $input->getOption('greeting');

$output->writeln("{$greeting} {$who}!");

configure()

execute()

Input Options

petre$ bin/console demo:hello "Symfony București" --greeting "Salut"
Salut Symfony București!

petre$ bin/console demo:hello "Symfony București" -g "Salut"
Salut Symfony București!

Output Options

  • Coloring output
  • Creating custom styles
  • Helpers

Coloring Output

Coloring Output

Some simple styles are built-in

$output->writeln('<info>Information message</info>');
$output->writeln('<comment>A comment</comment>');
$output->writeln('<question>User interaction/Question</question>');
$output->writeln('<error>Errors, exceptions, all that good stuff</error>');

Roll your own

$yoloStyle = new OutputFormatterStyle('yellow', 'green', ['bold']);
$output->getFormatter()->setStyle('yolo', $yoloStyle);

$output->writeln('<yolo>Custom style</yolo>');

Helpers

  • Question
  • Progress Bar
  • Table
  • Fully extensible, add your own

Question Helper

$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('<question>Continue?</question> ', false);

if (true === $helper->ask($input, $output, $question)) {
    return 0;
} else {
    return 1;
}
petre$ bin/console demo:helper:question
The answer to the Ultimate Question of Life, The Universe, and Everything is 42. Correct? no
You need to crunch the numbers again. See you in ten million years.

Progress Helper

petre$ bin/console demo:helper:progress -vvv
  7/10 [===================>--------]  70% 6 secs/9 secs 8.0 MiB
$progress = new ProgressBar($output);
$progress->start(10);

for ($iterator = 0; $iterator < 10; $iterator++) {
    $progress->advance();
    sleep(1);
}

Table Helper

petre$ bin/console demo:helper:table
+--------------------------------------+------------+-----------+---------------------------------+
| ID                                   | First Name | Last Name | Company                         |
+--------------------------------------+------------+-----------+---------------------------------+
| b142579b-9cd1-350e-a8ec-cc43c29718a6 | Lottie     | Yost      | Murray, Hamill and Koch         |
| 06af6dd6-a5f3-3bd4-bfed-1628e26efdfe | Guido      | Boyle     | Gislason-McGlynn                |
| 3b84e7a2-19c3-3e60-948b-203d94ac6b21 | Esta       | Von       | Kiehn-McClure                   |
| e133b0f3-b603-30dd-9c01-bd5a763cf87d | Fern       | Denesik   | Conn, Langosh and Buckridge     |
| 291fdadf-7f13-3667-9c7b-f671e7beb447 | Ilene      | McKenzie  | Botsford, Kautzer and Schneider |
| 76e85603-ad97-3219-baa2-4b934dd79adc | Aisha      | Crooks    | Hudson Group                    |
| c34fbdeb-bc69-3fbc-84a0-16994720e3e1 | Brandt     | Graham    | Metz Group                      |
| 5d76dd0f-112a-394e-80cc-0c97e79f0fff | Dion       | Bergstrom | Luettgen Group                  |
| 238cd2dd-34b1-39c4-9eee-a476d304a26c | Makayla    | Mohr      | Hyatt Ltd                       |
| ceb8362f-e579-388c-a272-dd756408693c | Conor      | Schneider | Schmidt Group                   |
+--------------------------------------+------------+-----------+---------------------------------+

Table Helper

$faker = Factory::create();

$table = (new Table($output))
    ->setHeaders([
        'ID',
        'First Name',
        'Last Name',
        'Company',
    ]);

for ($iterator = 0; $iterator < 10; $iterator++) {
    $table->addRow([
        $faker->uuid,
        $faker->firstName,
        $faker->lastName,
        $faker->company,
    ]);
}

$table->render();

Understand the history behind it

Growing Legacy Codebase

Many CLI scripts, all doing their own thing

Our App

I <3 reporting

I run for 3 days and import 10GB of data

Most CLI applications so far

  • Decoupled from project codebase
  • Perform basic maintenance operations
  • Generate long-running reports
  • No testing/no monitoring

CLI Apps with Console Component

  • Integrated into existing codebase
  • Can execute complex logic
  • Can use other Components (DI, Event Dispatcher)
  • Individual Commands are unit-testable

Integration into Symfony

Creating a Product

Product
Controller

Web User

Product
Service

Product
Repository

Data Store
(DB, API, etc.)

API Consumer

Product API Controller

CLI User

Product
Console Command

  • Validate Input
  • Call Business Logic
  • Return Output

Integration

  • Can access Container
    • Services
    • Parameters
  • Using DI component gives access to other layers
  • Tested Console code = tested production code
  • Forces devs to isolate business logic strictly to business layer

Our App

Possible use cases

  • Integrating with other OS workflows
    • User joins team
      • Create email
      • Create website account
  • Creating admin users securely
  • Expiring Redis/Memcache keys
  • Retrieving data from slow running API
  • Monitoring
  • Workers for processing queues
  • Importing large data sets

Case Study

Context

  • Working on feature with a lot of business value
  • Need to build autocomplete for list of cities
  • Data fetched from internal third-party API
  • API only has listing on data, no searching options
  • API is really slow
    • Really slow
    • 4 seconds for Belgium
    • 144(!) seconds for France
  • No resources available for refactoring.

What we did

  1. Added a provisioning layer in our architecture.
    1. When user gets details of a city, cache it in our infrastructure for 30 days.
    2. Cache data can be searched.
  2. Changed autocomplete logic.
    1. If city is found in cache, fetch details from there.
    2. If not, fetch from backend :(
  3. Create Command to fetch list of cities and "fetch" each of them.
    1. By fetching them all, we cache them all.
  4. Plug Command into pipeline
    1. Continuous Deployment
    2. Cronjob

Results

  • Reduction from 4/144 seconds to 200ms
  • Reuse 90% of our existing code
  • Development took only a few hours
  • Happy users
  • Happy management

Wrap-Up

Reading - Symfony Console

Reading - Linux daemons, process signaling

Code

  • https://github.com/petrepatrasc/understanding-symfony-console

Thank You!
Q&A

Understanding the Symfony Console

By Petre Pătraşc

Understanding the Symfony Console

A presentation for the Symfony Bucharest meet-up.

  • 903