phpFashion

Na navigaci | Klávesové zkratky

Velestručné testování presenterů v Nette

Téma testování presenterů by vydalo na celý seriál, ale ušetříme si čas a místo toho popíšu, jak v několika krocích začít.

Jako testovací framework budu používat Nette Tester. Pochopitelně by šel použít třeba i PHPUnit.

A jako vzorovou aplikaci můžeme vzít třeba Nette Sandbox, protože jej najdete v každé distribuci Nette, nebo si ho můžete stáhnout, včetně frameworku, pomocí Composeru:

composer create-project nette/sandbox myApplication

V něm už máme připravený testovací bootstrap, který vytváří DI kontejner (a vlastně se moc neliší od klasického app/bootstrap.php).

Vyrobíme si tedy instanci presenteru. Buď ručně operátorem new a předáme všechny závislosti, nebo jednodušeji za využití PresenterFactory:

// z DI kontejneru, který vytvořil bootstrap.php, získáme instanci PresenterFactory
$presenterFactory = $container->getByType('Nette\Application\IPresenterFactory');

// a vyrobíme presenter Sign
$presenter = $presenterFactory->createPresenter('Sign');

A bude vhodné vypnout autoCanonicalize, aby presenter nepřesměrovával na kanonické URL:

$presenter->autoCanonicalize = FALSE;

A rovnou můžeme začít testovat, třeba akci Sign:in:

// zobrazení stránky Sign:in metodou GET
$request = new Nette\Application\Request('Sign', 'GET', array('action' => 'in'));
$response = $presenter->run($request);

Presenter je stavěn na jedno voláním run(), pro další requesty vytvoříme vždy nový presenter.

Ověříme, zda odpověď je skutečně šablona:

Assert::true( $response instanceof Nette\Application\Responses\TextResponse );
Assert::true( $response->getSource() instanceof Nette\Templating\ITemplate );

Necháme šablonu vygenerovat HTML kód:

$html = (string) $response->getSource();

A nyní třeba zkontrolujeme, zda se na stránce nacházejí formulářová políčka pro jméno a heslo. Syntax je stejná jako u CSS selektorů.

$dom = Tester\DomQuery::fromHtml($html);

Assert::true( $dom->has('input[name="username"]') );
Assert::true( $dom->has('input[name="password"]') );

Toliko úvodem.


FTP Deployment: smart file upload

FTP deployment is a tool for automated deployment to an FTP server.

There is nothing worse than uploading web applications to FTP server manually, using tools like Total Commander. (Although, editing files directly on the server and then trying to keep some kind of synchronization is even worse ;-)

