POZOR! Článek jsem napsal před více jak rokem, a tudíž už nemusí reflektovat můj nynější názor nebo může být zastaralý.
Už to slyším… „Panebože, proč vytváříš něco, co tu je už hotové stokrát!“ Ale počkejte, mám k tomu pár důvodů.
Programátorům užívajících si pokročilejších frameworků jsou pojmy Dependency Injection a autowiring známý, a třeba jej i používají. Pro ty, co je neznají, doporučuji načíst :)
V rámci svého (doma provozovaného) sebevzdělávacího programu jsem se rozhodl, pokusit se, vytvořit si vlastní framework. Jedním z hlavních důvodů, proč se do něčeho takového vůbec pouštím je: naučit se něco nového, nikoli přijít s něčím naprosto revolučním.
Jakýsi návrh a první verzi mám již hotovou a přišlo mi přínosné si popsat, jak jednotlivé komponenty fungují a zveřejnit je, abych se okolní kritikou mohl něco přiučit. Začínám tedy s komponentou Dependency Injection kontejneru s automatickým autowiringem.
Hlavní myšlenky
- Chci, aby to bylo co nejjednodušší na používání.
- Nechci nic složitě konfigurovat
Z toho vyplývá, že se chci zbavit ručního psaní závislostí. V rámci jednoduchosti se všechny závislosti nadefinují jako parametry konstruktoru a všechny parametry budou objekty. Každá třída má v rámci kontejneru jedinou instanci. Takže použití by mělo být následují:
- Vytvořím si instanci kontejneru
- Vytáhnu si z něj objekt
- Zavolám její metodu (nebo s ní udělám cokoli jiného)
Nic víc nechci řešit. To, že jedna třída potřebuje pro své fungování jinou chci, aby za mne vyřešil právě ten kontejner.
Řešení
- Vytvořím si instanci kontejneru
- Dám kontejneru třídu, že které chci vrátit její instanci
- Kontejner se podívá jestli má třída nějaké závislosti
- Kontejner si každou ze závislostí projde a zjistí jestli nemá sama další závislosti, a pokud ano, opět si každou z nich projde a zanalyzuje, jestli také nemá nějaké závislosti a tak rekurzivně dál..
- Kontejner vrátí instanci třídy
Trochu jsem tápal, jak zjistit, jaké parametry vlastně v konstruktoru třída požaduje, aniž bych se právě dostal do stavu, kdy si to musím v konfiguraci nastavit. A dostavil aha efekt.
Odpovědí je reflexe. Konkrétně metoda getParameters třídy ReflectionMethod.
Takto vypadá mé řešení:
class Container
{
private $objects = [];
public function get(string $class_name)
{
if ($class_name === self::class) {
return $this;
}
if (!array_key_exists($class_name, $this->objects)) {
// Pokud není třída již inicializovaná, natáhnu si všechny její závislosti
$dependencies = $this->getDependencies($class_name);
$this->objects[$class_name] = new $class_name(...$dependencies);
}
return $this->objects[$class_name];
}
private function getDependencies(string $class_name, string $method_name = "__construct"): array
{
$dependencies = [];
$reflection_class = new \ReflectionClass($class_name);
if (!$reflection_class->hasMethod($method_name)) {
return $dependencies;
}
$parameters = $reflection_class->getMethod($method_name)->getParameters();
foreach ($parameters as $parameter) {
if ($parameter->getClass()) {
$dependencies[] = $this->get($parameter->getClass()->name);
} else {
// Neodpovídá pravidlu, kdy v konstruktoru mají být pouze závislosti
throw new \Exception("Depencency Injection: All parameters of $class_name::$method_name must be a class.");
}
}
return $dependencies;
}
}
A použítí je následovné:
class A
{
public function hello(string $world): string
{
return "hello " . $world;
}
}
class B
{
private $a;
public function __construct(A $a)
{
$this->a = $a;
}
public function render()
{
return $this->a->hello("world");
}
}
// Místo, abych napsal:
$a = new A();
$b = new B($a);
$b->render();
// Napíši toto:
$container = new Container();
$b = $container->get(B::class);
$b->render(); // hello world
Komponenta, včetně testu je publikovaná na GitHubu: https://github.com/gephart/dependency-injection.
A je dostupná i přes composer:
composer require gephart/dependency-injection
Znáte někoho, komu by článek mohl pomoct? Zasdílejte mu ho :)