Pull to refresh

Phemto и Паттерн Dependency Injection. Часть 2

Reading time8 min
Views6.2K
Original author: Marcus Baker
Продолжение, начало в Части 1

Установка Phemto


Phemto распростаняется простым тарболлом, так, что просто распакуйте его…
tar -zxf phemto_0.1_alpha6.tar.gz

Достаточно использовать require_once(), чтобы включить файл phemto.php

Единственная зависимость Phemto, это механизм PHP reflection.

Phemto в Вашей программе


Phemto лучше всего использовать в главном скрипте или классе приложения или фреймворка.

Сначала вы пишете классы, как обычно…


class Permissions { ... } 
 
class Authentication { 
    function __construct($permissions) { ... } 
} 
 
class MyPage implements Page { 
    function __construct($authentication) { ... } 
}


Обычная архитектура Page controller. Мы можем легко сделать модульный тест для Page, поскольку его зависимость от Authentication передается в конструктор. Мы можем использовать версию-имитацию для теста и сконцентрироваться на логике.

Теперь мы напишем файл с цепочечной конфигурацией, назовем его «wiring.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


require_once('lib/my_page.php'); 
 
$injector = include('wiring.php'); 
$page = $injector->create('Page'); 
?> 
<html>...</html>


Таким образом, наш код настолько изолирован от главного скрипта, что мы можем добавлять и убирать зависимости между классами, безо всякого вмешательства в главный скрипт.

У авторов фреймворков обычно другая цель. Скорее всего у них уже есть центральная точка для создания страниц, и главная работа, это адаптировать компоненты сторонних разработчиков.

Пусть мы хотим написать реализацию Authentication на основе интерфейса фреймворка…


interface Authentication { ... } 
 
class InternalFrontControllerActionChainThingy { 
    function __construct(Authentication $authentication, ...) { ... } 
}


Наш компонент будет использовать общее с фреймворком подключение к БД, и еще мы хотим взять кэширующий компонент третьей стороны.


require_once('cache.php'); 
 
class OurAuthentication implements Authentication { 
    function __construct(Database $database, DatabaseCache $cache) { ... } 
}


Для фреймворка, основанного на фабриках, такой расклад близок к кошмару, поскольку фреймворк не знает, как создать компонент кэша, и куда его деть. Заставить нас передать фреймворку и фабрику, – не выход, поскольку фреймворк все равно должен будет, куда-то выдать и положить кэширующий компонент. Если же фреймворк использует Dependency Injection, то задача сводится всего лишь к настройке цепочки.

Цепочка может быть изменена напрямую с помощью пользовательского файла…


$injector = include('framework/wiring.php'); 
$injector->willUse('OurAuthenticator'); 
return $injector;


Однако, скорее всего, фреймворк поместит инструмент DI в свою систему регистрации…


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); 
        } 
    } 
}



И тогда мы можем сделать такой вызов…


FrameworkRegistration::register('OurAuthentication', array('DatabaseCache'));


Цепочечный синтаксис Phemto


Простейший случай создания Phemto объекта, это через имя класса…


class Porsche911 { } 
 
$injector = new Phemto(); 
$car = $injector->create('Porsche911');


Среди зарегистрированных классов будет найден подходящий.

Если только один класс может удовлетворить условию, тогда именно этого класса и будет создан объект. Phemto в этом вопросе достаточно умен и понимает абстрактные классы и интерфейсы…


abstract class Car { } 
class Porsche911 extends Car { } 
 
$injector = new Phemto(); 
$car = $injector->create('Car');


Здесь $car – экземпляр класса Porsche911. Также и…


interface Transport { } 
class Porsche911 implements Transport { } 
 
$injector = new Phemto(); 
$car = $injector->create('Transport');


Опять будет создан объект класса Porsche911, как единственно возможный вариант.

Если имеет место неясность, то Phemto бросит исключение. Неясность можно разрешить добавив в цепочку дополнительную информацию…


interface Transport { } 
class Porsche911 implements Transport { } 
class RouteMaster implements Transport { } 
 
$injector = new Phemto(); 
$injector->willUse('Porsche911'); 
$car = $injector->create('Transport');


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

У Phemto есть два метода автоматического создания параметров. Первый, это с помощью типа…


interface Engine { } 
 
class Porsche911 { 
    function __construct(Engine $engine) { } 
} 
 
class Flat6 implements Engine { } 
 
$injector = new Phemto(); 
$car = $injector->create('Porsche911');


Это равнозначно new Porsche911(new Flat6()). Такой способ удобен авторам фреймворков, которым достаточно задать лишь имена интерфейсов.

Обратите внимание, – нам не пришлось менять основной код, даже несмотря на то, что мы поменяли сигнатуру конструктора.

Другой способ, – Phemto может создать параметр по имени аргумента…


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

class Porsche911 { 
    function __construct($fluffy_dice, $nodding_dog) { } 
} 
 
$injector = new Phemto(); 
$car = $injector->create('Porsche911', true, false);


Эти параметры займут свои места в конструкторе, в данном случае получится new Porsche911(true, false).

Неименованные параметры могут быть причиной ошибок, когда код станет посложнее, так, что можно воспользоваться и параметрами с именами…


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 может вызывать и методы отличные от конструктора…


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');


Этот код аналогичен...

$car = new Porsche911(); 
$car->fitDriversSeat(new BucketSeat());


Такой вызов методов, отличных от конструктора называется setter injection.

Далеко не всегда нужно создавать один и тот же объект. Иногда выбор должен определяться контекстом…


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, так, что контекст действует на все предыдущие настройки в цепочке, т.е…


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 и будем раздавать ссылки


class Porsche911 { } 
 
$injector = new Phemto(); 
$injector->willUse(new Reused('Porsche911')); 
$car = $injector->create('Porsche911'); 
$same_car = $injector->create('Porsche911');


$car и $same_car будут ссылаться на один и тот же объект. В конце концов, Porsche довольно дорогие машинки.

Ссылки и дополнительная информация
Tags:
Hubs:
+29
Comments8

Articles