Once the process is automated, it costs you a fraction of time and minimizes the risk of error (didn't I forget to upload some files?). There are lots of sophisticated deploying techniques available today, but many people are still using FTP. This tool is designed for them.

FTP Deployment is a script written in PHP (requires PHP 5.4 or newer) and will automate the entire process. Just say which local folder to upload and where. This information is stored in a deployment.ini text file, which you can associate with deployment.php script, so deployment will become a one click thing.

php deployment.php deployment.ini

And what does the deployment.ini file contain? Only the remote item is required, all the others are optional:

; log file (defaults to config file with .log extension)
log = ...

; directory for temporary files (defaults to system's temporary directory)
tempdir = /temp/deployment

; enable colored highlights? (defaults to autodetect)
colors = yes

[my site] ; Optional section (there may be more than one section).
; remote FTP server
remote = ftp://user:secretpassword@ftp.example.com/directory
; you can use ftps:// or sftp:// protocols (sftp requires SSH2 extension)

; FTP passive mode
passivemode = yes

; local path (optional)
local = .

; run in test-mode? (can be enabled by option -t or --test)
test = no

; files and directories to ignore
ignore = "
    .git*
    project.pp[jx]
    /deployment.*
    /log
    temp/*
    !temp/.htaccess
"

; is the script allowed to delete remote files? (defaults to yes)
allowdelete = yes

; jobs to run before file upload
before[] = local: lessc assets/combined.less assets/combined.css
before[] = http://example.com/deployment.php?before

; jobs to run after file upload
after[] = remote: unzip api.zip
after[] = http://example.com/deployment.php?after

; directories to purge after file upload
purge[] = temp/cache

; files to preprocess (defaults to *.js *.css)
preprocess = no

; file which contains hashes of all uploaded files (defaults to .htdeployment)
deploymentfile = .deployment

Configuration can also be stored in a PHP file.

In test mode (with -t option) uploading or deleting files is skipped, so you can use it to verify your settings.

Item ignore uses the similar format to .gitignore:

log - ignore all 'log' files or directories in all subfolders
/log - ignore 'log' file or directory in the root
app/log - ignore 'log' file or directory in the 'app' in the root
data/* - ignore everything inside the 'data' folder, but the folder will be created on FTP
!data/db/file.sdb - make an exception for the previous rule and do not ignore file 'file.sdb'
project.pp[jx] - ignore files or folders 'project.ppj' and 'project.ppx'

Before the upload starts and after it finishes, you can execute commands or call your scripts on the server (see before and after), which can, for example, switch the server to a maintenance mode. If you use php-config – you can run lambda function with deployment environment.

Syncing a large number of files attempts to run in (something like) a transaction: all files are uploaded with extension .deploytmp and then quickly renamed.

An .htdeployment file is uploaded to the server, which contains MD5 hashes of all the files and is used for synchronization. So the next time you run deployment.php, only modified files are uploaded and deleted files are deleted on server (if it is not forbidden by the allowdelete directive).

Uploaded files can be processed by a preprocessor. These rules are predefined in the deployment.php file: .css files are compressed using the YUI Compressor and .js minified by Google Closure Compiler. These tools are already included in the distribution, however, they require the presence of Java.

There is also a rule for expanding mod_include Apache directives. For example, you can create a file combined.js:

<!--#include file="jquery.js" -->
<!--#include file="jquery.fancybox.js" -->
<!--#include file="main.js" -->

This tool will combine scripts together and minify them with the Closure Compiler to speed-up your website.

In the deployment.ini, you can create multiple sections, i.e. you may have separate rules for data and for application.


FTP Deployment: nahrávejte přes FTP chytře

Není nic horšího, než uploadovat soubory na FTP ručně, například pomocí Total Commanderu. (Ačkoliv, ještě horší je editovat soubory přímo na serveru a pak se zoufale pokoušet o jakousi synchronizaci.) Jakmile totiž proces nezautomatizujete, stojí vás mnohem víc času a hrozí riziko chyby. Třeba, že některý soubor zapomenete nahrát.

Dnes už se používají sofistikované techniky nasazování aplikací na web, například pomocí Gitu, ale mnoho lidí stále zůstává u nahrávání jednotlivých souborů skrze FTP. Právě pro ně je určen nástroj FTP Deployment, který zautomatizuje a zjednoduší nahrávání aplikací přes FTP.

FTP Deployment je skript napsaný v PHP, který celý proces zautomatizuje. Stačí jen říct, který adresář (local) má kam nahrát (remote). Tyto údaje zapíšete do souboru deployment.ini, jehož odkliknutí můžete rovnou asociovat se spuštěním skriptu, takže deployment se stane věcí jednoho kliknutí:

php deployment.php deployment.ini

A jak vypadá soubor deployment.ini? Povinná je vlastně jen položka remote, všechny ostatní jsou nepovinné:

; remote FTP server
remote = ftp://user:secretpassword@ftp.example.com/directory

; local path (optional)
local = .

; run in test-mode? (can be enabled by option -t or --test too)
test = no

; files and directories to ignore
ignore = "
    .git*
    project.pp[jx]
    /deployment.*
    /log
    temp/*
    !temp/.htaccess
"

; is allowed to delete remote files? (defaults to yes)
allowdelete = yes

; jobs to run before file upload
before[] = http://example.com/deployment.php?before

; jobs to run after file upload
after[] = http://example.com/deployment.php?after

; directories to purge after file upload
purge[] = temp/cache

; files to preprocess (defaults to *.js *.css)
preprocess = no

; file which contains hashes of all uploaded files (defaults to .htdeployment)
deploymentfile = .deployment

V testovacím režimu (při spuštění s parametrem -t) k uploadu nebo mazání souborů na FTP nedochází, můžete jej tedy použít k ověření, zda máte všechny hodnoty dobře nastavené.

Položka ignore používá stejný formát jako .gitignore:

  • log – ignoruje všechny soubory či adresáře log, i uvnitř všech podsložek
  • /log – ignoruje soubor či adresář log v kořenovém adresáři
  • app/log – ignoruje soubor či adresář log v podsložce app kořenového adresáře
  • data/* – ignoruje vše uvnitř složky data, ale samotnou služku na FTP vytvoří
  • !data/session – z předchozího pravidla učiní výjimku pro soubor či složku session
  • project.pp[jx] – ignoruje soubory či složky project.ppjproject.ppx

Před započetím uploadu a po jeho skončení můžete nechat zavolat skripty na vašem serveru (viz before a after), které mohou například server přepnout do maintenance režimu, kdy bude odesílat hlavičku 503.

Aby synchronizace i velkého množství souborů proběhla (v rámci možností) transakčně, všechny soubory se nejprve nahrají s příponou .deploytmp a poté, což už je rychlé, přejmenují. Zároveň se na server uloží soubor .htdeployment, kde jsou uloženy md5 otisky všech souborů a právě pomocí něj se nadále web synchronizuje.

Při dalším spuštění tedy nahrává pouze změněné soubory a maže smazané (pokud to nezakážeme direktivou allowdelete).

Nahrávané soubory je možné nechat zpracovat preprocesorem. Ve skriptu deployment.php jsou uvedeny pravidla, že všechny .css soubory se zkomprimují pomocí YUI Compressor a .js pomocí Google Closure Compiler. Tyto nástroje jsou už součástí distribuce, nicméně vyžadují přítomnost Javy. Před samotnou komprimací se ještě expandují základní mod_include direktivy Apache. Můžete tedy vytvořit například soubor combined.js:

<!--#include file="jquery.js" -->
<!--#include file="jquery.fancybox.js" -->
<!--#include file="main.js" -->

Který vám bude Apache na lokálním serveru za běhu sestavovat spojením tří uvedených souborů. Říci si o to můžete takto:

<FilesMatch "combined\.(js|css)$">
    Options +Includes
    SetOutputFilter INCLUDES
</FilesMatch>

Přičemž na server se nahraje už ve spojené a zkomprimované podobě. Vaše HTML stránka tak bude šetřit zdroje a načítat jediný JavaScriptový soubor.

V konfiguračním souboru deployment.ini můžete vytvořit i více sekcí, případně si udělat jeden konfigurák zvlášť pro data a jeden pro aplikaci, aby synchronizace byla co nejrychlejší a nemusel se vždy počítat otisk velkého množství souborů.

Nástroj FTP Deployment jsem si vytvořil před mnoha lety a plně pokrývá mé požadavky na deployovací nástroj. Zároveň je třeba zdůraznit, že FTP protokol tím, že přenáší heslo v čitelné podobě, představuje bezpečnostní riziko a rozhodně byste jej neměli používat třeba na veřejných Wi-Fi.


Objeví Rails Dependency Injection?

Málokdo má takovou potřebu zdůrazňovat svou domnělou nadřazenost, jako právě Railisti. Abyste mě nechápali špatně, jde o dobrou marketingovou strategii. Nepříjemné je, když jí podlehnete do té míry, že zbytek světa vnímáte jen jako upachtěné kopírovače bez šance se vám někdy přiblížit. Svět takový totiž není.

Příkladem je Dependency Injection. Zatímco lidé kolem PHP nebo JavaScriptu objevili DI se zpožděním, Ruby on Rails zůstávají dosud nepolíbené. Bylo mi záhadou, proč framework s tak pokrokovou image zůstává kdesi pozadu a začal v tom pátrat. Odpověď mi dala řada zdrojů na Google nebo karmiq a zní:

Ruby je tak dobrý jazyk, že vůbec Dependency Injection nepotřebuje.

Fascinující argument, který je navíc v elitářském prostředí sebepotvrzující. A je skutečně pravdivý? Nebo jde jen o zaslepení pýchou, stejné zaslepení, jaké způsobilo nedávno přetřásané bezpečnostní díry v Rails?

Říkal jsem si, že je možné, že Ruby znám natolik málo, aby mi nějaký klíčový aspekt unikl, a že skutečně jde o jazyk, který DI nepotřebuje. Jenže primárním smyslem Dependency Injection je zřejmé předávání závislostí, aby byl kód srozumitelný a předvídatelný (a je pak i lépe testovatelný). Jenže když se podívám do dokumentace Rails na tutoriál „blog za pár minut“, vidím tam třeba:

def index
  @posts = Post.all
end

Tedy pro získání blogpostů používají statickou metodu Post.all, která odněkud (!) vrátí seznam článků. Z databáze? Ze souboru? Vyčaruje je? Nevím, protože se tu nepoužívá DI. Místo toho se tu vaří nějaké statické peklo. Ruby je bezesporu šikovný jazyk, ale DI nenahrazuje.

V Ruby lze za běhu přepisovat metody (Monkey patch; obdobně jako v JavaScriptu), což je forma Inversion of control (IoC), která třeba pro potřeby testů dovolí podstrčit jinou implementaci statické metody Post.all. Tohle ale nenahrazuje DI, kód to zřejmější neudělá, spíše naopak.

Mimochodem, zaujala mě i třída Post tím, že reprezentuje jak jeden článek na blogu, tak funguje jako repozitář (metoda all), což je porušení Single Responsibility Principle jako vyšité.

Jako odůvodnění, proč Ruby nepotřebují DI, se často odkazuje na článek LEGOs, Play-Doh, and Programming. Důkladně jsem ho pročetl, sledoval, jak autor párkrát zaměňuje „DI“ s „DI frameworkem“ (tedy něco jako zaměňovat „Ruby“ s „Ruby on Rails“) a nakonec zjistil, že k závěru, že Ruby Dependency Injection nepotřebují, vůbec nepřišel. Psal, že nepotřebují DI frameworky, jaké zná z Javy.

Jeden mylně interpretovaný závěr, pokud pohladí ego, dokáže zcela pobláznit obrovskou skupinu inteligentních lidí. Nakonec mýtus, že špenát obsahuje neobyčejné množství železa, se taky drží od roku 1870.

Ruby je velmi zajímavý jazyk, vyplatí se v něm jako v každém jiném používat DI a existují pro něj i DI frameworky. Rails je zajímavý framework, který zatím neobjevil DI. Až ho objeví, půjde o velké téma některé z příštích verzí.

(Po pokusu diskutovat o DI s Karmim, kterého pokládám za nejinteligentnějšího Railistu, nechávám komentáře uzavřené, omlouvám se.)

Viz také: Dependency injection není pouze o jednoduším testování

před 3 lety v rubrice Web | shlédnuto 15960×


PHP 5.4 má nepoužitelný typ callable

Jednou z novinek PHP 5.4 je typehint callable. Byla by to parádní věc, kdyby to tvůrci tak děsně nezprasili.

PHP typ callbable je pseudotypem, jehož hodnotou může být buď název metody či funkce (tj. řetězec) nebo dvojice třída/objekt a její metoda, tedy pole. V PHP se používá od pradávna, nicméně od verze 5.4 pro něj existuje typehint:

class Template
{
    function registerHelper($name, callable $helper)
    {
        ...
    }
}

$template = new Template;
$template->registerHelper('date', 'Helpers::date');

Skvělé. Tedy až do chvíle, než to vyzkoušíte a PHP vás zdupne:

error:  Argument 2 passed to Template::registerHelper() must be callable, string given

Aha, tak je to asi potřeba zapsat jako pole:

$template->registerHelper('date', ['Helpers', 'date']);

Výsledek je opět:

error: Argument 2 passed to Template::registerHelper() must be callable, array given

Ve skutečnosti PHP vadí to, že třída Helpers neexistuje. Totiž už během volání registerHelper její přítomnost vyžaduje. Typehint callable, narozdíl od všech jiných typehintů, neověřuje jen formální platnost předaného argumentu, ale ujišťuje se, že uvedená třída skutečně existuje a má zmíněnou metodu. Pokud třída neexistuje, pokusí se ji načíst autoloadingem.

Což jednak zabíjí lazyloading – už samotné předání parametru s callbackem načte příslušnou třídu, která by se třeba jinak vůbec načítat nemusela.

A za druhé to generuje naprosto idiotské chybové hlášky. Což je věc, na kterou jsem dosti citlivý. Ze zprávy must be callable, string given těžko někdo pochopí, že typ string je použitý správně, jen neexistuje třída či metoda. Já bych to chápal tak, že funkce neakceptuje řetězce. Přitom třeba funkce call_user_func ve stejné situaci generuje zcela smysluplné chyby jako:

Warning: call_user_func() expects parameter 1 to be a valid callback, class 'Helpers' not found

Warning: call_user_func() expects parameter 1 to be a valid callback, class 'Helpers' does not have a method 'date'

Na chybu jsem upozornil, bohužel Rasmus je se současným chováním nadmíru spokojen. Setrvávat na zavádějících chybových hláškách je projevem arogance a hlouposti: namísto opravy raději uvedou ve zmatek statisíce programátorů a připraví je o spoustu času. Nette Framework rozhodně zprasený typ callable používat nebude.


Monkey patching v PHP

PHP přistupuje ke třídám způsobem známým ze staticky typovaných jazyků a neumožňuje monkey patching, tedy měnit za běhu metody tříd, kopírovat je mezi instancemi a podobně.

Abyste porozuměli, co mám na mysli, vytvořme třídu Greeting s metodou say():

class Greeting
{
    function __construct($name)
    {
        $this->name = $name;
    }

    function say($message)
    {
        echo "$message $this->name.";
    }
}

$g = new Greeting('John');
$g->say('Hello'); // Hello John.

V PHP neočekáváme, že by bylo možné metodu třeba uložit do proměnné či jiného atributu a poté zase zavolat:

$method = $g->say;
$g->greet = $method;
$g->greet('Hello');

Nebo dokonce za chodu přidávat metody nové:

$g->shout = function($message) {
    echo "$message $this->name!!!";
};

$g->shout('Hello'); // Hello John!!!

Na jedné straně je mi líto, že tohle PHP neumí, na straně druhé vidím ve statickém pojetí tříd podstatné výhody. A na straně třetí: v PHP lze tohle chování snadno emulovat.

Emulace dynamiky

Vytvořit funkci jako je výše uvedená shout() a vložit ji do proměnné objektu PHP už umí od verze 5.3. Ale abychom ji mohli zavolat běžným zápisem, musíme si vypomoci magickou metodou __call():

class Greeting
{
    function __call($name, $args)
    {
        if (!isset($this->$name) || !$this->$name instanceof Closure) {
            throw new Exception("Method $name not found.");
        }
        return call_user_func_array($this->$name->bindTo($this, $this), $args);
    }

    ...
}

A nyní už bude příklad s metodou shout() fungovat.

Abychom mohli stejně nakládat i se statickými metodami, jako byla třeba výše uvedená say(), doplníme ještě __get():

class Greeting
{
    function __get($name)
    {
        if (!method_exists($this, $name)) {
            throw new Exception("Property $name not found.");
        }
        return function() use ($name) {
            return call_user_func_array(array($this, $name), func_get_args());
        };
    }

    ...
}

A nyní bude fungovat i první příklad s přiřazením $method = $g->say a následným voláním.

Pro verzi 5.3

Uvedené příklady vyžadují PHP 5.4. Ve verzi 5.3 jsou closures ořezané a nesmí se v nich používat $this. Řešení by vypadalo trošičku jinak:

// for PHP 5.3

class Greeting
{
    function __call($name, $args)
    {
        if (!isset($this->$name) || !$this->$name instanceof Closure) {
            throw new Exception("Method $name not found.");
        }
        array_unshift($args, $this);
        return call_user_func_array($this->$name, $args);
    }

    function __get($name)
    {
        if (!method_exists($this, $name)) {
            throw new Exception("Property $name not found.");
        }
        return function() use ($name) {
            $args = func_get_args();
            return call_user_func_array(array(array_shift($args), $name), $args);
        };
    }

    ...
}

A namísto $this bychom uvnitř closure použili první argument, pojmenovaný třeba $self.

$g->shout = function($self, $message) {
    echo "$message $self->name!!!";
};

Nicméně narozdíl od 5.4 varianty má nyní funkce přístup jen k veřejným proměnným třídy.


Víte, co znamená $ v regulárním výrazu?

Nejprve otázka: mačne nebo nemačne?

$str = "123\n";

echo preg_match('~^\d+$~', $str);

Kdo z vás si myslí, že funkce vrátí false, protože regulární výraz běží v jednořádkovém režimu a nepovoluje v řetězci žádné jiné znaky krom číslic, ten se mýlil.

Malinko odbočím. Regulární výrazy v jazyce Ruby mají jednu nectnost (nesoulad s de facto standardem PERLu): znaky ^ a $ neoznačují začátek a konec řetězce, ale jen jednoho řádku v něm. Neznalost tohoto faktu může způsobit bezpečnostní zranitelnost, jak třeba upozorňuje dokumentace Rails. PHP se chová standardně, ale málokdo už ví, co přesně ono standardní chování znamená. Dokumentace meta-znaku $ je totiž nepřesná. (už opraveno)

Správně má být, že znak $ znamená konec řetězce nebo ukončující odřádkování; ve víceřádkovém režimu (modifikátor m) znamená konec řádku.

Skutečný konec řetězce chytá sekvence \z. Nebo je možné použít dolar společně s modifikátorem D.

$str = "123\n";
echo preg_match('~^[0-9]+$~', $str); // TRUE
echo preg_match('~^[0-9]+$~D', $str); // FALSE
echo preg_match('~^[0-9]+\z~', $str); // FALSE

DI a property injection

Dependency Injection je zřejmé předávání závislostí, tedy že se každá třída otevřeně hlásí ke svým závislostem, místo toho, aby je někde pokoutně získávala. Co kdyby se závislosti předávaly přímo do proměnných? Proberu úskalí a výhody property injection.

Property injection má jednu podstatnou výhodu: stručnost. Srovnejte:

class Foobar
{
    /** @var HttpRequest */
    private $httpRequest;

    /** @var Router */
    private $router;

    function __construct(HttpRequest $httpRequest, Router $router)
    {
        $this->httpRequest = $httpRequest;
        $this->router = $router;
    }

}

versus

class Foobar
{
    /** @var HttpRequest @inject */
    public $httpRequest;

    /** @var Router @inject */
    public $router;

}

Proměnné musíme definovat tak jako tak. Zpravidla u nich uvádíme i anotaci @var a příslušný datový typ. Je lákavé si ušetřit práci a místo psaní rutinního kódu konstruktoru nebo metody inject() doplnit prosté @inject. Property injection kromě minimálního režijního kódu navíc parádně řeší problém s předáváním závislostí a dědičností.

Použití anotace představuje jinou konvenci pro předání závislostí. Zdůrazňuji slovo jinou, protože ať už vyjmenujeme závislosti jakožto argumenty metody nebo anotováním, jde o ekvivalentní činnost. Čímž oponuji názoru, že použití anotace představuje závislost na kontejneru. To v žádném případě není pravda, jde jen o konvenci, koneckonců dosud o kontejnerech nepadla řeč a ukázky dávají smysl.

Stejně tak se nedívejte na anotaci @inject jako nějakou odpornou magii, kterou musíte nastudovat, abyste ji mohli používat. Žádná magie tu není. Jde o obyčejné veřejné proměnné a anotace je jen doplňující informace pro programátora, říkající, že objekt vyžaduje tyto proměnné naplnit. (Nutno dodat, že Jakub Vrána reagoval na použití anotací u private proměnných, což magie je.)

V článku o předávání závislostí jsem se používání proměnných širokým obloukem vyhnul, protože mají vážné nedostatky:

  • public proměnné nezajistí typovou kontrolu
  • public proměnné nezajistí neměnnost
  • private proměnné nelze naplnit žádnou jazykovou konstrukcí
  • private proměnné nejsou součástí veřejného API – nejde tedy o deklaraci závislosti!
  • pro protected proměnné platí nevýhody obou

Ještě bych přidal, že anotace nejsou nativní součástí jazyka PHP a jde tedy o nestandardní konvenci, oproti třeba injektáži přes konstruktor.

Poznámka: vstřikování závislostí do privátních proměnných posvětila třeba Java EE 6 a je to skutečně ee. Třída své závislosti tají (private = neveřejný) a nelze ji instancovat jinak, než kontejnerem (závislost na kontejneru). Jde zcela proti smyslu Dependency Injection, jak je popsán v perexu tohoto článku, a také proti základnímu principu OOP, zapouzdření. Označil bych to jako „Inversion of Dependency Injection.“

Pro properly property injection bychom potřebovali once-write-only veřejnou proměnnou s typovou kontrolou. Kdyby tohle PHP umělo, nic by nebránilo je používat. Jenže PHP to neumí.

Emulace inject property

PHP to neumí, ale lze to emulovat!

Emulaci zajistíme pomocí magických metod __set a __get. Jak ale dosáhnout toho, aby se k public proměnné přistupovalo skrze tyto metody? Použijeme trik: v konstruktoru ji unsetneme. Proměnná zmizí a při přístupu k ní se již použijí magické metody.

Příklad implementace ve formě základní třídy Object by mohl vypadat třeba takto:

class Object
{
    private $injects = array();

    function __construct()
    {
        // následující analýza proměnných by se mohla kešovat
        $rc = new ReflectionClass($this);
        foreach ($rc->getProperties() as $prop) {
            if ($prop->isPublic() && strpos($prop->getDocComment(), '@inject')
                && preg_match('#@var\s+(\S+)#', $prop->getDocComment(), $m)
            ) {
                // unset property to pass control to __set() and __get()
                unset($this->{$prop->getName()});
                $this->injects[$prop->getName()] = array('value' => NULL, 'type' => $m[1]);
            }
        }
    }


    function __set($name, $value)
    {
        if (!isset($this->injects[$name])) {
            throw new Exception("Cannot write to an undeclared property $$name.");

        } elseif ($this->injects[$name]['value']) {
            throw new Exception("Property $$name has already been set.");

        } elseif (!$value instanceof $this->injects[$name]['type']) {
            throw new Exception("Property $$name must be an instance of {$this->injects[$name]['type']}.");

        } else {
            $this->injects[$name]['value'] = $value;
        }
    }


    function __get($name)
    {
        if (!isset($this->injects[$name])) {
            throw new Exception("Cannot read an undeclared property $$name.");
        }
        return $this->injects[$name]['value'];
    }

}

pak stačí deklarovat výše uvedenou třídu Foobar jako potomka Object a vše bude fungovat standardně podle očekávání:

class Foobar extends Object
{
    /** @var HttpRequest @inject */
    public $httpRequest;

    /** @var Router @inject */
    public $router;

}

$fb = new Foobar;
$fb->router = new Router;

Navíc však máme zajištěnou neměnnost a typovou kontrolu:

$fb->router = new Router;
// Exception: Property $router has already been set.

$fb->httpRequest = new Router;
// Exception: Property $httpRequest must be an instance of HttpRequest.");

Čistá cesta nebo prasárna?

Zkusme se zamyslet nad tím, co vlastně anotace @inject představuje: hint pro programátora, že proměnnou má při vytváření objektu nastavit a že ji později nesmí měnit. Anotace @var pak nařizuje typ.

Je na programátorovi, aby dodržel kontrakt. Stejně jako v případě anotace @private v PHP 4 nebo JavaScriptu, či anotace @return v současném PHP. Jde o pravidla, u nichž se předpokládá, že je programátor dodrží, aniž to lze na úrovni interpreteru ověřit.

Třída Object rozšiřuje PHP o schopnost kontroly za běhu, usnadní tedy identifikaci chyb. Je to vychytávka navíc. Z mého pohledu tedy akceptovatelná cesta k použití property injection v PHP. Možná by se dalo uvažovat nad zařazením do Nette\Object a legitimizace této injektáže v Nette.


Všechny části:


DI a předávání závislostí presenterům

V Nette jsme narazili na zajímavý problém, který úzce souvisí s článkem Dependency Injection a předávání závislostí a komplikacemi způsobenými dědičností.

Stěžejním bodem tvorby aplikací v Nette je vytváření vlastních presenterů, tedy potomků třídy Nette\Application\UI\Presenter (dále Presenter s velkým P). Ta má své závislosti. Byl bych rád, aby uživatelé stavěli aplikace v souladu s DI a tedy se i jejich presentery hlásily ke svým závislostem.

Jenže jak to realizovat? Možná řešení:

  1. Presenter i potomek uvedou závislosti v konstruktoru – to by bylo peklo
  2. pro každou závislost se použije samostatný setter – zbytečně moc režijního kódu
  3. použije se jedna metoda pro závislosti Presenteru a jedna pro potomka
    1. Presenter využije konstruktor, potomek metodu inject()
    2. potomek využije konstruktor, Presenter metodu inject()
    3. nebudeme do toho tahat konstruktor, použijí se jiné metody
  4. použije se třída PresenterDependencies sdružující závislosti do jedné proměnné
  5. injektování přímo do proměnných – nevím, jak pojmout

ad 1) Jak už jsem psal v odkazovaném článku, pokud by Presenter i jeho potomek pro předání závislostí použili konstruktor, musel by se ten překrývající postarat o předání všech rodičovských závislostí. Ale jakmile se rodičovské závislosti změní (refactoring Presenteru), všechny třídy se rozbijí. Neakceptovatelné.

