CLI skripty v Nette aplikaci
Když se řekne Nette aplikace, vybaví se mi webová aplikace. Prezentery, šablony, formuláře a následně HTML výstup pro prohlížeč. Ale paralelně se světem webových prohlížečů existuje ještě jeden, naprosto odlišný, svět příkazové řádky (CLI). A pokud naše webová aplikace potřebuje například periodicky spouštět nějaké úkony na pozadí, musíme se do světa CLI vydat.
V tomto článku popíšu jeden ze způsobů, jak se dá s CLI skripty v Nette aplikaci poprat. Popíšu, jak snadno vytvořit jednoúčelové skripty bez potřeby dalších knihoven.
Kam uložit CLI skripty
Když už se rozhodneme nějaký CLI skript napsat, první co budeme řešit je, kam ho uložíme. Osvědčila se mi následující adresářová struktura:
app/ bin/ <-- jednoúčelové CLI skripty cron/ <-- skripty pro cron log/ sessions/ www/
K oddělení složek bin
a cron
jsem došel
časem. Pokud máte skriptů víc, začnete ztrácet přehled v tom, který
můžete jen tak ručně spustit a který se spouští periodicky sám. To, jak
jsem omylem ve dvě hodiny odpoledne rozeslal šest tisíc emailů, které měly
odejít až po půlnoci, dál už rozebírat nebudu.
Zkoušel jsem používat konvenci bin/cron-xyz.php
, jindy zase
podsložku bin/cron/
, ale z nějakého důvodu jsem skončil se
dvěma oddělenými složkami.
Spustitelné skripty
Ve Windows můžeme pravým kliknutím na PHP soubor zvolit „Otevřít
v programu…“ a vybrat cestu k php.exe
. Následně už můžeme
skripty spouštět dvojklikem jako obyčejné programy. Osobně na Windows
vyvíjím, ale používám Cygwin/GIT bash. Potom je postup stejný jako na
Linuxu.
Spustit PHP skript z CLI je snadné:
> php bin/skript.php
Psát interpret stále dokola omrzí. V PHP skriptu můžeme používat shebang. Používám:
#!/usr/bin/env php
<?php
Nastavíme spustitelný bit:
> chmod ug+x bin/skript.php
A můžeme spouštět:
> bin/skript.php # i s interpretem stále funguje: > php bin/skript.php
Execute bit je pro shebang důležitý detail. Na Linuxu je to v pohodě, ale vyvíjím na Windows a stále zapomínám tento příznak zaverzovat. Na Windows k tomu musíme git instruovat:
> git add bin/skript.php --chmod=+x
Zatím jsem nepřišel na žádné automatizované elegantní řešení.
Něco na způsob .gitattributes
by bylo fajn.
Použití shebang na Windows ničemu nevadí. PHP interpret je na to
připravený a shebang řádek přeskočí. Potíž je ale s vkládáním
souborů. Pokud byste zvažovali volání require 'bin/skript.php';
vypíše se shebang řádek na standardní výstup a to je problém.
S příponou, nebo bez
Jeden čas jsem koketoval s myšlenkou vypustit .php
příponu
u skriptů. Přišlo mi, že je nepodstatná.
> bin/skript # kratší > bin/skript.php # vs. delší
Ale upustil jsem od toho. Objevilo se totiž několik nevýhod. Editory
nezvýrazňovaly, analyzátory neanalyzovaly a i obyčejné
find . -name '*.php'
přestalo fungovat.
Integrace s Nette
Dopracujeme se ke skriptu pro cron task, který bude provádět údržbu v databázi. Vždy po půlnoci smaže logy starší jednoho roku. Je tu ale několik otázek, na které dříve nebo později narazíme.
Debug vs. produkce
Jaký mód použít na webu je jasné a zaběhlé. V debug módu se zobrazuje Tracy panel a červené „bluescreeny“, v produkčním módu je aplikace potichu a chyby pouze loguje. Ale co v CLI?
Musíme si položit otázku: „Kdo bude CLI skript spouštět?“
V mých aplikacích jsem to vždy já, nebo kolega vývojář, nebo cron.
Proto pro CLI skripty vždy zapínám debug režim. Pokud skript selže, vyvalí
se na mě stack trace a já si přečtu, kde je chyba. Pokud selže skript pro
cron, stack trace mi přijde emailem (cron jakýkoliv stdout i stderr odesílá
emailem na konto, pod kterým úloha běží). Pokud to máte stejně tak pouze
připomenu, že při zapnutém debug režimu v adresáři log/
nic
nenajdete.
Vaše situace ale může být jiná. Skripty mohou spouštět i jiní a
emaily s výstupem cronu se k vám nedostanou. Potom bych preferoval
produkční režim s volitelným zapnutím debug režimu, například pomocí
CLI parametru --debug
.
Služby z DI kontejneru
Služby pro CLI skripty budeme získávat z DI kontejneru, ale jeho
nastavení pro web, nebo CLI se může lišit. Současný trend ve vytváření
DI kontejneru (a Nette Sandbox to tak dělá také) je pomocí třídy
Bootstrap
, kterou načítá Composer. Já to dělám v podstatě
stejně:
namespace App;
use Nette\Configurator;
use Tracy;
final class Bootstrap
{
public static function boot(): Configurator
{
return self::createConfigurator([
'10.10.0.5', # milo.dev
]);
}
private static function createConfigurator($debugMode): Configurator
{
$configurator = new Configurator;
$configurator->setDebugMode($debugMode);
$configurator->enableDebugger(__DIR__ . '/../log', 'milo@example.tld');
$configurator->setTempDirectory(__DIR__ . '/../temp');
$configurator->createRobotLoader()
->addDirectory(__DIR__)
->addDirectory(__DIR__ . '/../lib')
->register();
$configurator->addConfig(__DIR__ . '/config/common.neon');
$configurator->addConfig(__DIR__ . '/config/local.neon');
return $configurator;
}
}
To je takový základ. Webová aplikace, konkrétně index.php
,
už jen udělá:
<?php
require __DIR__ . '/../vendor/autoload.php';
App\Bootstrap::boot()->createContainer()
->getByType(Nette\Application\Application::class)
->run();
Pro potřeby CLI skriptů si vytvoříme vlastní Bootstrap
metodu. Může to být bootForCli()
, nebo
bootForCron()
. Záleží na osobních preferencích:
public static function bootForCron(): Configurator
{
# Debug mód pouze pokud existuje --debug přepínač
return self::createConfigurator(in_array('--debug', $_SERVER['argv'], true));
}
A tím máme připraveno vše, co CLI skripty potřebují.
Ukázka CLI skriptu
#!/usr/bin/env php
<?php
namespace App\Cron;
use App;
use Dibi;
# Autoloading tříd přes Composer - tedy i naší Bootstrap třídy
require __DIR__ . '/../vendor/autoload.php';
# Necháme konfigurátor, aby nám sestavil DI kontejner
$container = App\Bootstrap::bootForCron()
->createContainer();
# Z kontejneru si nyní můžeme vytahat služby, které potřebujeme
$db = $container->getByType(Dibi\Connection::class);
# Kousek kódu, který chceme vykonat
$db->query('DELETE FROM log.access WHERE at < now() - %s::interval', '1 year');
Následně nakonfigurujeme cron task a máme hotovo.
00 00 * * * www-data /var/www/sites/example.tld/cron/clean-up-logs.php
Závěrem
Výsledný kód je až směšně krátký. A pokud bychom ukázku mazání logů zapouzdřili do samostatné služby, bude kód ještě kratší:
App\Bootstrap::bootForCron()
->createContainer()
->getByType(VictorTheCleaner::class)
->clean();
Ukázal jsem, jak psát skripty pro CLI velmi rychle a jednoduše. Neberte to
jako „to jediné správné“ řešení. Hlavní nevýhodou, se kterou se
potýkám, je zpracování parametrů příkazové řádky. Použití
getopt()
je otravné. Parsování parametrů z
$_SERVER['argv']
je zase pracné. Občas tak použiji balíček Nette Command-Line, který
zpracování parametrů výrazně ulehčí.
Někdo řeší cron úlohy přes web. Periodicky volá
wget https://example.tld/cron/task
.
Dalším a dost možná i nejznámějším řešením pro CLI je Symfony Console. Je to rozhodně robustní řešení a protože ho používá opravdu hodně vývojářů, najdete pro něj integraci ve spouště balíčků, například databázových migrací. Na Componette najdete integraci i pro Nette.
Co se mě týče, pokud mohu, stále zůstávám u popisovaných jednoúčelových skriptů. Jsou to konec konců jen misky vah. Na jedné misce poskytnutý tooling a pohodlí od knihovny třetí strany, a na druhé misce závislost v Composeru, kterou musím neustále hlídat.
Komentáře
Díky za pěkný článek.
Já jsem si v CLI scriptech oblíbil metodu callMethod (akceptuje jakoukoliv funkci, nejen metodu) na containeru, která autorwiruje služby z DI kontejneru do parametrů funkce. Je to hlavně užitečně, když ten script vyžaduje více služeb z kontejneru. A taky funguje veškerá statická analýza, napovídání atd. automaticky bez nějakých extensions a konfigurace.
Takže kód pak vypadá následovně:
#1 David Matějka To je parádní trik!
To je fakt dobrej trik s tim
callMethod
.Super článek, poprvé mi ani nedošlo, kolik je v něm pro mě novinek :)
callMethod musím jít hned vyzkoušet, to vypadá fakt dobře :)
Chcete-li odeslat komentář, přihlaste se