phpFashion

Rubrika PHP

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.


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.



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.



DI versus Service Locator

Když se mluví o Dependency Injection, bývá zmiňován i service locator, jako jakési zlé dvojče. O co se vlastně jedná?

Dependency Injection jsem v prvním článku popsal jako zřejmé předávání závislostí, tedy že se každá třída hlásí ke svým závislostem v konstruktoru nebo jiné metodě, místo toho, aby je někde v těle pokoutně získávala z globálního přístupového bodu. Dodržování tohoto principu vede ke srozumitelnějšímu a předvídatelnějšímu kódu.

Nicméně na vás číhá nástraha v podobě service locatoru.

Service locator je velechytrý objekt, který umí vrátit veškeré závislosti, které třída potřebuje, nebo i nepotřebuje. Pokud by si všechny třídy předávaly jeden service locator, předaly by si tak v jediném parametru všechny závislosti.

class Authenticator
{
	private $locator;

	function __construct(ServiceLocator $locator)
	{
		$this->locator = $locator;
	}

	function authenticate($user, $pass)
	{
		$database = $this->locator->getDatabase();
		$database->query(...)
	}
}

Bohužel Service Locator není v souladu s DI.

Proč? Není v souladu s tím, že předávání závislostí je zřejmé a že se třída ke svým závislostem otevřeně hlásí. Třída Authenticator

  • potřebuje databázi, ale hlásí se k velmi obecnému service locatoru, což je v naprostém nepoměru vůči tomu, co skutečně potřebuje
  • že potřebuje zrovna databázi se nedá zjistit jinak, než zkoumáním její implementace

Třída se tedy musí hlásit ke všem svým závislostem a právě jen k nim. Jinak o svých závislostech lže.

(Může nastat situace, kdy požadovat service locator je korektní: pokud ho třída skutečně jako takový potřebuje. Třeba kvůli jeho vizualizaci apod.)

Co naopak service locator není

Občas někdo pojmenuje validní konstrukci termínem service locator a na základě toho ji odsoudí. Podobný styl uvažování je zavádějící. Znamená, že něco používáme či zavrhujeme a už nevím proč vlastně.

Důležité je si uvědomit, co je tím špatným na service locatoru, tj. proč jej řadíme mezi antipatterny, a ověřit, jestli naše konstrukce netrpí stejnými vadami. Tedy diskuse o tom, zda jde o service locator nebo ne, je pak podružná.

Jako příklad si ukažme refaktoringu třídy Foo se třemi závislostmi:

class Foo
{
	function __construct(A $a, B $b, C $c)
}

$foo = new Foo($a, $b, $c);

Závislosti vytkneme do jedné (immutable) třídy:

class FooDependencies
{
	function __construct(A $a, B $b, C $c)
}

class Foo
{
	function __construct(FooDependencies $d)
}

$foo = new Foo(new FooDependencies($a, $b, $c));

Z hlediska DI jsou obě alternativy korektní, třídy se hlásí ke všem svým závislostem a právě jen k nim. Neplatí tu námitky proti service locatoru. Samozřejmě je otázka, zda uvedený refactoring byl opodstatněný a správný, ale to už je jiný příběh.

Obdobně v úvodním článku Co je Dependency Injection, kde jsem skupinu závislostí třídy Article v presenteru zredukoval na továrničkou ArticleFactory, nejsou na místě obavy, že továrnička je service locator. Nevykazuje totiž jeho negativní rysy. Stejně tak i příklad v článku Dependency Injection versus Lazy loading, kde Authenticator byl nahrazen za AuthenticatorAccessor, neboť presenter chtěl službu získat skrze lazy loading.

Jak vidno, v DI nejde jen o jakékoliv předávání závislostí. Musí být zřejmé. A pokud si nejste jisti, zda používáte korektní objektový návrh v souladu s DI, udělejte si test „slepým API“. Skryjte si těla metod nebo vygenerujte API dokumentaci a závislosti jednotlivých tříd musí být stále jednoznačně patrné.



