Symfony po krůčkách – spouštíme procesy

Pokud chceme z našeho PHP skriptu spustit jiný program či příkaz, při použití čistého PHP se můžeme značně nadřít. Dnes se proto podíváme na další Symfony komponentu – Process, která se stará o spouštění procesů a umožňuje s nimi elegantně pracovat.

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

Proč by kdo z PHP spouštěl jiné programy?!

Spouštění programů z běžícího PHP skriptu vypadá na první pohled trochu jako zhůvěřilost. Jsou ale situace, kdy nám to přijde vhod – zpravidla se ale nejedná o použití na webu (tedy že bychom v rámci HTTP requestu spouštěli nějaké systémové příkazy a programy), nýbrž o spouštění „z příkazové řádky“, kdy PHP skript obstarává nějakou automatizační úlohu.

Použití PHP místo shellového skriptu pro spouštění příkazu či sekvence příkazů pak může mít různé výhody, např. možnost použití na různých platformách či snazší začlenění do stávající infrastruktury.

Process komponentu využívá plno známých projektů, namátkou Composer, Behat, Scrutinizer či Laravel.

Jak v PHP spustit příkaz?

V čistém PHP je možné použít pro spouštění systémových příkazů či programů několik funkcí: exec(), system(), passthru(), popen() a konečně proc_open(). Nejmocnější je poslední jmenovaná, a právě okolo té je postavená komponenta Symfony/Process.

Vytvoření a spuštění příkazu je jednoduché, pouze se „obalí“ třídou Process:

<?php // get-webshot-version.php
require_once __DIR__ . '/vendor/autoload.php';

use Symfony\Component\Process\Process;

// Vytvoříme instanci Process
$process = new Process('./node_modules/.bin/webshot --version');
// Proces spustíme
$process->run();
// Po dokončení vypíšeme výstup
echo 'Verze je: ' . $process->getOutput();

A náš PHP skript, který uvnitř zavolá příkaz webshot a vypíše výstup, spustíme pomocí php get-webshot-version.php.

V příkladech budeme spouštět utilitu webshot, která umožňuje vytvořit z příkazové řádky screenshot zadaného webu. Pokud byste si ji chtěli nainstalovat, stačí mít npm a spustit npm install webshot-cli.

Process komponenta nám ale poskytuje mnohem širší možnosti. Aby se nám vše lépe psalo, využijeme opět komponentu Console známou z předchozích dílů seriálu, a náš PHP skript tak budeme dále psát jako vlastní Command.

Tentokrát budeme chtít udělat nějakou reálnou funkcionalitu – z URL, zadaného jako parametr, budeme chtít vygenerovat náhled. Zároveň si ošetříme některé chybové stavy.

Kompletní kód příkladu najdete na GitHubu.

// src/Command/WebshotSimpleCommand.php
...
use Symfony\Component\Process\Process;
...
$process = new Process(
    sprintf(
        './node_modules/.bin/webshot "%s" "%s"',
        $url,
        $outputFile
    )
);
$process->run();

// Pokud proces nedoběhl v pořádku, vypíšeme chybu
if (!$process->isSuccessful()) {
    if ($process->getExitCode() == 127) {
        $output->writeln('Příkaz webshot nenalezen. Nelze spustit:');
        $output->writeln($process->getCommandLine());
    } else {
        $output->write($process->getErrorOutput());
    }

    return 1;
}

$output->writeln('V pořádku hotovo!');

return 0;

Náš skript nyní spustíme pomocí příkazu ./cli.php webshot:simple http://zdrojak.cz. Do adresáře output/ bude následně vygenerován screenshot stránky zdrojak.cz.

V kódu již vidíme první věci, které nám komponenta Process radikálně usnadnila – práci se standardním a s chybovým výstupem spouštěného programu a také s jeho návratovými kódy.

Něco obdobného by šlo samozřejmě udělat i čistě v shellu. Situace bude poněkud jiná ve chvíli, kdy bychom například seznam URL adres načítali z databáze či fronty, chybový výstup chtěli logovat do našeho log management systému a podobně. Pak můžeme s výhodou používat naše stávající PHP knihovny a nepsat vše znovu.

Process Builder pro snazší vytváření procesů

Nyní si zkusíme přidat proměnné prostředí (environment variables), které bude využívat spouštěný program. V našem příkladu nastavíme prostředí procesu proměnnou http_proxy, dle které webshot pozná, že se stránky mají stahovat skrze danou proxy.

Tuto proměnnou můžeme přidat v poli jako třetí parametr konstruktoru Process, my ale vyzkoušíme ProcessBuilder, který ještě více usnadňuje přípravu procesů.

Celý skript naleznete opět na GitHubu.

// src/Command/WebshotProcessBuilderCommand.php
...
use Symfony\Component\Process\ProcessBuilder;
...
$builder = (new ProcessBuilder())
    ->setPrefix('./node_modules/.bin/webshot')
    ->setTimeout(5)
    ->add('--window-size=1280/1024')
    ->add($url)
    ->add($outputFile)
;

if ($useProxy) {
    $builder->setEnv('http_proxy', self::PROXY);
}

$process = $builder->getProcess();
$process->run();
...

Spuštění provedeme obdobně jako minule, jen jsme náš Console Command nyní pojmenovali jinak: ./cli.php webshot:builder http://zdrojak.cz

Zde vidíme, že ProcessBuilder nám hezky zpřehlednil vytváření instance procesu: jednotlivé argumenty jsme příkazu webshot přidali metodou add(), zároveň jsme pomocí setTimeout() nastavili timeout procesu na 5 sekund a taktéž jsme mu skrze proměnnou prostředí nastavili metodou setEnv() onu proxy.

