Tento materiál není na fóru, páč neni určen k flejmvérování, nýbrž k seriózní diskuzi.
Dependency injection container
Představa Honzy Marka
Základní teze
O existenci DI ví jen minimum kódu.
(což nesplňují současné Contexty a Environmenty)
Dependency injection container
Asi tomu budu řikat ServiceContainer.
Co má umět?
- základní operace se službami
- ArrayAccess pro snadnější ovládání
- je možné vyměnit implementaci DI containeru
- načítat proměnné prostředí a konfiguraci
- definice závislostí (služba používá jiné služby)
- načítat služby je možné z více souborů pro případ modulární aplikace
- vychytanou konfiguraci, aby bylo potřeba co nejméně továrniček
- vícenásobné aliasy služeb
Konfigurace
ServiceContainer či ServiceContainerBuilder by měl načítat konfiguraci z PHP polí (možná objektů speciální třídy). Pak je jednoduché před to hodit načítání z čehokoliv, třeba z config.ini nebo z neon souborů s definicemi služeb.
Zásadní rozdíly oproti současné definici služeb:
- zrušit options, není dobré nařizovat, že veškeré parametry mají přijít v poli
- zrušení parametru run a možná jeho nahrazení univerzálnější tagováním služeb (tagy run, templatehelper, …)
- možnost definice setter injection
- možnost definice constructor injection
- myslím, že v současném nette nelze definovat aliasy již v konfiguračním souboru
- v budoucnu pokročilejší možnosti definice služeb
- možnost posílat jako parametry továrničce nebo konstruktoru jiné služby, proměnné prostředí nebo části konfigurace (bylo by vhodné sloučit proměnné prostředí a konfiguraci – z mého pohledu je to to samé)
- pro oslabení WTF faktoru bych rozdělil definici služeb pomocí názvu třídy a pomocí továrničky
- pamatovat na situaci, že továrničkou na servisy může být i metoda jiné servisy
Příklad:
array(
// služba vytvořená pomocí názvu třídy
"ISluzba1" => array("class" => "Sluzba1"),
// služba vytvořená pomocí továrničky, aliasy
"ISluzba2" => array("factory" => "Sluzba2::createInstance", "aliases" => array("Foo", "Bar")),
// možná upřednostnit symfonní způsob definice aliasů
// služba nemusí vědět, jaké všechny její aliasy budou potřeba
"Foo" => "@ISluzba2",
// autowiring (constructor injection)
"ISluzba3" => array("class" => "Sluzba3", "autowire" => TRUE),
// constructor injection
"ISluzba4" => array("class" => "Sluzba4", "arguments" => array(
123, // konstanta
"@ISluzba3", // služba
'%var%', // proměnná prostředí
'%appDir%/slozka/slozka', // text obsahující proměnnou prostředí
'%database%', // konfigurace
)),
// setter injection
"ISluzba5" => array("class" => "Sluzba4", "calls" => array(
"setNumber" => array(123),
"setFoo" => array("@Foo"),
"setVarAndConfig" => array('%var%', '%database%'),
)),
)
Speciální parametry (ne konstanty) by se posílaly jako text v nějakém speciálním tvaru. U proměnných musí být označen začátek i konec, aby se dal řetězec expandovat jako v současném Environment::expand. To by mělo fungovat u argumentů konstruktorů/továrniček/setterů, ale nahrazení za proměnnou či text z konfigurace by měl jít i u parametrů class a factory.
Implementace kontejneru
Zvážil bych rozdělení na třídy ServiceContainer a ServiceContainerBuilder. ServiceContainer by umožňovala nastavit službu jen buď jako hotový objekt nebo továrničku. ServiceContainerBuilder by služby uměl do kontejneru sypat ze sofistikovaných konfigurací v PHP polích či možná v objektech třídy Nette\DependencyInjection\Definiton nebo nějaké podobné.
Z mého pohledu ideální interface IContainer. Je jednodušší než současné IContext
interface IContainer
{
// $definiton by bylo buď pole nebo speciální třída, to jsem se ještě nerozhodl
public function addService($name, $definition); // nebo možná setService
public function getService($name);
public function hasService($name);
public function removeService($name);
}
Pokročilé funkce, o kterých lze do budoucna přemýšlet
Tady napíšu funkce, které jsou náročnější buď na implementaci nebo na pochopení (nelze říct, že prvotní myšlenka je ta nejsprávnější). Navíc se nejedná o věci bezpodmínečně potřebné, tak je lze odložit na později, až budeme mít více zkušeností s DI.
Autowiring
Aby mohl ServiceContainer podporovat autowiring, musí mít informaci o tom, která služba je implementovaná kterou třídou či jaké implementuje rozhraní. V Nette se nabízí možnost použít jako nosič této informace název (nebo alias) služby, protože služby bývají pojmenované jako rozhraní. To asi ale není nejčistější řešení, protože se jedná o jinou informaci. Z tohoto důvodu bych s implementací autowiringu nepospíchal a nechal ho na další fázi.
Tagování služeb
Služby by bylo možné otagovat a v nějakých částech aplikace si vytáhnout služby označené určitým tagem. Například by se na tagování dala převést současná volba run využívaná robotloaderem. Nebo by tag templatehelper mohl značit služby, co mohou být využity jako šablonové helpery.
Configurator
Možnost strčit právě vytvořenou instanci služby metodě, která ji donakonfiguruje.
$service = new Service;
ServiceConfigurator::configure($service);
V příkladu je konfigurátorem metoda
ServiceConfigurator::configure. Konfigurátory by se daly
používat jako doplňková možnost k setter a constructor injection.
Scope
Například místo toho, aby Context byl jinou instancí s jinými službami
v Environment i v aplikaci, tak by bylo možné nastavit, že některé
služby jsou dostupné jen ve scope application, do kterého by se
container v aplikaci přepnul. Tohle jsem vymyslel inspirován tím, že
v symfonním containeru je nějaké scope, o kterém nevím co dělá. Možná
přesně toto :-D Takže tohle je vysoce experimentální nápad.
Integrace do Nette
Application a Environment by měl mít stejnou instanci ServiceContaineru, což teď není. Respektive by Environment jakožto statická třída měl odejít do důchodu a mít pravomoc možná tak na vydání aktuální instance aplikace.
Injektování služeb do presenteru
V Nette již existuje PresenterFactory. Její výchozí implementace vytvoří objekt třídy, jejíž název odpovídá nějaké konvenci, a předává instanci contextu do presenteru. Zájemci si mohou implementaci změnit například tak, aby i presentery musely být službou a předávaly se jim jiné služby přes konstruktor.
Bootstrap
V bootstrapu by měla být poslední šance, jak ovlivnit container. Pak by se měl ServiceContainer předat aplikaci, která by jej zmrazila a distribuovala dál.
Poznámky
- Hack, který zavádí nějaké to DI do současné verze Nette (novější verze s autowiringem) – funguje na principu továrničky + options, které si továrnička přebere
- Testovací implementace v Nella Frameworku