ad 2) Zda předávat všechny závislosti jednou metodou nebo zda vytvořit metodu/injektor pro každou závislost zvlášť, je v principu jedno. Protože nemám rád, když je moc režijního kódu a zbytečně bobtná API, volím raději jednu metodu.

ad 3) Šikovně vypadají přístupy 3a a 3b, tedy použít konstruktor pro závislosti jedné strany a metodu inject() pro závislosti druhé. O tom, který způsob použít, jsme se přeli s Honzou Tichým. On preferoval způsob 3b s tím, že když už nemáme žádné ideální řešení, zvolme z možných to, které bude funkční, pokud uživatel začne věc řešit intuitivně, aniž by věděl o nějaké metodě inject(). A intuitivně použije konstruktor.

Namítal jsem, že ho tak utvrdíme v mylném dojmu, že předek, tedy třída Presenter, vlastně žádné své závislosti nemá, jelikož si programátor v konstruktoru předává jen ty své. Ale jde víceméně o banalitu.

Horší je, že konstruktor potomka je volán nad objektem, který ještě není plně inicializován, tj. nejsou mu předány závislosti rodiče. Zkrátka se volá ještě před voláním inject(). Pokud by uživatel v konstruktoru použil funkci, která nějakou takovou závislost vyžaduje (například getUser()), výsledkem bude zavádějící chybová hláška Fatal Error: Call to a member function xyz on a non-object nebo nějaká podobná. Což není dobře.

