Инъекция зависимостей 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());
}
}
Есть ли причина не делать этого? Я пропустил какую-то магию Пимпла, которая не сработает, если мы идем по этому маршруту?
Ответы
Ответ 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;
};
}
}