DI versus Lazy loading

Lazy loading je návrhový vzor, který odkládá vytváření objektů až do okamžiku, kdy je aplikace skutečně potřebuje. Jak to skloubit s Dependency Injection, které naopak rádo objekty získává už v konstruktorech?

Jak jsme si řekli, Dependency Injection je zřejmé předávání závislostí, tedy že se každá třída ke svým závislostem otevřeně hlásí v inicializačních metodách, obvykle přímo v konstruktoru.

Mějme SignPresenter, který zobrazuje a obhospodařuje formulář pro přihlašování se do aplikace. Po odeslání formuláře se zavolá metoda formSubmitted(), která ověří přihlašovací údaje pomocí autentikátoru. Zjednodušený kód by vypadal takto:

class SignPresenter
{
	function formSubmitted()
	{
		// $GLOBALS['authenticator']->authenticate(...)
		Registry::getAuthenticator->authenticate(...)
	}
}

V souladu s principem DI si nebudeme autentikátor čarovat z globálního prostředí, ale přiznáme se k této závislosti v konstruktoru:

class SignPresenter
{
	private $auth;

	function __construct(Authenticator $auth)
	{
		$this->auth = $auth;
	}

	function formSubmitted()
	{
		$this->auth->authenticate(...);
	}
}

V praxi však narazíme na zásadní problém: na jedno odeslání formuláře bude připadat třeba 1000 jeho zobrazení. Nicméně autentikátor se bude inicializovat vždy. A jelikož ověřuje vůči databázi, bude v souladu s DI vyžadovat v konstruktoru objekt PDO, jehož vytvoření způsobí připojení se k databázovému serveru.

Tedy každé zobrazení formuláře bude doprovázeno načtením tříd v 99.9 % případů nepotřebných, vytvářením nepotřebných objektů a zbytečným připojením k databázi.

To je závažný nedostatek, který nám vyřeší právě lazy loading. Jednou z možností je vytvořit tzv. proxy, objekt implementující rozhraní Authenticator a obalující původní autentikátor, který jej ovšem instancuje až při zavolání metody authenticate(). Druhou možností, která však vyžaduje změnu presenteru, je si místo autentikátoru předat továrničku, která nám jej vyrobí později:

class SignPresenter
{
	private $authFactory;
	private $auth;

	function __construct(AuthenticatorFactory $authFactory)
	{
		$this->authFactory = $authFactory;
	}

	function getAuthenticator()
	{
		if (!$this->auth) {
			$this->auth = $this->authFactory->create();
		}
		return $this->auth;
	}

	function formSubmitted()
	{
		$this->getAuthenticator()->authenticate(...);
	}

}

interface AuthenticatorFactory
{
	/** @return Authenticator */
	function create();
}

Metoda getAuthenticator() zajišťuje, že budeme ve třídě SignPresenter pracovat s jedinou instancí autentikátoru.

A tady by mohl článek skončit. Jenže nekončí.

Továrničku nebrat!

Zdálo se vám použití továrničky jako dobrý nápad či jako úplná samozřejmost? Pak na chvíli zbrzděte.

Zkuste se zamyslet nad rozdílem mezi:

  • budu potřebovat objekt získat
  • budu potřebovat objekt vyrobit

My objekt budeme potřebovat získat, což je obecnější formulace než vyrobit.

Vraťme se k prvnímu DI příkladu, kde získáváme autentikátor přes konstruktor. Co říká hlavička konstruktoru? Že se mu má předat autentikátor. Nikoliv, že se má vyrobit a poté předat. Metoda, která vytváří instanci SignPresenter, může autentikátor získat jakýmkoliv způsobem (a třeba jej vyrobit), ale samotnému presenteru je po tom kulový. Ten ho jen vyžaduje a neptá se po původu.

