Избавляемся от излишней связности
Я хочу рассказать про следующие вещи:
Предположим, в нашем веб-приложении надо хранить состояние пользователя:
interface UserInterface {
public function setLocale($locale);
public function getLocale();
public function isAuthenticated();
public function getId();
public function hasCredential($credential);
}
HTTP по своей природе не поддерживает сохранения состояния между запросами.
Именно поэтому в PHP есть сессии.
class SessionStorage {
public function __construct($cookie_name = 'PHPSESSID')
{
session_name($cookie_name);
session_start();
}
public function get($key)
{
return $_SESSION[$key];
}
public function set($key, $value)
{
$_SESSION[$key] = $value;
}
}
class User {
protected $storage;
public function __construct() {
$this->storage = new SessionStorage();
}
public function setLocale($locale) {
$this->storage->set('locale', $locale);
}
public function getLocale() {
return $this->storage->get('locale');
}
}
$user = new User();
class User {
protected $storage;
public function __construct() {
$this->storage = new SessionStorage('AWESOME_COOKIE_NAME');
}
}
define('SESSION_COOKIE_NAME', 'AWESOME_COOKIE_NAME');
class User {
protected $storage;
public function __construct() {
$this->storage = new SessionStorage(SESSION_COOKIE_NAME);
}
}
class User {
protected $storage;
public function __construct($cookieName) {
$this->storage = new SessionStorage($cookieName);
}
}
class User {
protected $storage;
public function __construct() {
$this->storage = Registry::get('session');
}
}
Теперь User
зависит от Registry
и скрытно — от SessionStorage.
Хотели как лучше...
class User {
protected $storage;
public function __construct(SessionStorage $storage) {
$this->storage = $storage;
}
}
$user = new User(new MySQLSessionStorage('SESSION_ID'));
Мы не создаем зависимости внутри нашего кода, мы их внедряем извне.
class A {
public function __construct(B $b);
}
Используем для обязательных зависимостией.
class A {
public function setB(B $b);
}
Используем для необязательных зависимостией.
class A {
/** @var B */
public $b;
}
Используем в самом крайнем случае. Это нарушение базовых принципов ООП, но... иногда выбирать не приходится.
class Controller {
protected $templating;
protected $routing;
protected $user;
public function whateverAction(Request $request);
}
То самое третье лицо, ага.
Раз уж мы собираемся на Laravel...
composer require "illuminate/container ~5.0"
use Illuminate\Container\Container;
class Kohana extends Kohana_Core {
public static function get_container() {
return Container::getInstance();
}
}
bootstrap.php:
// Tons of bootstrap code
require_one __DIR__.'/config/container.php';
use Illuminate\Container\Container;
$container = new Container();
// All the services definitions here...
Container::setInstance($container);
$container->bind('SessionStorage', 'RedisSessionStorage');
$container->bind('UserInterface', 'User');
$user = $container->make('UserInterface');
// или $container['UserInterface'];
Если другому сервису будет нужен инстанс SessionStorage
, контейнер создаст объект RedisSessionStorage
и передаст его.
$container->bind('encryptor', function() {
$rsa = new \phpseclib\Crypt\RSA();
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::ENCRYPTION_PKCS1);
$rsa->loadKey(Kohana::$config->load('google_play')->get('key'));
return $rsa;
});
$container->singleton('FooBar', function(Container $c) {
return new FooBar();
});
$container->bind('alias', function(Container $c, $parameters) {
return new MyAwesomeClass($parameters);
});
$object = $container->make('alias', ['name' => 'value']);
class Controller_API_Purchases extends Controller_API {
/** @var RSA */
private $encryptor;
/** @var Service_SubscriptionManager */
private $manager;
public function __construct(Request $request, Response $response,
RSA $encryptor, Service_SubscriptionManager $subscriptionManager) {
parent::__construct($request, $response);
$this->encryptor = $encryptor;
$this->manager = $subscriptionManager;
}
}
Честно говоря, меня прям бесит, что Request и Response создаются до контроллера. По идее, каждый action контроллера должен принимать Request и создавать Response:)
Но разработчиков Kohana мое мнение волнует мало:)
$container->bind('subscription_manager', 'Service_SubscriptionManager');
$container->bind('controller.purchases', function($c, $parameters) {
return new Controller_API_Purchases(
$parameters['request'],
$parameters['response'],
$c['encryptor'],
$c['subscription_manager']);
});
Ну да, она пока не умеет создавать контроллеры из сервисов.
public function execute_request(Request $request, Response $response)
{
// tons of code here
$controller = $this->create_controller(
$prefix, $controller, $request, $response
);
$response = $controller->execute();
}
private function create_controller($prefix, $controller_name, Request $request, Response $response)
$container = Kohana::get_container();
$service = 'controller.'.strtolower($controller_name);
if ($container->bound($service)) {
$controller = $container->make($service, ['request' => $request, 'response' => $response]);
}
else {
// Создаем контроллер стандартным методом Коханы,
// который мы извлекли из execute_request();
// Create a new instance of the controller
$controller = $class->newInstance($request, $response);
}
if ($controller instanceof Service_ContainerAware) {
$controller->set_container($container);
}
return $controller;
}
Это способ внедрить сам контейнер в качестве зависимости.
Используйте это в случае крайней необходимости!
class Controller_Awesome extents Controller
implements Service_ContainerAware {
/** @var Container */
private $container;
public function set_container(Container $container) {
$this->container = $container;
}
}
Route::set('tour_finish', 'tour/finish')->filter(['Filters', 'auth'])->defaults([
'controller' => 'tour', 'action' => 'finish']);
$container->bind('controller.tour', function($c, $parameters) {
return new Controller_Tour($parameters['request'],
$parameters['response']);
});
class User {
protected $storage;
public function __construct() {
$this->storage = new SessionStorage();
}
Этот парень слишком многое знает.
public function set_container(Container $container) {
$this->container = $container;
}
Первое правило DIC: никто не должен знать о DIC.
by the way, ServiceLocator — не плохо. Плохо смешивать!