Vlastní DI container s autowiringem

PHP

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

  1. Chci, aby to bylo co nejjednodušší na používání.
  2. 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í:

  1. Vytvořím si instanci kontejneru
  2. Vytáhnu si z něj objekt
  3. 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í

  1. Vytvořím si instanci kontejneru
  2. Dám kontejneru třídu, že které chci vrátit její instanci
  3. Kontejner se podívá jestli má třída nějaké závislosti
  4. 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..
  5. 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 :)

Komentáře