Asynchronní a paralelní spouštění procesů

V dosavadních ukázkách jsme proces spouštěli metodou run(). Ta je ovšem blokující (synchronní) – dokud příkaz nedoběhne (a neskončí vykonávání metody run()), ve vykonávání našeho PHP skriptu se nepokračuje. My ovšem můžeme chtít procesů spustit několik paralelně, třeba abychom využili všechna jádra procesoru u časově náročnějších operací. Pokoušet se o toto v čistém PHP se může proměnit ve výlet do horoucích pekel. Naštěstí i s tímto nám pomůže Process komponenta – v následujícím příkladu si předáme několik URL adres a procesy pro vytvoření náhledu pro každou URL se spustí všechny souběžně.

Znovu připomínám, že celý skript si můžete prohlédnout na GitHubu.

// src/Command/WebshotMultipleCommand.php
...
/** @var Process[] $processSet */
$processSet = [];
$i = 0;
foreach ($urls as $url) {
    $builder = (new ProcessBuilder())
        ->setPrefix('./node_modules/.bin/webshot')
        ->setTimeout(5)
        ->add('--window-size=1280/1024')
        ->add($url)
        ->add($outputFilePrefix . $i++ . '.png')
    ;

    $process = $builder->getProcess();
    $process->start();
    $processSet[$url] = $process;
}

while (!empty($processSet)) {
    foreach ($processSet as $url => &$process) {
        // Nejprve zkontrolujeme, zda-li již nenastal timeout procesu
        $process->checkTimeout();
        // Pokud proces již neběží, odebereme jej z pole a vypíšeme výsledek
        if (!$process->isRunning()) {
            unset($processSet[$url]);
            if (!$process->isSuccessful()) {
                $output->writeln('Chyba při zpracování URL ' . $url);
                $output->write($process->getErrorOutput());
            } else {
                $output->writeln('Hotovo: ' . $url);
            }

            $output->writeln('Zbývajících procesů: ' . count($processSet));
        }
    }

    // Počkáme 100 ms do další kontroly běžících procesů
    usleep(100000);
}
...

Příkaz pak můžeme spustit s několika URL adresami najednou a screenshoty se budou generovat souběžně v samostatných procesech. Ve výstupu také uvidíme, jak jednotlivé procesy dobíhají:

$ ./cli.php -v webshot:multiple https://www.zdrojak.cz https://devel.cz http://www.root.cz
Hotovo: http://devel.cz/
Zbývajících procesů: 2
Hotovo: https://www.zdrojak.cz/
Zbývajících procesů: 1
Hotovo: http://www.root.cz/
Zbývajících procesů: 0

Je vidět, že asynchronní spouštění má několik specifik – spuštění probíhá metodou start() nikoliv run(), sami si musíme hlídat, kdy který proces doběhl, a kontrolovat, zdali mu nenastal timeout. Pokud bychom nepočkali na doběhnutí procesů (nebyla by zde například while smyčka) a PHP skript ukončili, ukončí se tím i spuštěné podprocesy.

V praxi používáme asynchronní spouštění v projektu Steward, kde se paralelně spouští procesy PHPUnitu se seleniovými testy, a ve smyčce se čeká, až procesy doběhnou.

Pro úplnost ještě dodám, že při asynchronním spouštění můžeme s běžícími procesy dělat několik další věcí:

  • zjišťovat inkrementální výstup skrze metody getIncrementalOutput() a getIncrementalErrorOutput(),
  • zastavit proces metodou stop() (která se napřed pokusí zastavit proces signálem TERM, až pokud se to do zadaného času nepovede, zastaví proces signálem KILL),
  • či posílat procesům ostatní unixové signály metodou signal().

Kam jsme dnes dokráčeli

V tomto díle jsme si ukázali, že spouštění procesů z PHP může být díky Process komponentě vcelku bezbolestné. Víme také, že:

  • jde snadno zpracovávat výstup procesu, ať již standardní či chybový,
  • můžeme jednoduše pracovat s návratovými kódy procesu,
  • procesy můžeme spouštět synchronně i asynchronně, v druhém případě pak není problém spustit více procesů zároveň,
  • se vysloveně nabízí synergie Console a Process komponent,
  • a že Proces komponenta se nám postará o eliminaci některých multiplatformních problémů, čímž může být náš skript snadno přenositelný mezi různými operačními systémy.

A kam jít dál?

Zdrojový kód všech uvedených příkladů najdete v repozitáři na GitHubu.

Další podrobnosti ohledně použití komponenty je možné (jako vždy) nalézt v obsáhlé oficiální dokumentaci komponenty.

V příkladech výše jsme využívali také komponentu Console, o které byl jeden z minulých dílů seriálu. Při tomto použití obou komponent zároveň může další práci ušetřit ProcessHelper, který je součástí Console komponenty a který vypisuje naformátované informace o spouštění, průběhu a výsledku procesu.

Na závěr ještě zmíním specifický typ Process objektu, totiž PhpProcess, skrze který lze spustit zadaný PHP kód v samostatném izolovaném PHP procesu. Detaily opět viz dokumentace.

Pracuji v LMC, kde se podílím na vývoji kariérního portálu Jobs.cz. Vyvíjím převážně v PHP a v Symfony, několik let předtím jsem dělal v Zend Frameworku 1.

Mám rád, když jsou věci dobře udělané a fungují – proto se také zajímám o testování a QA obecně a starám se o testovací open-source nástroj Steward.

Komentáře: 2

Přehled komentářů

Lukáš Brzák super
Ondřej Machulda Re: super
Zdroj: https://www.zdrojak.cz/?p=17047