Kdyby se uživatel zeptal, jak má tedy situaci vyřešit a v konstruktoru plné inicializace docílit, odpovědí by bylo: nejde to, přesuň kód do metody startup(). (Pokud nejsem srozumitelný, podívejte se na příklad níže.) Tímto problémem řešení 3a netrpí.

ad 5) injektování přímo do proměnných s anotací @inject by bylo vůbec nejšikovnější. Naprosté minimum režijního kódu, konec problémů s dědičností. Má to ale svá úskalí: obsah public proměnných nelze kontrolovat, private proměnné nejsou součástí veřejného API (jak je plnit?) nicméně k tomuto tématu se ještě v nějakém dalším článku vrátím.

Nakonec jsme se s Vaškem Purchartem domluvili na šalamounském řešení 4, kdy se všechny závislosti budou předávat konstruktorem s tím, že závislosti třídy Presenter sbalíme do jediného objektu PresenterDependencies, ten už se bude předávat snadno a při změně závislostí Presenteru se nic nerozbije.

Pokud budu chtít do svého presenteru předat objekt ArticleFacade, bude to vypadat takto:

class ArticlePresenter extends Presenter
{
    function __construct(Nette\Application\UI\PresenterDepedencies $pd, ArticleFacade $af)
    {
        parent::__construct($pd);
        $this->articleFacade = $af;

        // nyní je objekt plně inicializován a lze volat třeba:
        if (!$this->getUser()->isLoggedIn()) { ... }
    }
}

