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ů)
- Symfony po krůčkách – Event Dispatcher 30. 11. 2015
- Symfony Console jako první rande se Symfony 7. 12. 2015
- Symfony po krůčkách – Filesystem a Finder 14. 12. 2015
- Symfony po krůčkách – Paralýza možností? OptionsResolver tě zachrání 21. 12. 2015
- Symfony po krůčkách – spouštíme procesy 4. 1. 2016
- Symfony po krůčkách – Translation – překlady jednoduše 11. 1. 2016
- Symfony po krůčkách – Validator (1) 18. 1. 2016
- Symfony po krůčkách – Validator (2) 25. 1. 2016
- Symfony po krůčkách – Routing 1. 2. 2016
- Symfony po krůčkách – MicroKernel 9. 2. 2016
- Konfigurujeme Symfony pomocí YAMLu 16. 2. 2016
- Symfony po krůčkách – oblékáme MicroKernel 23. 2. 2016
- Symfony po krůčkách – ClassLoader 29. 2. 2016
- Symfony po krůčkách – Twig 8. 3. 2016
- Symfony po krůčkách – Twig II. 15. 3. 2016
- Symfony po krůčkách – DomCrawler a CssSelector 23. 3. 2016
- Symfony po krůčkách – HTTP fundamentalista 12. 4. 2016
- 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:
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 dovendor\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 notacikategorie: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
:
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ě.
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 pro vstup od uživatele
- Progress Bar pro ukazatel průběhu
- Table pro formátování dat do tabulky
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í.
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();
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();
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.
Symfony Console doporučuju použít, i když vám třeba zbytek aplikace jede na Nette, ve kterém podpora CLI není řešené příliš dobře.
Ve Slevomatu máme takhle přes 200 commandů, které slouží k různým účelům – jsou pouštěny cronem, Supervisorem, při deployi i ručně.
Do Symfony 2.8/3.0 přibyly konzolové styly, které sjednocují vzhled a zjednodušují práci s různými vypisovacími helpery: https://symfony.com/blog/new-in-symfony-2-8-console-style-guide
Jak to máte s Nette integrované? Přes Kdyby\Console nebo jinak?
Díky za odkaz, akorát jsem na to taky narazil a chtěl jsem ho přidat :-)
Nemáme žádnou speciální integraci, po Nette chceme jen aby nám jeho DI kontejner vytvořil Console Application a přidal nám do ní dostupné commandy :)
Nutno podotknout, že v Symfony již ContainerAwareCommand existuje a není tak potřeba vytvářet vlastní abstraktní třídu…
Dále bych uvedl, že commandy jsou perfektní pro jakékoli cron operace, feedy, prvotní importy nebo náročné asynchronní operace. Právě možnost asynchronního spouštění je perfektní pro daemonizované aplikace, které mohou například synchronizovat eshop vs. ERP systém apod… Ještě je dobré prozkoumat verbositu (ukecanost) commandu, kdy Outputu je možno nastavit verbosity level a na základě toho vypisovat údaje…
Seriál je fajn, je vidět, že cílíte především na Nette vývojáře :-)
Díky za něj a držím palce…
Ten
ContainerAwareCommand
v Symfony je určený pro použití se Symfony containerem. Příklad v článku měl ukázat, jak to vyřešit pro svou aplikaci v jiném frameworku.