CLI skripty v Nette aplikaci

před 13 dny od Miloslav Hůla     edit

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žíval 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 schebang 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 stadardní 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čené 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 staší 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
	{
		Tracy\Debugger::$maxDepth = 10;

		$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ámně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 (RSS)

  1. 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ě:

    $container = App\Bootstrap::bootForCron()
    	->createContainer();
    $container->callMethod(function (VictorTheCleaner $cleaner) {
    	$cleaner->clean();
    });
    před 10 dny · replied [2] Milo
  2. #1 David Matějka To je parádní trik!

    před 10 dny
  3. To je fakt dobrej trik s tim callMethod.

    včera

Chcete-li odeslat komentář, přihlaste se