Подтвердить что ты не робот

Контролируемые контроллеры с зависимостями

Как я могу разрешить зависимости с контролируемым контроллером?

Как это работает: URI направляется на контроллер, у контроллера могут быть зависимости для выполнения определенной задачи.

<?php

require 'vendor/autoload.php';

/*
 * Registry
 * Singleton
 * Tight coupling
 * Testable?
 */

$request = new Example\Http\Request();

Example\Dependency\Registry::getInstance()->set('request', $request);

$controller = new Example\Controller\RegistryController();

$controller->indexAction();

/*
 * Service Locator
 *
 * Testable? Hard!
 *
 */

$request = new Example\Http\Request();

$serviceLocator = new Example\Dependency\ServiceLocator();

$serviceLocator->set('request', $request);

$controller = new Example\Controller\ServiceLocatorController($serviceLocator);

$controller->indexAction();

/*
 * Poor Man
 *
 * Testable? Yes!
 * Pain in the ass to create with many dependencies, and how do we know specifically what dependencies a controller needs
 * during creation?
 * A solution is the Factory, but you would still need to manually add every dependencies a specific controller needs
 * etc.
 *
 */

$request = new Example\Http\Request();

$controller = new Example\Controller\PoorManController($request);

$controller->indexAction();

Это моя интерпретация примеров шаблонов проектирования

Реестр:

  • Singleton
  • Плотная связь
  • Testable? Нет

Локатор сервисов

  • Testable? Жесткий/Нет (?)

Бедный Человек Ди

  • Testable
  • Трудно поддерживать со многими зависимостями

Реестр

<?php
namespace Example\Dependency;

class Registry
{
    protected $items;

    public static function getInstance()
    {
        static $instance = null;
        if (null === $instance) {
            $instance = new static();
        }

        return $instance;
    }

    public function set($name, $item)
    {
        $this->items[$name] = $item;
    }

    public function get($name)
    {
        return $this->items[$name];
    }
} 

Локатор сервисов

<?php
namespace Example\Dependency;

class ServiceLocator
{
    protected $items;

    public function set($name, $item)
    {
        $this->items[$name] = $item;
    }

    public function get($name)
    {
        return $this->items[$name];
    }
} 

Как я могу разрешить зависимости с контролируемым контроллером?

4b9b3361

Ответ 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 вызовут вас только "О, у этого есть имя? Я думал, что это просто здравый смысл".

  • реалистичный уровень модели

    Объясняет, что эти магические "сервисы" находятся в описании выше.

Ответ 2

Я пробовал это из http://culttt.com/2013/07/15/how-to-structure-testable-controllers-in-laravel-4/

Как вы должны структурировать контроллеры, чтобы сделать их проверяемыми.

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

К счастью, Laravel 4 очень легко отделяет проблемы вашего контроллера. Это делает тестирование ваших контроллеров действительно прямым, если вы правильно структурировали их.

Что мне следует тестировать в моем контроллере?

Прежде чем я расскажу о том, как структурировать контроллеры для проверки, сначала важно понять, что именно нам нужно проверить.

Как я уже упоминал в настройке вашего первого контроллера Laravel 4, контроллеры должны заниматься только перемещением данных между моделью и представлением. Вам не нужно проверять, что база данных извлекает правильные данные, только то, что контроллер вызывает правильный метод. Поэтому ваши тесты контроллера никогда не должны касаться базы данных.

Это действительно то, что я собираюсь показать вам сегодня, потому что по умолчанию довольно легко слиться с объединением Controller и Model вместе. Пример плохой практики

В качестве иллюстрации того, что я пытаюсь избежать, приведен пример метода контроллера:

public function index()
{
  return User::all();
}

Это плохая практика, потому что мы не можем насмехаться User::all();, и поэтому связанный тест будет вынужден попасть в базу данных.

Инъекция зависимостей на спасение

Чтобы обойти эту проблему, мы должны ввести зависимость в контроллер. Dependency Injection - это то, где вы передаете классу экземпляр объекта, вместо того, чтобы позволить этому объекту создать экземпляр для его самого.

Вводя зависимость в контроллер, мы можем передать класс вместо mock вместо базы данных вместо фактического объекта базы данных во время наших тестов. Это означает, что мы можем протестировать функциональность контроллера, не касаясь базы данных.