Podstatné je, že všechny závislosti budou předány přímo v konstruktoru, objekt bude ihned inicializován a nebudou se zavádět žádné novoty v podobě metod inject. Jak Vašek psal, jde o ústupek pohodlnosti v zájmu přehlednosti: „Většinou v Nette vyhrávala vždy pohodlnost … mám za to, že v rámci plánovaných změn a refaktoringů by Nette mělo tuhle houpačku mírně zhoupnout směrem k přehlednosti za cenu absolutní pohodlnosti.“

Poznámka: aby bylo evidentní, že třída PresenterDependencies je jen workaround nedostatku v návrhu jazyka a že vlastně nemá sémantický význam v objektovém modelu, nevadilo by mi ji implementovat takto triviálně:

class PresenterDependencies
{
    private $services;

    function __construct(Application\Application $application, ..., Security\User $user)
    {
        $this->services = func_get_args();
    }

    function get()
    {
        return $this->services;
    }
}


abstract class Presenter
{
    function __construct(PresenterDependencies $pd)
    {
        list($this->application, ..., $this->user) = $pd->get();
    }

}

Konec dobrý, všechno dobré. Vašek připravil pull request a já si uvědomil…

Slepá ulička

…že tohle řešení je vlastně nejhorší ze všech. Proč?