Jenže řešení s továrničkou kromě podpory lazy loadingu předjímá i způsob získávání objektu: bude se vyrábět. Takže zatímco v prvním případě dostává SignPresenter jeden autentikátor, ve druhém případě dostává nástroj, kterým si může vyrobit autentikátorů více. Ale o to nám nešlo. Nestojíme před potřebou vyrábět autentikátory, řešíme potřebu lazy-loadingu jediného autentikátoru.

Zdánlivě korektní nasazení továrny je chybné, za chvíli se k tomu ještě vrátím. Správné řešení je místo továrny předat něco, co nám autentikázor později vrátí (ne nutně vyrobí). Říkejme tomu třeba Getter nebo Accessor (v žádném případě nejde o Service locator):

class SignPresenter
{
	private $authAccessor;

	function __construct(AuthenticatorAccessor $authAccessor)
	{
		$this->authAccessor = $authAccessor;
	}

	function formSubmitted()
	{
		$this->authAccessor->get()->authenticate(...);
	}

}

interface AuthenticatorAccessor
{
	/** @return Authenticator */
	function get();
}

Kód presenteru se nám navíc zjednoduší, protože nepotřebujeme metodu getAuthenticator(). Samotný accessor totiž zajišťuje, že pracujeme stále se stejnou instancí.

Jak AuthenticatorFactory tak AuthenticatorAccessor uvádím jako rozhraní, neboť na implementaci vůbec nezáleží.

Zkusme se podívat, jak může v praxi vypadat kupříkladu testování presenteru:

// vyrobíme si mockovaný autentikátor
$auth = ...;

// a potřebujeme ho dostat do presenteru
$presenter = new SignPresenter(new TrivialAuthenticatorAccessor($auth));

kde TrivialAuthenticatorAccessor je vskutku triviální:

class TrivialAuthenticatorAccessor implements AuthenticatorAccessor
{
	private $instance;

	function __construct(Authenticator $instance)
	{
		$this->instance = $instance;
	}

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

Pokud bychom místo accessoru šli původně navrhovanou cestou továrničky, měli bychom docela problém, jak $auth do presenteru propašovat. (Mimochodem ukázkový příklad, jak testování vede k lepšímu návrhu kódu).

Přičemž jakoukoliv továrničku lze do podoby accessoru snadno přetransformovat, například pomocí obecného CallbackAccessor:

abstract class CallbackAccessor
{
	private $instance;
	private $callback;

	function __construct(/*callable*/ $callback)
	{
		$this->callback = $callback;
	}

	function get()
	{
		if (!$this->instance) {
			$this->instance = call_user_func($this->callback);
		}
		return $this->instance;
	}
}

Jakýkoliv callback pak můžeme přetavit do podoby AuthenticatorAccessor:

class CallbackAuthenticatorAccessor extends CallbackAccessor implements AuthenticatorAccessor
{}

$presenter = new SignPresenter(new CallbackAuthenticatorAccessor(function(){
	return ...;
}));

před 13 lety v rubrice PHP


Co je Dependency Injection?

Dependency Injection je prostá a skvělá technika, která vám pomůže psát mnohem srozumitelnější a předvídatelnější kód.

Kód téměř vždy píšeme pro jiné: spolupracovníky, uživatele našich open source knihoven nebo o pár let starší sebe sama. Abychom předešli nepříjemným WTF momentům při jeho používání, je dobré dbát na srozumitelnost. Ať už v pojmenování identifikátorů, výřečnosti chybových zpráv nebo návrhu rozhraní tříd. A ke srozumitelnosti bych přidal ještě předvídatelnost. Schválně, očekávali byste, že volání $b->hello() v této ukázce může nějak změnit stav zcela nezávislého opodál stojícího objektu $a?

$a = new A;

$b = new B;
$b->hello();

To by bylo divné, že? Jo, kdyby oba objekty byly nějak explicitně propojeny, třeba kdybychom volali $b->hello($a) (tj. s argumentem $a) nebo předtím nastavili $b->setA($a), tak by mezi oběma objekty existovala vazba a dalo by se očekávat, že $b může něco provádět s $a. Ale bez toho by to bylo nečekané, nesportovní a matoucí…

Říkáte si, že to je přece jasné? Že jen blázen by takový magický kód psal? Tak se podívejte na následující příklad, který v různých obměnách najdete v řadě příruček „blog in 15 minutes with our amazing framework“:

$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();

Třída Article reprezentuje článek na blogu a metoda save() nám jej uloží. Kam? Asi do databázové tabulky. Skutečně? Co když ho uloží do souboru na disk? A pokud do databáze, tak do jaké tabulky? K jaké databázi se vlastně připojí? Ostré nebo testovací? K SQLite nebo k Mongu? Pod jakým účtem?

Jde o stejný případ, jako v předchozí ukázce, jen pod $a si představte (neviditelný) objekt reprezentující databázové spojení a $b->hello() nahraďte za $article->save(). Co se však nezměnilo, je nepředvídatelnost a nesrozumitelnost kódu.

Museli bychom se podívat, jak je implementovaná metoda save(), abychom zjistili, kam se data ukládají. Zjistili bychom, že si šahá do nějaké globální proměnné udržující databázové spojení. Museli bychom pátrat dál, kde se v kódu databázové spojení vytváří, a pak bychom teprve měli obrázek o tom, jak vše funguje.

Nicméně, i kdybychom pochopili, jak je vše provázané, byl by oříšek do toho zasáhnout. Jak třeba za účelem testování uložit článek jinam? Asi by to vyžadovalo změnit nějakou statickou proměnnou. Ale nerozbili bychom tím něco jiného?

Jaj, statické proměnné jsou zlo. Vytvářejí skryté závislosti, kvůli kterým nemáme kód pod kontrolou. Kód má pod kontrolou nás ☹

Řešení je Dependency Injection

Dependency Injection (dále jen DI) neboli zřejmé předávání závislostí říká: odeberte třídám zodpovědnost za získávání objektů, které potřebují ke své činnosti.

Budete-li psát třídu vyžadující ke své činnosti databázi, nevymýšlejte uvnitř jejího těla, odkud ji získat (tj. ze žádné globální proměnné, statické metody, singletonu, registru atd.), ale požádejte o ni v konstruktoru nebo jiné metodě. Popište závislosti svým API. Nebudete muset tolik přemýšlet a získáte srozumitelný a předvídatelný kód.

A to je vše. To je celé slavné DI.

Pojďme si to ukázat v praxi. Začneme u nešťastné implementace třídy Article:

class Article
{
	public $id;
	public $title;
	public $content;

	function save()
	{
		// uložíme do databáze
		// …ale kde databázové spojení seženu?
		// GlobalDb::getInstance()->query() ?
	}
}

Autor metody save() musel řešit nelehkou otázku, kde vzít připojení k databázi. Kdyby použil DI, nemusel by nad ničím uvažovat (a to mají programátoři rádi), neboť DI dává jasnou odpověď: pokud potřebuješ databázi, ať ti ji někdo dodá. Jinými slovy: nic nesháněj, ať se postará někdo jiný.

class Article
{
	public $id;
	public $title;
	public $content;

