Symfony Console jako první rande se Symfony

V článku si ukážeme možnosti Symfony Console. Je to samostatná komponenta s minimem závislostí, takže ji lze velmi snadno začít používat v existující aplikaci. Považuji to za super způsob, jak se nenásilně seznámit s ekosystémem Symfony.

Seriál: Symfony po krůčkách (18 dílů)

  1. Symfony po krůčkách – Event Dispatcher 30.11.2015
  2. Symfony Console jako první rande se Symfony 7.12.2015
  3. Symfony po krůčkách – Filesystem a Finder 14.12.2015
  4. Symfony po krůčkách – Paralýza možností? OptionsResolver tě zachrání 21.12.2015
  5. Symfony po krůčkách – spouštíme procesy 4.1.2016
  6. Symfony po krůčkách – Translation – překlady jednoduše 11.1.2016
  7. Symfony po krůčkách – Validator (1) 18.1.2016
  8. Symfony po krůčkách – Validator (2) 25.1.2016
  9. Symfony po krůčkách – Routing 1.2.2016
  10. Symfony po krůčkách – MicroKernel 9.2.2016
  11. Konfigurujeme Symfony pomocí YAMLu 16.2.2016
  12. Symfony po krůčkách – oblékáme MicroKernel 23.2.2016
  13. Symfony po krůčkách – ClassLoader 29.2.2016
  14. Symfony po krůčkách – Twig 8.3.2016
  15. Symfony po krůčkách – Twig II. 15.3.2016
  16. Symfony po krůčkách – DomCrawler a CssSelector 23.3.2016
  17. Symfony po krůčkách – HTTP fundamentalista 12.4.2016
  18. Symfony po krůčkách – ušli jsme pořádný kus 19.4.2016

I když je článek cílený zejména na ty, kteří se Symfony Console zatím nemají mnoho zkušeností, tak věřím, že i zkušení „konzoláři“ se v něm dozví něco nového.

Téměř každá větší PHP aplikace potřebuje skripty spouštěné z konzole. Typicky to jsou různé importy, crony a podobně. A i když je zbytek aplikace napsaný kvalitně a udržovatelně, tak tyto skripty bývají neudržovatelná změť php a bash skriptů.

Vzpomínáte, jak dříve vypadaly PHP aplikace? clanek.php, kategorie.php a podobně. Oblíbenou kratochvílí bylo zapomenutí kontroly oprávnění uživatele v některém z těchto vstupních bodů. Zlepšilo se to využíváním vzoru Front Controller, kdy jsou všechny požadavky směrované na index.php, odkud se teprve volají jednotlivé akce. Lze říci, že Symfony Console je implementace Front Controller pro konzolové skripty.

Se Symfony Console jste se mohli setkat již dříve například v Composeru, Drupalu, phpdocumentoru a dalších. Například do Nette ji integruje Kdyby/Console. Troufám si tvrdit, že pro řešení konzolové části v PHP nemá dnes smysl používat cokoliv jiného.

Instalace

Článek předpokládá, že již znáte Composer a ve svém projektu ho používáte. Pokud ne, tak by to měl být první krok.

S naší aplikací začneme tím, že do composer.json přidáme jednoduchý autoload:

"autoload": {
    "psr-4": {
        "App\\": "src"
    }
}

A nainstalujeme Symfony Console:

composer require symfony/console

Dalším krokem je vytvoření vstupního bodu do aplikace, v našem případně souboru cli.php v rootu projektu:

<?php //cli.php
require_once __DIR__ . '/vendor/autoload.php';

use Symfony\Component\Console\Application;

$console = new Application('Symfony Console demo for Zdroják.cz', '3.7.4');
$console->run();

Že vše funguje správně, ověříme z konzole pomocí: php cli.php, měli bychom dostat následující výstup:

console01-app

Ukázkovou aplikaci, kterou v průběhu článku vytvoříme, najdete na githubu. Pokud by vám nějaký krok nefungoval, tak se můžete podívat do historie, jak změna měla vypadat.

  • Tip 1: Na Windows používám cmder, se kterým je práce v konzoli příjemnější.
  • Tip 2: Pokud se vám v konzoli špatně vypisují české znaky, tak zavolejte chcp 65001
  • Tip 3: V cmderu je možné si volání chcp 65001 přidat do vendor\init.bat, takže se při otevření nové konzole zavolá automaticky.

Vytvoření příkazu

Při použití Symfony Console se jednotlivé příkazy vytvářejí jako potomci třídy Command. Vytvoříme tedy jednoduchý příkaz HelloCommand ve složce src/Command:

<?php // src/Command/HelloCommand.php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class HelloCommand extends Command
{
    protected function configure()
    {
        $this
            ->setName('zdrojak:hello')
            ->setDescription('Jednoduchy Hello World!');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln('Hello World from Symfony Console!');
    }
}
  • Pomocí setName() určujeme jméno příkazu pro spouštění. Je běžné používat notaci kategorie:prikaz, díky čemuž se dostupné příkazy vypisují vizuálně oddělené, což se hodí, jakmile bude příkazů v aplikaci více (třeba Doctrine jich v Symfony přidává přes dvacet).
  • Metoda configure() se volá při inicializaci aplikace, neměla by tedy dělat déle trvající činnosti.
  • Metoda execute() je zavolána při spuštění příkazu a dostane naparsované vstupní parametry v $input (ukážeme si později) a výstup $output, do kterého zapisujeme. Pro výstup do konzole se nepoužívá žádné echo.

Dále je potřeba příkaz integrovat v cli.php pomocí: $console->add(new App\Command\HelloCommand());

Po zavolání php cli.php se přidaný příkaz vypíše mezi možnostmi a můžete ho spustit pomocí php cli.php zdrojak:hello:

console02-hello

Parametry

Chování příkazu je možné ovlivnit předáním parametrů dvou typů:

  • Argument může být jedna nebo více hodnot. Záleží na jejich pořadí, nepovinné mohou být až na konci podobně jako u PHP funkcí. Příkladem je název souboru, který spouštíme v php cli.php (cli.php je argument).
  • Option, neboli přepínače, jsou vždy volitelné parametry, nezáleží na jejich pořadí. Když se vrátím k příkladu s PHP, tak přepínač je -l pro kontrolu syntaxe (php -l cli.php).

Použití ukážu na následujícím příkladu. Přidal jsem jeden argument s výchozí hodnotou World. První přepínač backwards nepotřebuje hodnotu a nabízí zkrácené volání jako -b. Druhý přepínač greeting hodnotu vyžaduje (pokud je použit).

class HelloCommand extends Command
{
    protected function configure()
    {
        $this
            ->setName('zdrojak:hello')
            ->setDescription('Jednoduchy Hello World!')
            ->addArgument('name', null, 'Koho zdravíme?', 'World')
            ->addOption('backwards', 'b', null, 'Pozpátku?')
            ->addOption('greeting', null, InputOption::VALUE_REQUIRED, 'Pozdrav', 'Hello');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $message = sprintf(
            '%s %s from Symfony Console!',
            $input->getOption('greeting'),
            $input->getArgument('name')
        );
        if ($input->getOption('backwards')) {
            $message = strrev($message);
        }
        $output->writeln($message);
    }
}

Spuštění pak může vypadat takto php cli.php zdrojak:hello Martin --greeting Ahoj --backwards, kdy na pořadí --greeting a --backwards nezáleží, příkaz se bude chovat stejně.

console03-hello-params

Tip 4: Pokud používáte PHPStorm, tak doporučuji instalaci Symfony pluginu, který i v samostatně použitém Symfony Console napovídá názvy argumentů, options či helperů.

Tip 5: Pokud potřebujete, aby příkaz vracel číselný chybový kód, třeba pro spojování více volání pomocí &&, tak stačí vrátit jakékoliv číslo z metody execute().

Console Helpers

Pro řešení běžných úkolů v konzolových skriptech obsahuje Symfony Console užitečné helpery, z nichž nejzajímavější jsou:

Question Helper

Jednoduché použití Question helperu může vypadat takto:

$questionHelper = $this->getHelper('question');
$question = new Question('Zadejte jméno souboru: ', 'default.txt');

$filename = $questionHelper->ask($input, $output, $question);

$output->writeln(sprintf('Použije se soubor %s', $filename));

Místo Question je možné použít ConfirmQuestion (očekává y) nebo ChoiceQuestion pro výběr z několika možností.

console04-question

Progress Bar

Při vytvoření ProgressBar nastavujeme počet úkonů, při jejich zpracování voláme metodu advance(), o zbytek se postará samotný helper. Pro činnosti s neurčitým počtem úkonů můžete udělat „nekonečný“ progressbar vynecháváním maximální hodnoty při vytváření ProgressBar.

$data = range(1, 100);

$progress = new ProgressBar($output, count($data));
$progress->start();

foreach ($data as $item) {
    //@todo do some work with $item
    usleep(30000);

    $progress->advance();
}
$progress->finish();

console05-progress

Table

Při vypisování tabulkových dat do konzole je zbytečně pracné počítat, kolik kde vypsat mezer, aby byl výstup správně zarovnaný. Je lepší špinavou práci nechat na Table helperu:

$table = new Table($output);
$table
    ->setHeaders(array('Datum', 'Název', 'Autor'))
    ->setRows(array(
        array('27.11.2015', 'Lumines: Vytváříme hru v React.js 1', 'Tobiáš Potoček'),
        array('26.11.2015', 'Poznámky z Reactive 2015. Ochutnávka budoucnosti, včera bylo pozdě a použití teď a tady', 'Daniel Steigerwald'),
        array('24.11.2015', 'Global Day of Coderetreat 2015', 'Milan Lempera'),
        array('16.11.2015', 'Jaká byla konference W-JAX 2015', 'Tomáš Dvořák'),
    ));
$table->render();

console06-table

Spouštění jiných příkazů

Z jednoho příkazu je možné volat jiné, což je užitečné pro vytváření meta příkazů (třeba sada úkolů pro hodinový cron). Commandu se předá vstup a výstup. V příkladu níže předáváme už existující výstup do konzole, ale obdobně bychom mohli předat BufferedOutput pro uložení výstupu do stringu nebo NullOutput pro jeho zahození (pokud by nás zajímal jen návratový kód metody run()).

<?php
// src/Command/MetaCommand.php

namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class MetaCommand extends Command
{
    protected function configure()
    {
        $this
            ->setName('zdrojak:meta')
            ->setDescription('Spusteni dalsich prikazu!');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        //zavoláme HelloCommand s parametry
        $hello = $this->getApplication()->find('zdrojak:hello');
        $hello->run(new ArrayInput([
            'name' => 'Martin',
            '--greeting' => 'Nazdar',
        ]), $output);

        //zavoláme ProgressCommand bez parametrů
        $progress = $this->getApplication()->find('zdrojak:progress');
        $progress->run(new ArrayInput([]), $output);
    }
}

Integrace do existující aplikace

Vše se samozřejmě nejlépe předvádí na malinké aplikaci a zajímavé to začne být teprve ve chvíli, kdy se pokusíte o integraci do existující aplikace. Myslím si, že v případě Symfony Console by to ale nemusel být problém.

Předpokladem je inicializace aplikace v souboru cli.php. Výsledkem by měl být container, ze kterého se dají vytáhnout potřebné služby. Následně si vytvoříme rozšíření třídy Command o instanci $containeru a upravíme jednotlivé příkazy, aby od ní dědily.

<?php //src/Command/ContainerAwareCommand.php
namespace App\Command;

use Symfony\Component\Console\Command\Command;

abstract class ContainerAwareCommand extends Command
{
    /**
     * @var ...
     */
    private $container;

    /**
     * @param ... $container
     */
    public function setContainer($container)
    {
        $this->container = $container;
    }

    /**
     * @return ...
     */
    public function getContainer()
    {
        return $this->container;
    }
}
<?php //src/Command/HelloCommand.php
class HelloCommand extends ContainerAwareCommand
{
.....

V cli.php je nutné upravit přidávání příkazů tak, aby se na nich volal setContainer():

$command = new App\Command\HelloCommand();
$command->setContainer($container);
$console->add($command);

Pokud byste chtěli elegantnější řešení, než to dlouze nastavovat v cli.php, tak stačí zdědit třídu Application, container ji nastavovat v kostruktoru a přetížit její metodu add(), aby na přidávaném Commandu volala setContainer() ona.

V samotném Commandu pak můžete použít kód podobný tomuto: $db = $this->getContainer()->get('db'); (samozřejmě je potřeba, aby container tu db obsahoval).

Další variantou je je nadefinovat Commandy přímo v DI containeru a mít rovnou vyřešené jejich závislosti.

Konkrétní způsob řešení bude samozřejmě závislý na architektuře aplikace. Při integraci Symfony Console do jedné staré ZF1 aplikace jsem nic nikam předávat nemusel, protože vše bylo dostupné v globálním stavu (což obecně není dobře, ale tady to zrovna práci ušetřilo).

Samotná Symfony integruje konzoli tím způsobem, který je popsaný výše. Jednotlivé Commandy se dědí od ContainerAwareCommand a container je zevnitř Commandu přístupný pomocí metody getContainer().

Závěrem

Doufám, že vás článek namotivoval k tomu, abyste do svých CLI skriptů vnesli pořádek a systém. Dejte tomu šanci, není to tolik práce a vyplatí se to. Integrovali jste Symfony Consoli do své aplikace v poslední době? Povzbuďte ostatní v komentářích, že to vážně není tak těžké!

Tip na konec: Pokud do konzole vypíšete "\7", tak to udělá „beep“ (funguje minimálně na Windows a na Linuxu) :-) Může se to hodit u déle běžících skriptů, které občas potřebují interaktivní vstup.

Komentáře: 5

Přehled komentářů

Ondřej Mirtes Výborný úvod
Martin Hujer Díky za odkaz
Ondřej Mirtes Re: Díky za odkaz
Lukáš Brzák ContainerAwareCommand..
Martin Hujer Re: ContainerAwareCommand..
Zdroj: https://www.zdrojak.cz/?p=16658