В качестве общего руководства везде, где вы видите класс, создающий экземпляр другого объекта, обычно это признак того, что это может быть лучше обработано с помощью инъекции зависимостей. Вы никогда не хотите, чтобы ваши объекты были тесно связаны, и поэтому, не позволяя классу создавать экземпляр другого класса, вы можете предотвратить это.

Автоматическое разрешение

Laravel 4 имеет прекрасный способ обращения с инъекцией Dependancy Injection. Это означает, что вы можете разрешать классы без какой-либо конфигурации вообще во многих сценариях.

Это означает, что если вы передадите классу экземпляр другого класса через конструктор, Laravel автоматически добавит эту зависимость для вас!

В принципе, все будет работать без какой-либо конфигурации с вашей стороны.

Ввод базы данных в контроллер

Итак, теперь вы понимаете проблему и теорию решения, теперь мы можем исправить контроллер, поэтому он не связан с базой данных.

Если вы помните, что на прошлой неделе вы писали о репозиториях Laravel, возможно, вы заметили, что я уже исправил эту проблему.

Итак, вместо этого:

public function index()
{
  return User::all();
}

Я сделал:

public function __construct(User $user)
{
  $this->user = $user;
}

/**
 * Display a listing of the resource.
 *
 * @return Response
 */
public function index()
{
  return $this->user->all();
}

При создании класса UserController автоматически запускается метод __construct. В метод __construct вводится экземпляр репозитория User, который затем устанавливается в свойстве $this- > user для класса.

Теперь, когда вы хотите использовать базу данных в своих методах, вы можете использовать экземпляр $this- > user.

Измельчение базы данных в тестах контроллера

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

Первое, что я хочу сделать, - создать новую папку под каталогом тестов, называемой функциональной. Мне нравится думать о тестах Controller как о функциональных тестах, потому что мы тестируем входящий трафик и визуализированное представление.

Далее Я собираюсь создать файл UserControllerTest.php и записать следующий шаблонный код:

<?php

class UserControllerTest extends TestCase {

}

Издевательство над насмешкой

Если вы помните назад к моему сообщению, что такое Test Driven Development?, я говорил о Mocks как о замене зависимых объектов.

Чтобы создать Mocks для тестов в Cribbb, я собираюсь использовать фантастический пакет под названием Mockery.

Mockery позволяет вам высмеивать объекты в вашем проекте, поэтому вам не нужно использовать реальную зависимость. Издеваясь над объектом, вы можете указать Mockery, какой метод вы хотите вызвать и что вы хотите вернуть.

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

Например, если вы хотите вызвать метод all() в объекте базы данных, вместо фактического попадания в базу данных вы можете высмеять вызов, указав Mockery, который вы хотите вызвать методом all(), и он должен вернуть ожидаемый стоимость. Вы проверяете, может ли база данных возвращать записи или нет, вам все равно, что вы можете вызвать этот метод и обработать возвращаемое значение.

Установка издевательств Как и все хорошие пакеты PHP, Mockery можно установить через Composer.

Чтобы установить Mockery через Composer, добавьте следующую строку в ваш файл composer.json:

"require-dev": {
  "mockery/mockery": "dev-master"
}

Затем установите пакет:

composer install --dev

Настройка mockery

Теперь, чтобы настроить Mockery, нам нужно создать несколько тестовых файлов в настройках:

public function setUp()
{
  parent::setUp();

  $this->mock = $this->mock('Cribbb\Storage\User\UserRepository');
}

public function mock($class)
{
  $mock = Mockery::mock($class);

  $this->app->instance($class, $mock);

  return $mock;
}

Метод setUp() запускается до любого из тестов. Здесь мы захватываем копию UserRepository и создаем новый макет.

В методе mock() $this->app->instance сообщает контейнеру Laurvels IoC привязать экземпляр $mock к классу UserRepository. Это означает, что всякий раз, когда Laravel хочет использовать этот класс, он будет использовать макет вместо этого. Написание первого теста контроллера

Затем вы можете написать свой первый тест контроллера:

public function testIndex()
{
  $this->mock->shouldReceive('all')->once();

  $this->call('GET', 'user');

  $this->assertResponseOk();
}

В этом тесте я прошу макету вызывать метод all() один раз на UserRepository. Затем я вызываю страницу с помощью запроса GET, а затем утверждаю, что ответ был одобрен.

Заключение

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

может вам это помочь.