Подтвердить что ты не робот

Инъекция зависимостей PHP - Pimple и т.д. - Зачем использовать ассоциативные массивы против геттеров?

Мы рассматриваем возможность интеграции контейнера для инъекций зависимостей в наш проект. Каждый DIC, на который я смотрел, использует ассоциативные массивы и/или магические методы. Например, здесь образец с страницы Pimple:

$container['session_storage'] = function ($c) {
    return new $c['session_storage_class']($c['cookie_name']);
};

$container['session'] = function ($c) {
    return new Session($c['session_storage']);
};

Есть ли причина для этого? Мне не нравятся строки в моем коде как что-то иное, чем буквальная строка, которая будет отображаться где-то. Вы теряете столько возможностей IDE (что делает код более сложным для поддержания, чего мы пытаемся избежать!).

Мои предпочтения были бы более похожими:

class Container {

    function getSessionStorage()
    {
        return new $this->getSessionStorageClass($this->getCookieName);
    }

    function getSession()
    {
        return new Session($this->getSessionStorage());
    }

}

Есть ли причина не делать этого? Я пропустил какую-то магию Пимпла, которая не сработает, если мы идем по этому маршруту?

4b9b3361

Ответ 1

"Магия" расширения ArrayAccess в Pimple заключается в том, что он полностью многоразовый и совместимый. Одной из важных особенностей Pimple как DIC является то, что определенный сервис может использовать ранее определенные сервисы и/или параметры. Пусть говорят (по какой-либо причине), что у вас есть объект Session, для которого требуется экземпляр Filter. Без DIC вы могли бы написать:

$session = new Session(new Filter);

С прыщами вы можете написать:

$pimple['filter'] = function($c) {
    return new Filter;
};
$pimple['session'] = function($c) {
    return new Session($c['filter']);
}

Pimple использует ранее зарегистрированную услугу "Фильтр" при создании объекта Session. Это преимущество не уникально для DIC, которое реализует ArrayAccess, но повторное использование очень полезно для повторного использования кода и совместного использования. Вы, конечно же, можете использовать хеддери/сеттеры для определенных сервисов или все из них, но преимущество повторного использования почти потеряно.

Другой вариант - использовать магические методы в качестве геттеров/сеттеров. Это даст DIC API больше похоже на то, что вы хотите в своем коде, и вы даже можете использовать их в качестве обертки над кодом Pimple ArrayAccess (хотя вам может быть лучше написать специально созданный DIC в этой точке). Обертка поверх существующих методов Pimple может выглядеть примерно так:

public function __call($method, $args) {
    if("set" === substr($method, 0, 3)) {
        return $this[substr($method, 3)];
    }
    if("get" === substr($method, 0, 3) && isset($args[0])) {
        return $this[substr($method, 3)] = $args[0];
    }
    return null;
}

Вы также можете использовать __set и __get, чтобы предоставить объектный доступ к службам и параметрам в DIC, например: (все еще обертывая методы Pimple ArrayAccess)

public function __set($key, $value) {
    return $this[$key] = $value;
}

public function __get($key) {
    return $this[$key];
}

Кроме того, вы можете полностью переписать DIC исключительно на использование магических методов и иметь вместо него объект-подобный синтаксис API вместо ArrayAccess, но это довольно легко понять:]

Ответ 2

Вы заботитесь о автозаполнении IDE, потому что вы собираетесь использовать свой контейнер в качестве Локатора служб, т.е. вы собираетесь называть свой контейнер.

Вы не должны этого делать в идеале. Шаблон локатора службы является анти-шаблоном: вместо того, чтобы вводить нужные вам зависимости (инъекция зависимостей), вы извлекаете их из контейнера. Это означает, что ваш код связан с контейнером.

Pimple (и его доступ к массиву) на самом деле не решает этого, поэтому я не отвечаю непосредственно на ваш вопрос, но я надеюсь, что это станет понятным.


Боковое примечание: какой "идеальный" способ? Инъекция зависимостей.

