На программистском жаргоне, Phemto – это легкий, автоматизированный контейнер dependency injection (управления зависимостями). Иначе говоря, задача Phemto – создавать экземпляр объекта, получая минимум информации, значительно ослабляя зависимости внутри приложения или фреймворка.
Зачем это нужно?
Проще всего понять паттерн DI можно, представив себе шкалу с "Используем DI" на одном конце и "Используем хардкодинг (т.е. жестко запрограммированные связи)" на другом. Мы с вами сейчас проделаем короткое путешествие от хардкодинга через паттерны Factory, Registry, Service Locator к DI. Если Вы и так знаете, что такое DI, переходите сразу к установке Phemto.
Заурядное создание объектов с помощью оператора new выглядит простым и понятным, однако скорее всего, мы столкнемся с трудностями, когда захотим что-то поменять потом. Посмотрим на код...
<?php
class MyController {
function __construct() {
...
$connection = new MysqlConnection();
}
}
Здесь MyController зависит от MysqlConnection.
Оператор new ясен и понятен, но MyController сможет использовать только БД MySQL. Небольшая переделка класса, позволяющая его наследовать, не спасет, поскольку тогда мы будем иметь в наследнике вместе с логикой дочернего контроллера и логику получения драйвера БД. В любом случае множественные зависимости не решаются наследованием, приводя к захламлению класса. Вообще говоря, Вы можете разыграть карту наследования только однажды.
Следующий шаг, – используем Factory...
<?php
class MyController {
function __construct($connection_pool) {
...
$connection = $connection_pool->getConnection();
}
}
Очень эффективное решение. Фабрика может быть настроена на нужный тип драйвера с помощью конфигурационного файла или явно. Фабрики могут создавать и объекты из разных групп, и тогда их называют Abstract Factory (Абстрактная Фабрика) или Repository (Репозиторий). Однако тут есть ограничения.
Фабрики приносят много дополнительного кода. Если надо тестировать классы с помощью mock-объектов, то придется имитировать не только возвращаемые фабрикой объекты, но и саму фабрику. Получаете дополнительную суету.
Да и в живом коде, если нужно вернуть объект, о котором автор фабрики не подумал, то придется наследовать или переписывать и саму фабрику, что для фреймворков может оказаться заметной проблемой.
Следующий ход в нашей борьбе с зависимостями, это вообще вынуть создание объекта Registry из основного объекта наружу...
<?php
class MyController {
function __construct($registry) {
...
$connection = $registry->connection;
}
}
...
$registry = new Registry();
$registry->connection = new MysqlConnection();
...
$controller = new MyController($registry);
Registry совсем пассивен, зато в основном коде мы создаем и перегружаем много объектов. Мы даже можем случайно насоздавать про запас объектов, которые никогда не потребуются, да так и оставить это место.
Кроме того, с помощью такого подхода мы не сможем использовать ленивое создание объектов (lazy loading). Неудача ждет нас, и если мы захотим, чтобы нам возвращался не один и тот же объект адаптера к БД, а разные объекты.
Жизнь сразу ухудшится, если в нашем примере будут еще зависимости, которые надо учесть. Т.е. если, например, для создания объекта-адаптера недостаточно сделать new, а нужно добавить в конструктор какой-то еще объект. В общем, предварительная настройка грозит сделаться весьма запутанной.
Мы можем сделать паттерн Registry более изощренным, если позволим объекту Registry самостоятельно создавать экземпляры нужных объектов. Наш объект стал Сервис-локатором (Service Locator)...
<?php
class MyController {
function __construct($services) {
...
$connection = $services->connection;
}
}
...
$services = new ServiceLocator();
$services->connection('MysqlConnection');
...
$controller = new MyController($services);
Теперь настройки, могут быть в любом порядке, однако ServiceLocator должен знать, как создать MysqlConnection. Задача решается с помощью фабрик или с помощью трюков с рефлексией, хотя передача параметров, может стать весьма кропотливой работой. Жизненный цикл объектов (напр. возвращать один и тот же объект, или создавать разные) теперь под контролем программиста, который может как, запрограммировать все в методах фабрики, так и вынести все в настройки или плагины.
К сожалению, эта почти серебряная пуля имеет ту же проблему, что и Registry. Любой класс, который будет пользоваться таким интерфейсом, неизбежно будет зависеть от Сервис-локатора. Если Вы попробуете смешать две системы с разными сервис-локаторами, вы почувствуете что такое "не повезло".
Dependency Injection заходит немного с другой стороны. Посмотрим на наш самый первый пример...
<?php
class MysqlConnection { ... }
class MyController {
function __construct() {
...
$connection = new MysqlConnection();
}
}
...и сделаем зависимость внешней...
<?php
class MysqlConnection { ... }
class MyController {
function __construct(Connection $connection) {
...
}
}
На первый взгляд, это просто ужасно. Теперь ведь каждый раз в скрипте придется все эти зависимости руками трогать. Чтобы изменить адаптер к БД, придется вносить изменения в сотне мест. Так бы оно и было, если бы мы использовали new...
<?php
$injector = new Phemto();
$controller = $injector->create('MyController');
Хотите верьте, хотите нет, но это все, что нам нужно.
Задача Phemto – выявление того, как создать объект, что позволяет на удивление здорово автоматизировать разработку. Только по типу параметра в интерфейсе он выведет, что MysqlConnection – единственный кандидат, удовлетворяющий нужному типу Connection.
Более сложные ситуации, могут потребовать дополнительной информации, которая обычно содержится в "цепочечном" файле. Вот пример такого файла из реальной жизни, чтобы можно было почувствовать мощь паттерна...
<?php
require_once('phemto/phemto.php');
$injector = new Phemto();
$injector->whenCreating('Page')->forVariable('session')->willUse(new Reused('Session'));
$injector->whenCreating('Page')->forVariable('continuation')->willUse('Continuation');
$injector->whenCreating('Page')->forVariable('alerts')->willUse('Alert');
$injector->whenCreating('Page')->forVariable('accounts')->willUse('Accounts');
$injector->whenCreating('Page')->forVariable('mailer')->willUse('Mailer');
$injector->whenCreating('Page')->forVariable('clock')->willUse('Clock');
$injector->whenCreating('Page')->forVariable('request')->willUse('Request');
return $injector;
?>
Такое количество настроек типично для проекта среднего размера.
Теперь контроллер задает только интерфейс, а работа по созданию объектов выполняется посредником. MyController теперь не должен вообще знать про MysqlConnection. Зато $injector знает и о том и о другом. Это называется обращение контроля Inversion of Control.
Phemto распростаняется простым тарболлом, так, что просто распакуйте его...
tar -zxf phemto_0.1_alpha6.tar.gzДостаточно использовать require_once(), чтобы включить файл phemto.php
Единственная зависимость Phemto, это механизм PHP reflection.
Phemto лучше всего использовать в главном скрипте или главном классе приложения или фреймворка.
Сначала вы пишете классы, как обычно...
<?php
class Permissions { ... }
class Authentication {
function __construct($permissions) { ... }
}
class MyPage implements Page {
function __construct($authentication) { ... }
}
Обычная архитектура Page controller. Мы можем легко сделать модульный тест для Page, поскольку его зависимость от Authentication передается в конструктор. Мы можем использовать версию-имитацию для теста и сконцентрироваться на логике.
Теперь мы напишем файл с цепочечной конфигурацией, назовем его "wiring.php". В нем содержатся все необходимые настройки для нашего приложения...
<?php
require_once('phemto/phemto.php');
$injector = new Phemto();
$injector->forVariable('authentication')->willUse('Authentication');
$injector->whenCreating('Authentication')
->forVariable('permissions')->willUse(new Sessionable('Permissions'));
return $injector;
?>
Здесь мы говорим нашему инжектору, что если он увидит аргумент $authentication, то нужно создать экземпляр Authentication. Объект для аргумента $permissions имеет другой жизненный цикл. Sessionable говорит о том, что если возможно, надо взять объект из сессии, иначе создать его и сохранить в сессии, так, что объект будет создан лишь однажды.
Наш главный скрипт вместо new теперь использует вызовы фабрик Phemto...
<?php
require_once('lib/my_page.php');
$injector = include('wiring.php');
$page = $injector->create('Page');
?>
<html>...</html>
Таким образом, наш код настолько изолирован от главного скрипта, что мы можем добавлять и убирать зависимости между классами, безо всякого вмешательства в главный скрипт.
У авторов фреймворков обычно другая цель. Скорее всего у них уже есть центральная точка для создания страниц, и главная работа, это адаптировать компоненты сторонних разработчиков.
Пусть мы хотим написать реализацию Authentication на основе интерфейса фреймворка...
<?php
interface Authentication { ... }
class InternalFrontControllerActionChainThingy {
function __construct(Authentication $authentication, ...) { ... }
}
Наш компонент будет использовать общее с фреймворком подключение к БД, и еще мы хотим взять кэширующий компонент третьей стороны.
<?php
require_once('cache.php');
class OurAuthentication implements Authentication {
function __construct(Database $database, DatabaseCache $cache) { ... }
}
Для фреймворка, основанного на фабриках, такой расклад близок к кошмару, поскольку фреймворк не знает, как создать компонент кэша, и куда его деть. Заставить нас передать фреймворку и фабрику, – не выход, поскольку фреймворк все равно должен будет, куда-то выдать и положить кэширующий компонент. Если же фреймворк использует Dependency Injection, то задача сводится всего лишь к настройке цепочки.
Цепочка может быть изменена напрямую с помощью пользовательского файла...
<?php
$injector = include('framework/wiring.php');
$injector->willUse('OurAuthenticator');
return $injector;
?>
Однако, скорее всего, фреймворк поместит инструмент DI в свою систему регистрации...
<?php
class FrameworkRegistration {
...
static function register($class, $dependencies = array()) {
$this->injector->whenCreating('Controller')->willUse($class);
foreach (dependencies as $dependency) {
$this->injector->whenCreating('Controller')
->whenCreating($class)
->willUse($dependency);
}
}
}
И тогда мы можем сделать такой вызов...
<?php
FrameworkRegistration::register('OurAuthentication', array('DatabaseCache'));
Простейший случай создания Phemto объекта, это через имя класса...
<?php
class Porsche911 { }
$injector = new Phemto();
$car = $injector->create('Porsche911');
Среди зарегистрированных классов будет найден подходящий.
Если только один класс может удовлетворить условию, тогда именно этого класса и будет создан объект. Phemto в этом вопросе достаточно умен и понимает абстрактные классы и интерфейсы...
<?php
abstract class Car { }
class Porsche911 extends Car { }
$injector = new Phemto();
$car = $injector->create('Car');
Здесь $car – экземпляр класса Porsche911. Также и...
<?php
interface Transport { }
class Porsche911 implements Transport { }
$injector = new Phemto();
$car = $injector->create('Transport');
Опять будет создан объект класса Porsche911, как единственно возможный вариант.
Если имеет место неясность, то Phemto бросит исключение. Неясность можно разрешить добавив в цепочку дополнительную информацию...
<?php
interface Transport { }
class Porsche911 implements Transport { }
class RouteMaster implements Transport { }
$injector = new Phemto();
$injector->willUse('Porsche911');
$car = $injector->create('Transport');
Это удобно и когда надо перекрыть стандартную реализацию, в то время как стандартный класс зарегистрирован в системе.
У Phemto есть два метода автоматического создания параметров. Первый, это с помощью типа...
<?php
interface Engine { }
class Porsche911 {
function __construct(Engine $engine) { }
}
class Flat6 implements Engine { }
$injector = new Phemto();
$car = $injector->create('Porsche911');
Это равнозначно new Porsche911(new Flat6()). Такой способ удобен авторам фреймворков, которым достаточно задать лишь имена интерфейсов.
Обратите внимание, – нам не пришлось менять основной код, даже несмотря на то, что мы поменяли сигнатуру конструктора.
Другой способ, – Phemto может создать параметр по имени аргумента...
<?php
class Porsche911 {
function __construct($engine) { }
}
interface Engine { }
class Flat6 implements Engine { }
$injector = new Phemto();
$injector->forVariable('engine')->willUse('Engine');
$car = $injector->create('Porsche911');
Опять мы для $car создаем объект класса new Porsche911(new Flat6()). Здесь мы воспользовались именем аргумента $engine, чтобы вычислить интерфейс. И дальше Phemto смог применить свои правила автоматизации.
Иногда все же надо передать параметры конструктору. Простейший способ сделать это, – добавить их в метод create...
<?php
class Porsche911 {
function __construct($fluffy_dice, $nodding_dog) { }
}
$injector = new Phemto();
$car = $injector->create('Porsche911', true, false);
Эти параметры займут свои места в конструкторе, в данном случае получится new Porsche911(true, false).
Неименованные параметры могут быть причиной ошибок, когда код станет посложнее, так, что можно воспользоваться и параметрами с именами...
<?php
class Porsche911 {
function __construct($fluffy_dice, $nodding_dog) { }
}
$injector = new Phemto();
$car = $injector->fill('fluffy_dice', 'nodding_dog')
->with(true, false)
->create('Porsche911', true);
Эти параметры также можно использовать и с зависимостями.
Phemto может вызывать и методы отличные от конструктора...
<?php
interface Seat { }
interface SportsCar { }
class Porsche911 implements SportsCar {
function fitDriversSeat(Seat $seat) { }
}
class BucketSeat implements Seat { }
$injector = new Phemto();
$injector->forType('SportsCar')->call('fitDriversSeat');
$car = $injector->create('Porsche911');
Этот код аналогичен...
<?php
$car = new Porsche911();
$car->fitDriversSeat(new BucketSeat());
Такой вызов методов, отличных от конструктора называется setter injection.
Далеко не всегда нужно создавать один и тот же объект. Иногда выбор должен определяться контекстом...
<?php
interface Seat { }
class Car {
function __construct(Seat $seat) { }
}
class FordEscort extends Car;
class Porsche911 extends Car;
class StandardSeat implements Seat { }
class BucketSeat implements Seat { }
$injector = new Phemto();
$injector->willUse('StandardSeat');
$injector->whenCreating('Porsche911')->willUse('BucketSeat');
$car = $injector->create('Porsche911');
Можете быть уверены, – по умолчанию $seat будет объектом класса StandardSeat, но для Porsche911 будет использован BucketSeat.
Метод whenCreating() создаст новую вложенную версию Phemto, так, что в этом контексте можно употреблять все вышеупомянутые методы, т.е...
<?php
class Car {
function __construct($seat) { }
}
class FordEscort extends Car;
class Porsche911 extends Car;
class StandardSeat { }
class BucketSeat { }
$injector = new Phemto();
$injector->willUse('StandardSeat');
$injector->whenCreating('Porsche911')
->forVariable('seat')->willUse('BucketSeat');
$car = $injector->create('Porsche911');
Жизненный цикл объектов, созданных с помощью Phemto можно контролировать.
Phemto имеет встроенные классы: Factory (по умолчанию), который всегда создает новый экземпляр объекта, Reused который отдает ссылки на один и тот же экземпляр, и Sessionable, который хранит экземпляр объекта в системной переменной PHP $_SESSION. Они все наследуют от базового абстрактного класса Lifecycle. Разработчики могут расширять эти классы..
Здесь мы создадим единственный экземпляр объекта Porsche911 и будем раздавать ссылки
<?php
class Porsche911 { }
$injector = new Phemto();
$injector->willUse(new Reused('Porsche911'));
$car = $injector->create('Porsche911');
$same_car = $injector->create('Porsche911');
$car и $same_car будут ссылаться на один и тот же объект. В конце концов, Porsche довольно дорогие машинки.
Ссылки и дополнительная информация