Udělal jsem si výčet faktorů, které chci u jednotlivých řešení sledovat (pořadí nesouvisí s důležitostí):

  • pohodlnost
  • intuitivnost
  • blbuvzdornost
  • moment inicializace
  • rozšiřitelnost (dědičnost)

ad pohodlnost: Povinné předání objektu PresenterDependencies je zdánlivá drobnost, ale troufám si tvrdit, že tohle by byl důvod, proč mnozí DI nepoužijí a raději si ArticleFacade vytáhnou ze service locatoru. Uvedený type hint je nejdelší název třídy z celého frameworku a psát ho je za trest.

Představil jsem si sám sebe, jak tohle ukazuju na školení a připadal bych si jako kokot. Jako bych školil nějaký Zend. Musel bych se ptát sám sebe, proč to neděláme nějak lépe.

ad intuitivnost: Honza položil otázkou, co se stane, pokud člověk začne věc řešit intuitivně. Dopadne to špatně. Nenapadne ho, že existuje nějaké PresenterDepedencies a že je ho potřeba předat. Nepředá ho, Presenter nezíská závislosti a bude v poloinicializovaném stavu. V nečekané chvíli vyskočí nějaký divoký nicneříkající Fatal Error.

S tím velmi úzce souvisí otázka blbuvzdornosti. I když budu vědět, že musím volat parent::__construct() (vždycky volejte!), mohu na to snadno zapomenout a bude problém. V případě presenteru by bylo možné (a vhodné) implementovat kontrolu, zda byl volán, stejně jako je tomu u metody startup(), ale problém je, že řešení 4 jde této chybě naproti.

Programátor také může udělat chybu v type hintu (všimli jste si, že jsem ji v příkladu udělal?) nebo jej neuvede vůbec. Sice jsem se snažil poladit chybové hlášky DI kontejneru, ale ty ve své obecnosti nikdy nemohou být dokonalé a programátor bude stejně zmaten.

Pod momentem inicializace myslím okamžik, kdy je objekt plně inicializován. PresenterDependencies ho dokázal dostat do konstruktoru, jiné řešení tohle neumí. Nicméně výhodu v případě presenterů relativizuje fakt, že uživatelé jsou stejně zvyklí úvodní kód dávat do metody startup(), kde už objekt plně inicializován je.

Pátý a dosud opomíjený faktor hodnotí, jak se řešení vypořádá s existencí více úrovní dědičnosti. Což je v případě Nette obvyklé, tj. vytvořit si nějaký abstraktní BasePresenter a teprve od něj dědit. Co když bude mít BasePresenter také nějaké závislosti? Řešení 4 předpokládá, že bychom je také uváděli v konstruktoru třídy BasePresenter a znovu opakovali v jeho potomcích. Otravnost by se tím zvyšovala.