Никогда не используйте или не вызывайте контейнер, за исключением корневого приложения (например, для создания контроллеров). Всегда вводите нужные вам объекты (зависимости), а не вставляя весь контейнер.

Ответ 3

Pimple предназначен для доступа к массиву (он реализует интерфейс ArrayAccess). Если вы хотите использовать подобный методу интерфейс, просто добавьте Pimple и используйте __call() магический метод:

class Zit extends Pimple
{
    public function __call($method, array $args)
    {
        $prefix = substr($method, 0, 3);
        $suffix = isset($method[3])
                ? substr($method, 3)
                : NULL;

        if ($prefix === 'get') {
            return $this[$suffix];
        } elseif ($prefix === 'set') {
            $this[$suffix] = isset($args[0])
                           ? $args[0]
                           : NULL;
        }
    }
}

Использование:

$zit = new Zit();

// equivalent to $zit['Foo'] = ...
$zit->setFoo(function() {
    return new Foo();
});

// equivalent to ... = $zit['Foo']
$foo = $zit->getFoo();

Что касается того, почему Pimple не поставляется с этой функциональностью из коробки, я понятия не имею. Вероятно, просто чтобы это было максимально просто.


Edit:

Что касается автозаполнения IDE, они также не будут доступны с помощью таких магических методов. Некоторые редакторы позволяют вам давать подсказки doc-block, чтобы компенсировать это, используя @property и @method, я полагаю.

Ответ 4

Поскольку вы хотите повысить производительность и сохранить конфигурацию, единственным вариантом является создание кода контейнера DI.

Простой вариант - подготовить методы, которые вам понадобятся, и написать генератор. Что-то вроде этого (непроверенный код, только для вдохновения):

$config_file = 'config.ini';
$di_file = 'var/di.php';
if (mtime($config_file) > mtime($di_file) // check if config changed
    || mtime(__FILE__) > mtime($di_file)  // check if generator changed
{ 
    $config = parse_ini_file($config_file, true); // get DI configuration
    ob_start(); // or use fopen($di_file) instead
    echo "<", "?php\n",
        "class DIContainer {\n";
    foreach ($config_file as $service_name => $service) {
        // generate methods you want, use configuration in $service as much as possible
        echo "function create", $service_name, "() {\n",
             "  return new ", $service['class'], "();\n\n";
    }
    echo "}\n";
    file_put_contents($di_file, ob_get_contents());
    ob_end_clean();
}

require($di_file);
$dic = new DIContainer();

Использование:

$service = $dic->createSomeService();
// Now you have instance of FooBar when example config is used

Пример конфигурационного файла:

[SomeService]
class = "FooBar"

[OtherService]
class = "Dummy"

Ответ 5

В итоге мы перешли с гибридом двух подходов.

Внутри dic Pimple управляет вещами, внешние объекты извлекаются через геттеры.

например.

abstract class DicAbstract {

    /**
     * @var \Pimple
     */
    protected $_dic;

    /**
     * Initialise the pimple container
     */
    public function __construct()
    {
        $this->_dic = new \Pimple();

        $this->defineContainer();
    }

    /**
     * Define dependency items
     */
    protected abstract function defineContainer();
}

class Dic extends DicAbstract {

    /**
     * @return \Component\Error\Manager
     */
    public function errorManager()
    {
        return $this->_dic['errorManager'];
    }

    /**
     * @return SomethingElse
     */
    public function somethingElse()
    {
        return $this->_dic['somethingElse'];
    }

    /**
     * Define the container
     */
    protected function defineContainer()
    {
        $this->_dic['errorTypesGeneral'] = function() {
            return new \Component\Error\Type\General();
        };

        $this->_dic['errorTypesSecurity'] = function() {
            return new \Component\Error\Type\Security();
        };

        $this->_dic['errorManager'] = function($dic) {
            $errorManager = new \Component\Error\Manager();

            $errorManager->registerMessages($dic['errorTypesGeneral']);
            $errorManager->registerMessages($dic['errorTypesSecurity']);

            return $errorManager;
        };
    }

}