	function save(Nette\Database\Connection $connection)
	{
		$connection->table('articles')->insert(array(
			'title' => $this->title,
			'content' => $this->content,
		));
	}
}

Takže aplikace principů DI znamená jen to, že jsme předali $connection jako parametr metodě? Jako vážně? Ano. Jako vážně.

Užití třídy Article se pochopitelně nepatrně změní:

$article = new Article;
$article->title = ...
$article->content = ...
$article->save($connection);

Díky této změně je nyní z kódu naprosto zřejmé, že se článek uloží do databáze, a taky do které databáze.

Řešení pomocí DI tak přestavuje win-win situaci: autor třídy Article nemusel řešit, kde objekt-databázi sežene, její uživatel nemusel pátrat, kde ho programátor sehnal. Z kódu je nyní zřejmé, že článek se uloží do databáze a lze velmi snadno nechat jej uložit do databáze jiné.

Můžete ale přijít s celou řadou námitek. Kde se třeba vezme v posledním příkladu proměnná $connection? DI opakuje: „ať se postará někdo jiný“. Databázové spojení zkrátka dodá ten, kdo zavolá uvedený kód.

Nojo, ale teď to vypadá, že používání DI značně zkomplikuje kód, protože kvůli vytvoření instance Article musíte uchovávat a předávat databázové spojení. Navíc časem může ve třídě Article vzniknout potřeba nějaká data formátovat a v souladu s DI bude potřeba předávat ještě další objekty. Komplikovalo by nám to například kontrolery:

class ArticlePresenter
{
	function __construct(Connection $connection, TextFormatter $formatter, ...)
	{
		$this->connection = $connection;
		$this->formatter = $formatter;
		...
	}

	function createArticle()
	{
		return new Article($this->connection, $this->formatter, ...);
	}
}

Když bude mít presenter co do činění s dalšími podobnými třídami jako je Article, bude mít haldu závislostí. Ba co víc, Article by měla projít refaktoringem, kdy nahradíme databázi obecnějším úložištěm IArticleStorage, nebo jí zodpovědnosti za ukládání sebe sama úplně zbavíme a delegujeme to na novou třídu ArticleRepository. To by znamenalo upravit aplikaci na mnoha místech; přinejmenším všude, kde se vytváří instance Article. Co s tím?

Elegantní řešení jsou továrničky. Místo ručního (tj. operátorem new) vytváření objektů Article si je necháme vyrábět továrničkou. A místo všech závislostí třídy Article si budeme předávat jen jeho továrničku:

class ArticleFactory
{
	function __construct(Connection $connection, TextFormatter $formatter, ...)
	{
		$this->connection = $connection;
		$this->formatter = $formatter;
		...
	}

	function create()
	{
		return new Article($this->connection, $this->formatter, ...);
	}
}

Původní ArticlePresenter se nám nejen krásně zjednoduší a zároveň bude jeho API lépe vystihovat podstatu, tedy že ArticlePresenter nepotřebuje žádnou databázi, chce prostě jen pracovat s články. Z takového refaktoringu má člověk vyloženě dobrý pocit:

class ArticlePresenter
{
	function __construct(ArticleFactory $articleFactory)
	{
		$this->articleFactory = $articleFactory;
	}

	function createArticle()
	{
		return $this->articleFactory->create();
	}
}

V praxi se ukazuje, že každá třída mívá jen několik málo závislostí, které ji předáváme. Důsledné používání DI tak navzdory obavám kód nijak nekomplikuje a výhody, jako je srozumitelnost, jednoznačně převažují nad tou troškou psaní navíc v konstruktorech.

Lze také namítnout, že netransparentní chování původní třídy Article, která článek uložila neznámo kam, vlastně vůbec nevadí, pokud ho metoda load() zase bude umět načíst. Vtip je v tom, že nad kódem navrženým podle DI principu vždycky můžeme takto fungující obálku vytvořit. Ale naopak toho docílit nelze.

Dependency Injection je technika z rodiny Inversion of Control (IoC), do které patří i Service locator. Ten bývá zmiňován jako jakési zlé dvojče. Proč se mu vyhnout si řekneme v dalším článku DI versus Service Locator.


Článek volně vychází z dokumentace Dependency Injection na stránkách Nette Framework a byl aktualizován 24. 9. 2012.

Řada lidí namítala, že zvolený příklad je nevhodný, že by třída Article neměla být takto provázaná s databází. Zcela s nimi souhlasím a o to víc mi zvolený příklad vyhovuje. Ukazuje totiž, jak DI mimoděk upozorňuje na chybný návrh, protože zdůrazňuje vazby, které byly dříve skryté.


Diskuse ke článku o lazy loadingu

Pod článkem Dependency Injection versus Lazy loading se vyrojilo velké množství komentářů, dle mého však nešlo o přínosnou diskusi, ale spíš o nepochopení a špičkování se bez jakýchkoliv hmatatelných argumentů. Aby neubližovaly samotnému článku, přenesl jsem je pod tento spot.

Lazy loading je technika, která musí být (stejně jako jakákoliv jiná technika!) pochopitelně použita jen tam, kde je pro ni vhodné místo. Nebylo předmětem článku tohle řešit (vydalo by to možná na celý seriál). Reálným problémem, kterému programátor přecházející od singletonů a Service Locatorů k Dependency Injection čelí, je, jak vůbec lazy loading použít, pokud jej skutečně potřebuje. Service Locator ho totiž nabízí by design. Na což právě zmíněný článek dává odpověď.



Export z Facebooku do Google Calendar

Jak vyexportovat události z Facebooku do Google Calendar, aby vše fungovalo dobře?

Otevřete si Facebook a klikněte na „Události“. Adresa s exportem událostí ve formátu iCalendar je trošku nečekaně ukrytá pod ikonkou lupy:

Adresu si zkopírujte do schránky:

Odkaz bychom mohli hned dát to Google kalendáře, ale narazili bychom na několik nedostatků. Google nedokáže korektně zobrazit neveřejnou událost, místo ní uvidíte pouze nicneříkající slovo „nedostupný“:

Google nedokáže ani filtrovat události podle toho, zda jsme potvrdili účast. Takže kalendář se nám plní nejen událostmi, na které jsme účast potvrdili, ale i těmi, na které nás někdo zatím jen pozval. Navíc u událostí v letním čase uvádí o hodinu více.

Vše lze vyřešit jednoduchým filtrem facebookfilter.php, který si umístěte na server:

// load events from Facebook
$s = file_get_contents("http://www.facebook.com/ical/u.php?$_SERVER[QUERY_STRING]");

// PREG requires \n line breaks
$s = str_replace("\r\n", "\n", $s);


// fix private events bug in Google Calendar
$s = str_replace('CLASS:PRIVATE', 'CLASS:PUBLIC', $s);


// remove unconfirmed events
function filterEvents($m) {
	return strpos($m[0], 'PARTSTAT:NEEDS-ACTION') ? '' : $m[0];
}
$s = preg_replace_callback('#^BEGIN:VEVENT\n(.*?)^END:VEVENT\n#sm', 'filterEvents', $s);


// fix daylight savings bug in Google Calendar
function fixTime($m) {
	$zone = new DateTimeZone('Europe/Prague');
	$time = new DateTime($m[2], new DateTimeZone('UTC'));
	$fix = $zone->getOffset($time) - $zone->getOffset(new DateTime('20000101', $zone));
	$time->modify("-$fix seconds");
	return $m[1] . $time->format('Ymd\THis\Z');
}
$s = preg_replace_callback('#^(DTSTART:|DTEND:)(.+)#m', 'fixTime', $s);


// revert line breaks back
$s = str_replace("\n", "\r\n", $s);

header('Content-Type: text/calendar; charset=utf-8');
echo $s;

Do Google kalendáře přidáme adresu našeho filtru se stejnými parametry v URL, jako měl původní skript od Facebooku:

A je to!


Nejkratší parse error v PHP

Jaký je nejkratší kód v PHP, který způsobí parse error? Má právě 5 znaků a vyžaduje povolené short_open_tag:

<?php

Po spuštění vyhodí:

Parse error: syntax error, unexpected $end of file in test.php on line 1

Syntaktickou chybu lze opravit přidáním mezery nebo odřádkování 🙂


phpFashion © 2004, 2025 David Grudl | o blogu

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.