Ven ze slepé uličky

Udělal jsem si tabulku, která u každého řešení uvádí, jak vyhovuje jednotlivým faktorům. Jasným vítězem se ukázalo být 3c. To konstruktor vůbec nepoužívá a každá třída si své závislosti předá metodou (nejlépe finální) s názvem začínajícím na inject. (Viz implementace.)

PresenterFactory, která má za úkol vytvářet instance presenterů, postupuje tak, že po vytvoření objektu zavolá všechny jeho metody inject v pořadí od třídy Presenter k potomkům. Tedy programátor si v každé třídě vytvoří metodu nazvanou inject() nebo podobně, a v ní uvede jednotlivé závislosti. Nemusí předávat nic navíc, minimalizuje se možnost chyby. Z hlediska pohodlnosti, rozšiřitelnosti a blbuvzdornosti je tak učiněno za dost.

Typický příklad předávání závislostí do presenterů bude v Nette vypadat takto:

class BasePresenter extend Presenter
{
    function injectBase(User $user, ...)
    {
        $this->user = $user;
        ...
    }
}

class ArticlePresenter extends BasePresenter
{
    function inject(ArticleFacade $af)
    {
        $this->articleFacade = $af;
    }
}

A co z hlediska intuitivnosti? Co když uživatel použije pro předání závislostí konstruktor, protože o žádných metodách inject() neví? Nevadí, fungovat to bude. Pochopitelně v konstruktoru nebude stále možné volat např. metodu getUser(), ale tuto daň rád zaplatím.

Celá věc by šla zjednodušit injektováním přímo do proměnných, třeba časem k tomu najedeme klíč. Do té doby se mi 3c jeví nejschůdnější.


DI a předávání závislostí

Víte, že Dependency Injection je zřejmé předávání závislostí, tedy že se každá třída otevřeně hlásí ke svým závislostem, místo toho, aby je někde pokoutně získávala. Otázka zní, jak se k nim hlásit a jak je předávat.

K předávání závislostí můžeme využít konstruktor:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
    {
        $this->httpRequest = $httpRequest;
        $this->router = $router;
        $this->session = $session;
    }

}

$foobar = new Foobar($hr, $router, $session);

Nebo metody:

class Foobar
{
    private $httpRequest, $router, $session;

    function setHttpRequest(HttpRequest $httpRequest)
    {
        $this->httpRequest = $httpRequest;
    }

    function setRouter(Router $router)
    {
        $this->router = $router;
    }

    function setSession(Session $session)
    {
        $this->session = $session;
    }
}

$foobar = new Foobar;
$foobar->setSession($session);
$foobar->setHttpRequest($hr);
$foobar->setRouter($router);

Nebo přímo naplnit jednotlivé proměnné:

class Foobar
{
    /** @var HttpRequest */
    public $httpRequest;

    /** @var Router */
    public $router;

    /** @var Session */
    public $session;
}

$foobar = new Foobar;
$foobar->session = $session;
$foobar->httpRequest = $hr;
$foobar->router = $router;

Které řešení je nejlepší? Aby článek nebyl neúměrně dlouhý, odkážu se na Předávání závislostí od Vaška Purcharta, tak si jej přečtěte, protože budu navazovat tam, kde končí.

Takže znovu: které řešení je nejlepší? Kdyby byly ekvivalentní, bylo by nejspíš to poslední, protože kód třídy je nejkratší a kratší kód minimalizuje možnost vzniku chyby a šetří čas při psaní i čtení. Nicméně řešení ekvivalentní nejsou. Jsou spíše diametrálně odlišené.

Immutability tedy neměnnost

Immutable object je objekt, který nemění svůj stav od chvíle, co byl vytvořen. Nevěřili byste, kolik problémů objektového návrhu se dá vyřešit jen tím, že se objekty stanou neměnné. Ale to je téma na jiný článek.

Prakticky vždy budeme chtít, aby závislosti objektu byly neměnné. A v tomto směru se jednotlivé varianty předávání liší. Veřejné (public) proměnné můžeme změnit kdykoliv a změnu nelze detekovat, což je zcela diskvalifikuje ze hry a dále už s touto variantou nebudu vůbec počítat. A to ani nemluvím o chybějící typové kontrole. (Viz také úvaha nad tím, jak by se dalo property injection řešit.)

Mohlo by vás napadnout nahradit public za private a vložit do nich závislosti některým z nízkoúrovňových triků (třeba pomocí reflexe), ale takové obcházení vlastností jazyka do obecných úvah o DI nepatří. Privátní proměnné nejsou součástí veřejného API třídy a nelze se jimi hlásit k závislostem. A také nehackujme jazyk, dokud to není nutné.

Neměnnost bychom si u metod mohli zajistit sami:

function setRouter(Router $router)
{
    if ($this->router) {
        throw new InvalidStateException('Router has already been set.');
    }
    $this->router = $router;
}

A protože metoda není klasický obecný setter, tj. lze ji volat jen jednou, nelze očekávat existenci getteru a můžeme její volání považovat za povinné, mohla by používat jiné názvosloví. Například prefix inject, v tomto případě injectRouter().

Vytvořili bychom tedy pro větší srozumitelnost konvenci, že závislosti předáváme metodami inject.

(Musím zdůraznit, že se bavíme o konvenci užitečné pro programátora, o žádných DI kontejnerech v článku nepadlo ani slovo. Pochopitelně by se jí dalo s úspěchem využít i v kontejnerech, nicméně je naprosto zásadní uvědomit si, co je příčinou a co důsledkem.)

Používání metod pro injektáž má svá úskalí:

  • musíme sami zajistit neměnnost
  • špatně se odhaluje okamžik, kdy jsou nastaveny všechny závislosti, abychom provedli inicializaci objektu
  • měli bychom také ověřovat, že se některé závislosti neopomněly nastavit
  • režijní kód bude poměrně ukecaný a dlouhý

