Внедрение зависимостей/Расположение сервисов
Следующий пример немного длинный, но объясняет, почему использовать расположение сервисов и внедрение зависимостей. Сначала давайте представим, что мы разрабатываем компонент под названием SomeComponent. Он выполняет задачу, которая сейчас не важна. Наш компонент имеет зависимость, которая представляет собой подключение к базе данных.
В этом первом примере подключение создается внутри компонента. Этот подход непрактичен, так как мы не можем изменить параметры подключения или тип системы баз данных, потому что компонент работает только в созданном виде.
class SomeComponent
{
/**
* The instantiation of the connection is hardcoded inside
* the component, therefore it's difficult replace it externally
* or change its behavior
*/
public function someDbTask()
{
$connection = new Connection(array(
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo"
));
// ...
}
}
$some = new SomeComponent();
$some->someDbTask();
Чтобы решить эту проблему, мы создали установщик, который внедряет зависимость внешне, прежде чем использовать её. На данный момент это, похоже, хорошее решение:
class SomeComponent
{
protected $_connection;
/**
* Sets the connection externally
*/
public function setConnection($connection)
{
$this->_connection = $connection;
}
public function someDbTask()
{
$connection = $this->_connection;
// ...
}
}
$some = new SomeComponent();
//Create the connection
$connection = new Connection(array(
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo"
));
//Inject the connection in the component
$some->setConnection($connection);
$some->someDbTask();
Теперь представьте, что мы используем этот компонент в разных частях приложения, и нам нужно будет несколько раз создать подключение, прежде чем передать его компоненту. Использование некоторого глобального реестра, где мы получаем экземпляр подключения и не должны создавать его снова и снова, могло бы решить эту проблему:
class Registry
{
/**
* Returns the connection
*/
public static function getConnection()
{
return new Connection(array(
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo"
));
}
}
class SomeComponent
{
protected $_connection;
/**
* Sets the connection externally
*/
public function setConnection($connection)
{
$this->_connection = $connection;
}
public function someDbTask()
{
$connection = $this->_connection;
// ...
}
}
$some = new SomeComponent();
//Pass the connection defined in the registry
$some->setConnection(Registry::getConnection());
$some->someDbTask();
Теперь предположим, что нам нужно реализовать два метода в компоненте: первый всегда должен создавать новое подключение, а второй всегда должен использовать общее подключение:
class Registry
{
protected static $_connection;
/**
* Creates a connection
*/
protected static function _createConnection()
{
return new Connection(array(
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo"
));
}
/**
* Creates a connection only once and returns it
*/
public static function getSharedConnection()
{
if (self::$_connection===null){
$connection = self::_createConnection();
self::$_connection = $connection;
}
return self::$_connection;
}
/**
* Always returns a new connection
*/
public static function getNewConnection()
{
return self::_createConnection();
}
}
class SomeComponent
{
protected $_connection;
/**
* Sets the connection externally
*/
public function setConnection($connection)
{
$this->_connection = $connection;
}
/**
* This method always needs the shared connection
*/
public function someDbTask()
{
$connection = $this->_connection;
// ...
}
/**
* This method always needs a new connection
*/
public function someOtherDbTask($connection)
{
}
}
$some = new SomeComponent();
//This injects the shared connection
$some->setConnection(Registry::getSharedConnection());
$some->someDbTask();
//Here, we always pass a new connection as parameter
$some->someOtherDbTask(Registry::getNewConnection());
До сих пор мы видели, как внедрение зависимостей решало наши проблемы. Передача зависимостей в качестве аргументов вместо их создания внутри кода делает наше приложение более поддерживаемым и слабо связанным. Однако в долгосрочной перспективе этот вид внедрения зависимостей имеет некоторые недостатки.
Например, если компонент имеет много зависимостей, нам нужно будет создать несколько аргументов установщиков для передачи зависимостей или создать конструктор, который передаст их с большим количеством аргументов. Кроме того, каждый раз, когда мы создаём зависимости, прежде чем использовать компонент, наш код становится менее поддерживаемым, чем хотелось бы:
//Create the dependencies or retrieve them from the registry $connection = new Connection(); $session = new Session(); $fileSystem = new FileSystem(); $filter = new Filter(); $selector = new Selector(); //Pass them as constructor parameters $some = new SomeComponent($connection, $session, $fileSystem, $filter, $selector); // ... or using setters $some->setConnection($connection); $some->setSession($session); $some->setFileSystem($fileSystem); $some->setFilter($filter); $some->setSelector($selector);
Представьте, что нам нужно создать этот объект во многих частях нашего приложения. Если вам когда-либо не понадобится ни одна из зависимостей, нам нужно будет пройтись по всему коду, чтобы удалить параметр в конструкторе или установщике, где мы внедрили код. Чтобы решить эту проблему, мы опять возвращаемся к глобальному реестру для создания компонента. Однако это добавляет новый уровень абстракции перед созданием объекта:
class SomeComponent
{
// ...
/**
* Define a factory method to create SomeComponent instances injecting its dependencies
*/
public static function factory()
{
$connection = new Connection();
$session = new Session();
$fileSystem = new FileSystem();
$filter = new Filter();
$selector = new Selector();
return new self($connection, $session, $fileSystem, $filter, $selector);
}
}
Минуточку, мы вернулись к началу, опять создаём зависимости внутри компонента! Мы можем продолжить и найти способ решить эту проблему каждый раз. Но, похоже, мы постоянно возвращаемся к плохим практикам.
Практичный и элегантный способ решения этих проблем — использование контейнера для зависимостей. Контейнеры действуют как глобальный реестр, который мы видели ранее. Использование контейнера для зависимостей в качестве моста для получения зависимостей позволяет нам уменьшить сложность нашего компонента:
class SomeComponent
{
protected $_di;
public function __construct($di)
{
$this->_di = $di;
}
public function someDbTask()
{
// Get the connection service
// Always returns a new connection
$connection = $this->_di->get('db');
}
public function someOtherDbTask()
{
// Get a shared connection service,
// this will return the same connection everytime
$connection = $this->_di->getShared('db');
//This method also requires an input filtering service
$filter = $this->_di->get('filter');
}
}
$di = new Phalcon\DI();
//Register a "db" service in the container
$di->set('db', function() {
return new Connection(array(
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "invo"
));
});
//Register a "filter" service in the container
$di->set('filter', function() {
return new Filter();
});
//Register a "session" service in the container
$di->set('session', function() {
return new Session();
});
//Pass the service container as unique parameter
$some = new SomeComponent($di);
$some->someTask();
Теперь компонент просто обращается к нужному ему сервису, когда ему это необходимо; если ему не нужен сервис, который даже не инициализирован, он экономит ресурсы. Компонент теперь сильно слабо связан. Например, мы можем заменить способ создания подключений, их поведение или любой другой аспект, и это не повлияет на компонент.
Наш подход
Phalcon\DI — это компонент, реализующий внедрение зависимостей и расположение сервисов, и сам является контейнером для них.
Поскольку Phalcon сильно слабо связан, Phalcon\DI необходим для интеграции различных компонентов фреймворка. Разработчик также может использовать этот компонент для внедрения зависимостей и управления глобальными экземплярами различных классов, используемых в приложении.
В принципе, этот компонент реализует шаблон обратного управления зависимостями. Применяя его, объекты не получают свои зависимости с помощью установщиков или конструкторов, а запрашивают инжектор зависимостей сервиса. Это уменьшает общую сложность, поскольку существует только один способ получения необходимых зависимостей в компоненте.
Кроме того, этот шаблон повышает возможность тестирования кода, тем самым делая его менее подверженным ошибкам.
Регистрация сервисов в контейнере
Фреймворк или разработчик могут регистрировать сервисы. Когда компонент А требует компонент B (или экземпляр его класса) для работы, он может запросить компонент B из контейнера вместо создания нового экземпляра компонента B.
Этот способ работы предоставляет нам множество преимуществ:
- Мы можем легко заменить компонент компонентом, созданным нами или сторонней компанией.
- Мы полностью контролируем инициализацию объекта, позволяя устанавливать эти объекты по мере необходимости, прежде чем передавать их компонентам.
- Мы можем получить глобальные экземпляры компонентов структурированным и унифицированным способом
Сервисы могут быть зарегистрированы с помощью нескольких типов определений:
//Create the Dependency Injector Container
$di = new Phalcon\DI();
//By its class name
$di->set("request", 'Phalcon\Http\Request');
//Using an anonymous function, the instance will be lazy loaded
$di->set("request", function() {
return new Phalcon\Http\Request();
});
//Registering an instance directly
$di->set("request", new Phalcon\Http\Request());
//Using an array definition
$di->set("request", array(
"className" => 'Phalcon\Http\Request'
));
Также разрешен синтаксис массивов для регистрации сервисов:
//Create the Dependency Injector Container
$di = new Phalcon\DI();
//By its class name
$di["request"] = 'Phalcon\Http\Request';
//Using an anonymous function, the instance will be lazy loaded
$di["request"] = function() {
return new Phalcon\Http\Request();
};
//Registering an instance directly
$di["request"] = new Phalcon\Http\Request();
//Using an array definition
$di["request"] = array(
"className" => 'Phalcon\Http\Request'
);
В приведенных выше примерах, когда фреймворку необходимо получить данные запроса, он запрашивает сервис под идентификатором «запрос» в контейнере. Контейнер, в свою очередь, вернёт экземпляр требуемого сервиса. В дальнейшем разработчик может заменить компонент, когда это необходимо.
Каждый из методов (продемонстрированных в приведенных выше примерах), используемых для установки/регистрации сервиса, имеет преимущества и недостатки. За разработчиком и конкретными требованиями остаётся выбор, какой из них использовать.
Установка сервиса по строке проста, но лишена гибкости. Установка сервисов с помощью массива предлагает гораздо больше гибкости, но делает код более сложным. Лямбда-функция представляет собой хороший баланс между двумя предыдущими методами, но может привести к большей необходимости в обслуживании, чем ожидалось.
Phalcon\DI предлагает отложенную загрузку для каждого хранимого сервиса. Если разработчик не выбирает явно создать объект и сохранить его в контейнере, любой объект, хранящийся в нём (через массив, строку и т. д.), будет загружаться по мере необходимости (т. е. создаваться только при запросе).
Простая регистрация
Как уже упоминалось, существует несколько способов регистрации сервисов. Мы называем их простыми:
Строка
Этот тип ожидает имя допустимого класса, возвращая объект указанного класса. Если класс не загружен, он будет создан с помощью автозагрузчика. Этот тип определения не позволяет указать аргументы для конструктора класса или параметры:
// return new Phalcon\Http\Request();
$di->set('request', 'Phalcon\Http\Request');
Объект
Этот тип ожидает объект. Поскольку объект не нуждается в разрешении, так как это уже объект, можно сказать, что это не совсем внедрение зависимостей, однако это полезно, если вы хотите гарантировать, что возвращаемая зависимость всегда будет одним и тем же объектом/значением:
// return new Phalcon\Http\Request();
$di->set('request', new Phalcon\Http\Request());
Замыкания/Анонимные функции
Этот метод предоставляет большую свободу в построении зависимости по вашему желанию, однако изменить некоторые параметры внешне трудно без полной перестройки определения зависимости:
$di->set("db", function() {
return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
"host" => "localhost",
"username" => "root",
"password" => "secret",
"dbname" => "blog"
));
});
Некоторые ограничения можно преодолеть, передав дополнительные переменные в среду замыкания:
//Using the $config variable in the current scope
$di->set("db", function() use ($config) {
return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
"host" => $config->host,
"username" => $config->username,
"password" => $config->password,
"dbname" => $config->name
));
});
Сложная регистрация
Если необходимо изменить определение сервиса без создания/разрешения сервиса, необходимо определить сервисы с использованием синтаксиса массива. Определение сервиса с помощью определения массива может быть немного более громоздким:
//Register a service 'logger' with a class name and its parameters
$di->set('logger', array(
'className' => 'Phalcon\Logger\Adapter\File',
'arguments' => array(
array(
'type' => 'parameter',
'value' => '../apps/logs/error.log'
)
)
));
//Using an anonymous function
$di->set('logger', function() {
return new \Phalcon\Logger\Adapter\File('../apps/logs/error.log');
});
Оба приведенных выше определения сервиса дают одинаковый результат. Однако определение массива позволяет при необходимости изменять параметры сервиса:
//Change the service class name
$di->getService('logger')->setClassName('MyCustomLogger');
//Change the first parameter without instantiating the logger
$di->getService('logger')->setParameter(0, array(
'type' => 'parameter',
'value' => '../apps/logs/error.log'
));
Кроме того, с помощью синтаксиса массива можно использовать три типа внедрения зависимостей:
Внедрение зависимостей в конструктор
Этот тип внедрения передает зависимости/аргументы в конструктор класса. Предположим, у нас есть следующий компонент:
namespace SomeApp;
use Phalcon\Http\Response;
class SomeComponent
{
protected $_response;
protected $_someFlag;
public function __construct(Response $response, $someFlag)
{
$this->_response = $response;
$this->_someFlag = $someFlag;
}
}
Сервис можно зарегистрировать таким образом:
$di->set('response', array(
'className' => 'Phalcon\Http\Response'
));
$di->set('someComponent', array(
'className' => 'SomeApp\SomeComponent',
'arguments' => array(
array('type' => 'service', 'name' => 'response'),
array('type' => 'parameter', 'value' => true)
)
));
Сервис «response» (Phalcon\Http\Response) разрешается для передачи в качестве первого аргумента конструктора, а второй — это логическое значение (true), которое передаётся как есть.
Внедрение зависимостей через установщики
Классы могут иметь установщики для внедрения необязательных зависимостей. Наш предыдущий класс можно изменить для принятия зависимостей с помощью установщиков:
namespace SomeApp;
use Phalcon\Http\Response;
class SomeComponent
{
protected $_response;
protected $_someFlag;
public function setResponse(Response $response)
{
$this->_response = $response;
}
public function setFlag($someFlag)
{
$this->_someFlag = $someFlag;
}
}
Сервис с внедрением зависимостей через установщики можно зарегистрировать следующим образом:
$di->set('response', array(
'className' => 'Phalcon\Http\Response'
));
$di->set('someComponent', array(
'className' => 'SomeApp\SomeComponent',
'calls' => array(
array(
'method' => 'setResponse',
'arguments' => array(
array('type' => 'service', 'name' => 'response'),
)
),
array(
'method' => 'setFlag',
'arguments' => array(
array('type' => 'parameter', 'value' => true)
)
)
)
));
Внедрение зависимостей через свойства
Менее распространённая стратегия — внедрение зависимостей или параметров непосредственно в общедоступные атрибуты класса:
namespace SomeApp;
use Phalcon\Http\Response;
class SomeComponent
{
public $response;
public $someFlag;
}
Сервис с внедрением зависимостей через свойства можно зарегистрировать следующим образом:
$di->set('response', array(
'className' => 'Phalcon\Http\Response'
));
$di->set('someComponent', array(
'className' => 'SomeApp\SomeComponent',
'properties' => array(
array(
'name' => 'response',
'value' => array('type' => 'service', 'name' => 'response')
),
array(
'name' => 'someFlag',
'value' => array('type' => 'parameter', 'value' => true)
)
)
));
Поддерживаемые типы параметров включают следующие:
| Тип | Описание | Пример |
|---|---|---|
| параметр | Представляет буквальное значение, которое нужно передать в качестве параметра | массив(‘тип’ => ‘параметр’, ‘значение’ => 1234) |
| сервис | Представляет другой сервис в контейнере сервисов | массив(‘тип’ => ‘сервис’, ‘имя’ => ‘запрос’) |
| экземпляр | Представляет объект, который должен быть построен динамически | массив(‘тип’ => ‘экземпляр’, ‘имя_класса’ => ‘DateTime’, ‘аргументы’ => массив(‘сейчас’)) |
Получение сервиса, определение которого сложное, может быть немного медленнее, чем простые определения, которые мы видели ранее. Однако они обеспечивают более надёжный подход к определению и внедрению сервисов.
Смешивание различных типов определений допускается; каждый может решить, какой способ регистрации сервисов наиболее подходит для потребностей приложения.
Получение сервисов
Получение сервиса из контейнера сводится к простому вызову метода «get». Будет возвращён новый экземпляр сервиса:
$request = $di->get("request");
Или с помощью магического метода:
$request = $di->getRequest();
Или с использованием синтаксиса доступа к массивам:
$request = $di['request'];
Аргументы можно передать в конструктор, добавив массив в качестве параметра к методу «get»:
// new MyComponent("some-parameter", "other")
$component = $di->get("MyComponent", array("some-parameter", "other"));
Общие сервисы
Сервисы могут быть зарегистрированы как «общие» сервисы, что означает, что они всегда будут действовать как синглтоны. После первого получения сервиса, тот же экземпляр возвращается каждый раз, когда потребитель получает сервис из контейнера:
//Register the session service as "always shared"
$di->setShared('session', function() {
$session = new Phalcon\Session\Adapter\Files();
$session->start();
return $session;
});
$session = $di->get('session'); // Locates the service for the first time
$session = $di->getSession(); // Returns the first instantiated object
Альтернативный способ регистрации общих сервисов — передать «true» в качестве третьего параметра «set»:
//Register the session service as "always shared"
$di->set('session', function() {
//...
}, true);
Если сервис не зарегистрирован как общий, и вы хотите убедиться, что каждый раз, когда сервис получается из DI, будет использоваться общий экземпляр, вы можете использовать метод «getShared»:
$request = $di->getShared("request");
Работа с сервисами по отдельности
После регистрации сервиса в контейнере сервисов вы можете получить его, чтобы работать с ним по отдельности:
//Register the "register" service
$di->set('request', 'Phalcon\Http\Request');
//Get the service
$requestService = $di->getService('request');
//Change its definition
$requestService->setDefinition(function() {
return new Phalcon\Http\Request();
});
//Change it to shared
$requestService->setShared(true);
//Resolve the service (return a Phalcon\Http\Request instance)
$request = $requestService->resolve();
Инициализация классов через контейнер сервисов
При запросе сервиса в контейнере сервисов, если не найдется сервис с таким же именем, будет попытка загрузить класс с таким же именем. С таким поведением мы можем заменить любой класс другим, просто зарегистрировав сервис с его именем:
//Register a controller as a service
$di->set('IndexController', function() {
$component = new Component();
return $component;
}, true);
//Register a controller as a service
$di->set('MyOtherComponent', function() {
//Actually returns another component
$component = new AnotherComponent();
return $component;
});
//Create an instance via the service container
$myComponent = $di->get('MyOtherComponent');
Вы можете воспользоваться этим, всегда инициализируя свои классы через контейнер сервисов (даже если они не зарегистрированы как сервисы). DI будет использовать корректный автозагрузчик, чтобы, наконец, загрузить класс. Благодаря этому вы можете легко заменить любой класс в будущем, реализовав его определение.
Автоматическое инжектирование самого DI
Если класс или компонент требует самого DI для поиска сервисов, DI может автоматически инжектировать себя в экземпляры, которые он создает. Для этого необходимо реализовать интерфейс Phalcon\DI\InjectionAwareInterface в ваших классах:
class MyClass implements \Phalcon\DI\InjectionAwareInterface
{
protected $_di;
public function setDi($di)
{
$this->_di = $di;
}
public function getDi()
{
return $this->_di;
}
}
Затем, после разрешения сервиса, $di будет автоматически передан в setDi:
//Register the service
$di->set('myClass', 'MyClass');
//Resolve the service (NOTE: $myClass->setDi($di) is automatically called)
$myClass = $di->get('myClass');
Избегание разрешения сервисов
Некоторые сервисы используются в каждом запросе к приложению. Устранение процесса разрешения сервиса может немного улучшить производительность.
//Resolve the object externally instead of using a definition for it:
$router = new MyRouter();
//Pass the resolved object to the service registration
$di->set('router', $router);
Организация сервисов в файлах
Вы можете лучше организовать свое приложение, переместив регистрацию сервисов в отдельные файлы вместо того, чтобы делать всё в загрузчике приложения:
$di->set('router', function() {
return include "../app/config/routes.php";
});
Затем в файле (”../app/config/routes.php”) верните решенный объект:
$router = new MyRouter();
$router->post('/login');
return $router;
Доступ к DI статическим способом
При необходимости вы можете получить доступ к последнему созданному DI в статическом методе следующим образом:
class SomeComponent
{
public static function someMethod()
{
//Get the session service
$session = Phalcon\DI::getDefault()->getSession();
}
}
Контейнер DI по умолчанию
Хотя отвязанный характер Phalcon предоставляет нам большую свободу и гибкость, возможно, нам просто нужно использовать его как фреймворк для всего стека. Для этого фреймворк предоставляет вариант Phalcon\DI под названием Phalcon\DI\FactoryDefault. Этот класс автоматически регистрирует соответствующие сервисы, поставляемые с фреймворком, чтобы действовать как полноценный фреймворк.
$di = new Phalcon\DI\FactoryDefault();
Конвенции имён сервисов
Хотя вы можете регистрировать сервисы с любыми желаемыми именами, Phalcon имеет несколько конвенций имён, которые позволяют ему получить правильный (встроенный) сервис, когда это необходимо.
| Имя сервиса | Описание | По умолчанию | Разделяемый |
|---|---|---|---|
| dispatcher | Сервис диспетчеризации контроллеров | Phalcon\Mvc\Dispatcher | Да |
| router | Сервис маршрутизации | Phalcon\Mvc\Router | Да |
| url | Сервис генерации URL | Phalcon\Mvc\Url | Да |
| request | Сервис окружения HTTP-запроса | Phalcon\Http\Request | Да |
| response | Сервис окружения HTTP-ответа | Phalcon\Http\Response | Да |
| cookies | Сервис управления HTTP-cookies | Phalcon\Http\Response\Cookies | Да |
| filter | Сервис фильтрации входных данных | Phalcon\Filter | Да |
| flash | Сервис сообщений флеша | Phalcon\Flash\Direct | Да |
| flashSession | Сервис сообщений флеша в сессии | Phalcon\Flash\Session | Да |
| session | Сервис сессии | Phalcon\Session\Adapter\Files | Да |
| eventsManager | Сервис управления событиями | Phalcon\Events\Manager | Да |
| db | Сервис низкоуровневого соединения с базой данных | Phalcon\Db | Да |
| security | Сервисы безопасности | Phalcon\Security | Да |
| crypt | Шифрование/дешифрование данных | Phalcon\Crypt | Да |
| tag | Помощники для генерации HTML | Phalcon\Tag | Да |
| escaper | Контекстуальное экранирование | Phalcon\Escaper | Да |
| annotations | Парсер аннотаций | Phalcon\Annotations\Adapter\Memory | Да |
| modelsManager | Сервис управления моделями | Phalcon\Mvc\Model\Manager | Да |
| modelsMetadata | Сервис метаданных моделей | Phalcon\Mvc\Model\MetaData\Memory | Да |
| transactionManager | Сервис управления транзакциями моделей | Phalcon\Mvc\Model\Transaction\Manager | Да |
| modelsCache | Кэш-бекенд для кэша моделей | Нет |
|
| viewsCache | Кэш-бекенд для фрагментов представлений | Нет |
|
Реализация собственного DI
Интерфейс Phalcon\DiInterface должен быть реализован для создания собственного DI, заменяющего предоставленный Phalcon или расширяющего существующий.
© 2011–2016 Phalcon Framework Team
Licensed under the Creative Commons Attribution License 3.0.
https://docs.phalconphp.com/en/2.0.0/reference/di.html