Ответ 1
Какими будут зависимости, о которых вы говорите в контроллере?
Основным решением будет:
- впрыскивание factory служб в контроллер через конструктор
- с использованием контейнера DI для передачи непосредственно в конкретных сервисах.
Я попытаюсь подробно описать оба подхода отдельно.
Примечание. все примеры будут не учитывать взаимодействие с представлением, обработку авторизации, работу с зависимостями службы factory и другие особенности
Инъекция factory
упрощенная часть этапа начальной загрузки, которая связана с отправкой материала на контроллер, выглядела бы вроде как
$request = //... we do something to initialize and route this
$resource = $request->getParameter('controller');
$command = $request->getMethod() . $request->getParameter('action');
$factory = new ServiceFactory;
if ( class_exists( $resource ) ) {
$controller = new $resource( $factory );
$controller->{$command}( $request );
} else {
// do something, because requesting non-existing thing
}
Этот подход обеспечивает четкий способ расширения и/или замены кода, связанного с модельным слоем, просто путем передачи в другом factory как зависимости. В контроллере он будет выглядеть примерно так:
public function __construct( $factory )
{
$this->serviceFactory = $factory;
}
public function postLogin( $request )
{
$authentication = $this->serviceFactory->create( 'Authentication' );
$authentication->login(
$request->getParameter('username'),
$request->getParameter('password')
);
}
Это означает, что для проверки этого метода контроллера вам нужно будет написать unit-test, который высмеивает содержимое $this->serviceFactory
, созданного экземпляра и переданного значения $request
. Саид-макет должен будет вернуть экземпляр, который может принимать два параметра.
Примечание. Ответ на пользователя должен обрабатываться полностью экземпляром вида, так как создание ответа является частью логики пользовательского интерфейса. Имейте в виду, что заголовок HTTP-местоположения имеет также форму ответа.
Единичный тест для такого контроллера будет выглядеть так:
public function test_if_Posting_of_Login_Works()
{
// setting up mocks for the seam
$service = $this->getMock( 'Services\Authentication', ['login']);
$service->expects( $this->once() )
->method( 'login' )
->with( $this->equalTo('foo'),
$this->equalTo('bar') );
$factory = $this->getMock( 'ServiceFactory', ['create']);
$factory->expects( $this->once() )
->method( 'create' )
->with( $this->equalTo('Authentication'))
->will( $this->returnValue( $service ) );
$request = $this->getMock( 'Request', ['getParameter']);
$request->expects( $this->exactly(2) )
->method( 'getParameter' )
->will( $this->onConsecutiveCalls( 'foo', 'bar' ) );
// test itself
$instance = new SomeController( $factory );
$instance->postLogin( $request );
// done
}
Контроллеры должны быть самой тонкой частью приложения. Ответственность диспетчера заключается в следующем: принять пользовательский ввод и, основываясь на этом вводе, изменить состояние слоя модели (и в редком случае - текущий вид). Это.
С контейнером DI
Этот другой подход... ну.. это в основном торговля сложностью (вычесть в одном месте, добавить больше на другие). Он также ретранслирует о наличии реальных контейнеров DI, а не прославленных сервисных локаторов, таких как Pimple.
Моя рекомендация: проверить Auryn.
Что делает контейнер DI, используя либо файл конфигурации, либо отражение, он определяет зависимости для экземпляра, который вы хотите создать. Собирает указанные зависимости. И передается в конструкторе для экземпляра.
$request = //... we do something to initialize and route this
$resource = $request->getParameter('controller');
$command = $request->getMethod() . $request->getParameter('action');
$container = new DIContainer;
try {
$controller = $container->create( $resource );
$controller->{$command}( $request );
} catch ( FubarException $e ) {
// do something, because requesting non-existing thing
}
Таким образом, помимо возможности генерации исключений, самонастройка контроллера остается практически такой же.
Кроме того, на этом этапе вы уже должны признать, что переход от одного подхода к другому в основном потребует полной перезаписи контроллера (и соответствующих модульных тестов).
Метод контроллера в этом случае будет выглядеть примерно так:
private $authenticationService;
#IMPORTANT: if you are using reflection-based DI container,
#then the type-hinting would be MANDATORY
public function __construct( Service\Authentication $authenticationService )
{
$this->authenticationService = $authenticationService;
}
public function postLogin( $request )
{
$this->authenticatioService->login(
$request->getParameter('username'),
$request->getParameter('password')
);
}
Что касается написания теста, в этом случае снова все, что вам нужно сделать, это предоставить некоторые макеты для изоляции и просто проверить. Но в этом случае модульное тестирование проще:
public function test_if_Posting_of_Login_Works()
{
// setting up mocks for the seam
$service = $this->getMock( 'Services\Authentication', ['login']);
$service->expects( $this->once() )
->method( 'login' )
->with( $this->equalTo('foo'),
$this->equalTo('bar') );
$request = $this->getMock( 'Request', ['getParameter']);
$request->expects( $this->exactly(2) )
->method( 'getParameter' )
->will( $this->onConsecutiveCalls( 'foo', 'bar' ) );
// test itself
$instance = new SomeController( $service );
$instance->postLogin( $request );
// done
}
Как вы можете видеть, в этом случае у вас есть еще один класс для макета.
Разные примечания
-
Связь с именем (в примерах - "проверка подлинности" ):
Как вы могли бы заметить, в обоих примерах ваш код будет связан с именем службы, которое было использовано. И даже если вы используете конфигурационный контейнер DI (как это возможно в symfony), вы все равно определите имя определенного класса.
-
Контейнеры DI не являются волшебными:
Использование контейнеров DI было несколько раздуто в последние пару лет. Это не серебряная пуля. Я бы даже сказал, что: контейнеры DI несовместимы с SOLID. В частности, потому что они не работают с интерфейсами. Вы действительно не можете использовать полиморфное поведение в коде, которое будет инициализировано контейнером DI.
Тогда возникает проблема с DI на основе конфигурации. Ну.. это просто красиво, а проект крошечный. Но по мере роста проекта файл конфигурации также растет. Вы можете получить великолепную СТЕНУ xml/yaml, которая понимается только одним человеком в проекте.
И третий вопрос - сложность. Хорошие контейнеры DI не просты в изготовлении. И если вы используете сторонний инструмент, вы вводите дополнительные риски.
-
Слишком много зависимостей:
Если ваш класс имеет слишком много зависимостей, то это not отказ DI как практика. Вместо этого это четкое указание, что ваш класс делает слишком много вещей. Он нарушает Принцип единой ответственности.
-
У контроллеров действительно есть (некоторая) логика:
Приведенные выше примеры были чрезвычайно простыми и взаимодействовали с модельным слоем через одну службу. В реальном мире ваши методы управления будут содержать контрольные структуры (циклы, условные обозначения, данные).
Самым основным вариантом использования будет контроллер, который обрабатывает контактную форму с раскрывающимся списком "субъект". Большинство сообщений будут направлены на службу, которая связывается с некоторым CRM. Но если пользователь выбирает "сообщить об ошибке", тогда сообщение должно быть передано службе разницы, которая автоматически создает билет в трекере ошибок и отправляет некоторые уведомления.
-
Это модуль PHP:
Примеры модульных тестов написаны с использованием PHPUnit. Если вы используете какую-то другую фреймворк или вручную пишете тесты, вам придется сделать некоторые основные изменения
-
У вас будет больше тестов:
Пример unit-test - это не весь набор тестов, которые вы будете иметь для метода контроллера. Особенно, когда у вас есть контроллеры, которые нетривиальны.
Другие материалы
Есть некоторые.. эмм... тангенциальные предметы.
Brace for: shameless self-promotion
-
управление доступом в MVC-подобной архитектуре
Некоторые структуры имеют неприятную привычку подталкивать проверки авторизации (не путайте с "аутентификацией".. другой вопрос) в контроллере. Помимо того, что он абсолютно глупый, он также вводит в контроллеры дополнительные зависимости (часто - глобально).
Существует другая публикация, в которой используется аналогичный подход для введения неинвазивных протоколирования.
-
Это направлено на людей, которые хотят узнать о MVC, но там есть материалы для общего образования в ООП и практики развития. Идея состоит в том, что к тому моменту, когда вы закончите с этим списком, MVC и другие реализации SoC вызовут вас только "О, у этого есть имя? Я думал, что это просто здравый смысл".
-
Объясняет, что эти магические "сервисы" находятся в описании выше.