Všechny tyto nedostatky řeší už z principu injektáž přes konstruktor, proto vychází jako nejvhodnější.

(…Tedy, ehm, neřeší… Ale k tomu se hnedle dostaneme.)

Constructor hell

Nenápadný problém předávání závislostí přes konstruktor tkví v tom, že nemáme žádné vodítko, v jakém pořadí jsou parametry uvedeny. Napadá mě snad leda řadit je abecedně (divné, co?). Pokud by dvě závislosti byly stejného typu, potom v pořadí source, destination apod.

Byť nám s tímto problémem může pomoci napovídání v IDE nebo automaticky generované kontejnery, nic to nemění na tom, že metoda s nejasnými parametry snižuje srozumitelnost kódu.

Jakožto líny člověk neoblibuji ani ty strojově se opakující přiřazování v těle konstruktoru. Jako zkratku lze použít:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
    {
        list($this->httpRequest, $this->router, $this->session) = func_get_args();
    }

}

Ale pokud by byla poslední závislost nepovinná, mohlo by to skončit u Notice: Undefined offset.

Uvažuji nad sepsáním RFC pro PHP, aby bylo možné používat zápis:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $this->httpRequest, Router $this->router, Session $this->session)
    {
    }

}

Nicméně tohle jsou jen syntaktické libůstky oproti kruciálnímu problému s dědičností.

Co se stane, když vytvoříme potomka:

class Barbar extends Foobar
{
    private $logger;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session, Logger $logger)
    {
        parent::__construct($httpRequest, $router, $session);
        $this->logger = $logger;
    }

}

Jak vidno, konstruktor potomka musí:

  • vyjmenovat závislosti rodiče
  • zavolat rodičovský konstruktor

To je v pořádku, závislosti rodiče jsou i jeho dědictvím. Jenže neexistuje mechanismus, kterým by se dalo volání rodičovského konstruktoru vynutit. Jednou z nejprotivnějších chyb se tak stane opomenutí volání parent::__construct. Takže předpoklad, že konstruktor už z principu vynucuje předání závislostí, je vlastně chybný. Konstruktor se dá snadno obejít.

Bez zajímavosti není, že zdáním je i neměnnost, protože nic nebrání zavolat na hotovém objektu $barbar->__construct(...) a protlačit mu jiné závislosti. Měl by tedy konstruktor testovat, zda není volán podruhé? Kašlete na to, konstruktor se prostě znovu volat nesmí. Otázka konvence.

Největší průšvih nastane ale ve chvíli, kdy provedu refactoring třídy Foobar, jehož důsledkem bude změna závislostí. Bude nutné přepsat konstruktory všech potomků. Jistě, je to logické, ale v praxi může jít o fatální zádrhel. Pokud například rodičem bude třída z frameworku (např. Presenter), jejíž potomky píše každý uživatel frameworku, fakticky se tak znemožní její vývoj, protože zásah do závislostí by byl kolosálním BC breakem.

Řada z výhod konstruktorové injektáže se rozplynula jak pára nad hrncem. Pokud se zdálo, že volání konstruktoru je vynuceno jazykem (silné a bezpečné), zatímco volání metod inject jen konvencí (opomenutelné), tak najednou se ukazuje, že to není zcela pravda.

Další možnosti

Možností, která částečně obchází problém konstruktoru a dědičnosti, je použití třídy FooDependencies zmíněné v článku Dependency Injection versus Service Locator:

class FoobarDependencies
{
    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
}

class Foobar
{
    function __construct(FoobarDependencies $deps)
}

class Barbar extends Foobar
{
    function __construct(FoobarDependencies $deps, Logger $logger)
    {
        parent::__construct($deps);
        $this->logger = $logger;
    }
}

Když se změní závislosti rodičovské třídy Foobar, nemusí to nutně rozbít všechny potomky, protože se předávají v jedné proměnné. Běda ale, pokud ji předat zapomenou… Navíc tento způsob vyžaduje největší množství režijního kódu (dokonce celou režijní třídu).

Nebo lze závislosti rodičovské třídy Foobar předávat metodami a konstruktor uvolnit pro potomky. Rodič by se pak fakticky inicializoval až po volání těchto metod, takže konstruktor potomka by se volal nad neinicializovaným objektem. To není dobré.

A co obráceně, závislosti rodičovské třídy Foobar předávat konstruktorem a potomka metodami? To eliminuje všechny problémy, krom toho, že se těžko odhalí okamžik, kdy jsou nastaveny všechny závislosti (kvůli inicializaci objektu) a zda jsou vůbec nastaveny.

A co kdyby se všechny závislosti potomka nastavily jedinou metodou inject()? To by nejspíš vyřešilo všechny komplikace.

Nicméně stále jde jen o dvojstupňový případ rodič – potomek. Pro každého dalšího potomka by bylo třeba přijít s novou injektovací metodou a byl by problém zajistit, aby byly volány ve správném pořadí.

Dovedu si proto představit, že by vzniklo nové čisté řešení využívající nějaké PHP magie uvnitř třídy, která by ušetřila psaní režijního kódu, elegantně exponovala závislosti a předávala je do proměnných. Ty by mohly být označené třeba anotací @inject, nicméně šlo by o anotaci určenu pro tuto vnitřní implementaci, nikoliv o hint pro DI kontejner. Efekt by to mělo ve chvíli, kdyby se z toho stala obecněji uznávaná konvence, jinak to bude jen magie.

tl;dr

Předávání závislostí různými cestami má svá úskalí. Použití metod vyžaduje velké množství režijního kódu. Není od věci tyto metody pojmenovávat jiným prefixem, než obecné settery, kupříkladu lze použít inject. Poskytne to totiž důležitou informaci pro programátora, sekundárně ji může využít i DI kontejner.

Pokud nepoužíváte dědičnost, je zpravidla nejšikovnější závislosti předat skrze konstruktor a PHP by mohlo v příštích verzích syntaxi ještě o něco zjednodušit. Pokud ale do hry vstoupí dědičnost, je najednou všechno jinak. Ukazuje se, že dokonalý obecný mechanismus asi ani neexistuje. Možná by nebylo od věci zkusit nějaký, byť za využití PHP magie, vymyslet.


Všechny části: