Własnoręczny Dependency Injection Container

0

Witam!

Piszę niewielką aplikację PHP bez wykorzystania frameworka (na początku go nie dodano a teraz sporo przepisywania). Więc naszła mnie chęć napisania na szybko kontenera DI. Okazało się że argumenty funkcji w PHP nie mogą przyjmować zmiennych jako wartości domyślnych co skomplikowało zadanie. Jednak coś tam wyskrobałem :

<?php


class DependencyInjectionContainer
{
    private static $objects = [];


    private static function register()
    {
        return Array(
            'PDO_',
            'UserRepository',
            'User'
        );
    }


    public static function boot()
    {
        $register = self::register();
        foreach ($register as $item)
            self::$objects[$item] = 0;
    }

    public static function setInstance($class, $object)
    {
        if (! isset(self::$objects[$class]))
            throw new Exception('There is no class "'.$class.'" in DI container');

        self::$objects[$class] = $object;

    }

    public static function Inject()
    {
        $function_parameters = [];
        $response = [];

        $caller = self::caller();
        $parameters = null;

        if (isset($caller['class']))
            $parameters = self::getReflectionParameters($caller['class'], $caller['function']);
        else
            $parameters = self::getReflectionParametersFunction($caller['function']);

        $i=0;
        foreach($parameters as $parameter)
        {
            $class = $parameter->getClass()->name;
            $value = isset($caller['args'][$i]) ? $caller['args'][$i] : null;
            $name = $parameter->getName();

            $function_parameters[$i]['name'] = $name;
            $function_parameters[$i]['class'] = $class;
            $function_parameters[$i]['value'] = $value;
            $i++;
        }



        foreach ($function_parameters as $parameter)
        {
            if (isset($parameter['class']) && self::is_registered_class($parameter['class']))
                if (is_null($parameter['value']))
                    $response[$parameter['name']] = self::getInstance($parameter['class']);

        }
        return $response;
    }

    private static function getInstance($class)
    {
        if(!self::$objects[$class])
            self::$objects[$class] = new $class;

        return self::$objects[$class];
    }

    private static function is_registered_class($_key)
    {
        foreach (self::$objects as $key => $value)
        {
            if (strtolower($key) == strtolower($_key))
                return true;
        }
        return false;
    }

    private static function caller($level = 1)
    {
        $dbt=debug_backtrace(null,$level+2);
        return $caller = isset($dbt[$level+1]) ? $dbt[$level+1] : null;
    }

    private static function getReflectionParameters($class, $method)
    {
        $reflection = new ReflectionMethod($class, $method);
        return $reflection->getParameters();
    }

    private static function getReflectionParametersFunction($function)
    {
        $reflection = new ReflectionFunction($function);
        return $reflection->getParameters();
    }
}

Przykład użycia:

<?php

class PDO_{}

class UserRepository{
    public function __construct(PDO_ $pdo=null)
    {
        extract( DependencyInjectionContainer::Inject() ); // Ta linia jest wymagana wszędzie gdzie chcemy korzystać z DI
    }

    public function getUser($id){
        return Array(
            'id' => 1,
            'name' => 'Karol'
        );
    }
}

class User{
    public function __construct(UserRepository $ur = null)
    {
        extract( DependencyInjectionContainer::Inject() );

        $this->ur = $ur;
    }

    public function showUser()
    {
        $user = $this->ur->getUser(1);
        var_dump($user);
    }
}

// Budowanie aplikacji
include 'DependencyInjectionContainer.php';
DependencyInjectionContainer::boot();

// jakieś tam pdo tworzone gdzieś w aplikacji
$pdo = new PDO_();
DependencyInjectionContainer::setInstance('PDO_', $pdo);


function Controller(User $user = null)
{
    extract( DependencyInjectionContainer::Inject() );

    $user->showUser();
}

Controller();

Do każdej funkcji zawierającej linie:

extract( DependencyInjectionContainer::Inject() );

kontener automatycznie wstrzykuje zdefiniowane w register() zależności.
Jeżeli chcemy wstrzykiwać jakąś konkretną instancje możemy użyć serInstance()

Chciałem jeszcze pozbyć się tej funkcji extract (wciągnąć ją do środka Inject) ale znalazłem tylko coś takiego

 you can modify the "parent scope" by changing the global EG(active_symbol_table)

https://stackoverflow.com/questions/3127824/how-to-set-a-variable-in-the-caller-scope-like-the-extract-function

Klasa jest statyczna ponieważ chciałem aby był do niej łatwy dostęp w funkcjach. Może jest jakiś lepszy sposób?

**Teraz pytanie do bardziej doświadczonych kolegów ;) Jak to wygląda waszym zdaniem? Gra warta świeczki? Może jakieś kardynalne błędy...
Czy odpuścić sobie i pobrać gotowy kontener, jakie dodatkowe zalety będzie miał? **

0

Strasznie niecodziennie do tego podszedłeś - widziałeś kiedyś w ogóle zastosowanie kontenera DI z prawdziwego zdarzenia w PHPie?

Czy odpuścić sobie i pobrać gotowy kontener, jakie dodatkowe zalety będzie miał?

Znasz pojęcie wynajdywania koła od nowa? ;-)

1

To jest DI/IoC, ktore kiedys napisalem i calkiem zgrabnie sie z tego korzysta:


<?php

declare(strict_types=1);

namespace Paneric\DIContainer;

use Psr\Container\ContainerInterface;

class DIContainer implements ContainerInterface
{
    private $container = [];

    private $mapper = [];

    public function get($id)
    {
        if ($this->has($id)) {
            return $this->getInstance($id);
        }

        return null;
    }

    public function has($id): bool
    {
        if (isset($this->mapper[$id]) && !empty($this->mapper[$id])) {
            return true;
        }

        return false;
    }

    public function merge(array $dependencies): void
    {
        if (empty($dependencies)) {
            return;
        }

        foreach ($dependencies as $deps) {
            $this->mapper = array_merge($this->mapper, $deps);
        }
    }

    private function getInstance(string $id)
    {
        if (isset($this->container[$id]) && !empty($this->container[$id])) {
            return $this->container[$id];
        }

        $this->container[$id] = $this->mapper[$id]($this);

        return $this->container[$id];
    }
}

A to przyklad injekcji (poprawiony skoro ktos to ocenil :D). Najpierw tworzymy kontener:

$container = new DIContainer();

$container->merge([
    require PROJECT_FOLDER.'Core\\config\\dependencies\\application.php',
]);

Dopiero teraz wlasciwa injekcja (application.php):

<?php

use Psr\Container\ContainerInterface;

return [
    Paneric\NNOptimizer\Network\Processor::class => function (ContainerInterface $nc) {
        $settings = $nc->get('settings');
        $inputSequencer = $nc->get(
            $settings['process_looper']['input_sequencer_name']
        );
        return new Paneric\NNOptimizer\Network\Processor($settings, $inputSequencer);
    },

    Paneric\NNOptimizer\Core\Helper\ArrayHelper::class => function (ContainerInterface $nc) {
        return new Paneric\NNOptimizer\Core\Helper\ArrayHelper();
    },

    Paneric\NNOptimizer\Network\Input\SlideSequencer::class => function (ContainerInterface $nc) {
        $arrayHelper = $nc->get(Paneric\NNOptimizer\Core\Helper\ArrayHelper::class);
        $settings = $nc->get('settings');
        return new Paneric\NNOptimizer\Network\Input\SlideSequencer($arrayHelper, $settings);
    },
];

1 użytkowników online, w tym zalogowanych: 0, gości: 1