Составление класса Controller с внедрением зависимостей в PHP - PullRequest
20 голосов
/ 06 февраля 2010

Как решить проблему составления класса Controller в PHP, который должен быть:

  • легко тестируется с помощью Dependency Injection,
  • предоставить общих объектов для конечного программиста
  • предоставляет способ загрузки новых пользовательских библиотек

Посмотрите вниз, для создания экземпляров контроллера с каркасом внедрения зависимостей


Проблема в том, что производные контроллеры могут использовать любые ресурсы, которые хочет программист (например, инфраструктура). Как создать единый доступ к общим ресурсам (БД, Пользователь, Хранилище, Кэш, Помощники), определенным пользователем Классам или другим библиотекам?

Элегантное решение?

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

  • Попробуйте передать все общие объекты конструктору ? (может создать конструктор даже с 10 заполнителями)
  • Создать добытчики, отстойники ? (раздутый код) $controller->setApplication($app)
  • Применить синглетонов на общих ресурсах ? User::getInstance() или Database::getInstance()
  • Использовать Контейнер внедрения зависимостей в качестве синглтона для совместного использования объектов внутри контроллера?
  • предоставить одно глобальное приложение Singleton на заводе? (это выглядит очень используемым в php-фреймворках, но это сильно противоречит принципам DI и закону Деметера)

Я понимаю, что создание сильно связанных классов не рекомендуется и изгоняется :), однако я не знаю, как эта парадигма применяется к отправной точке для других программистов (класс Controller), в который они должны иметь доступ общие ресурсы, предоставляемые архитектуре MVC. Я считаю, что разделение класса контроллера на более мелкие классы каким-то образом разрушит практический смысл MVC.


Структура внедрения зависимостей

DI Framework выглядит жизнеспособным выбором. Однако проблема все еще сохраняется. Такой класс, как Controller, находится не на уровне приложений, а на уровне RequestHandler / Response.

Как этот слой должен создавать экземпляр контроллера?

  • передать инжектор DI в этот слой?
  • DI Framework как синглтон?
  • поместить изолированную конфигурацию DI Framework только для этого уровня и создать отдельный экземпляр DI-инжектора?

Ответы [ 5 ]

3 голосов
/ 14 февраля 2010

Вы сами разрабатываете фреймворк? Если нет, ваш вопрос не применим, потому что вы должны выбрать из уже существующих фреймворков и их существующих решений. В этом случае ваш вопрос должен быть переформулирован так: «Как мне выполнить модульное тестирование / внедрение зависимостей в Framework X».

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

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

Синглтоны - элегантное решение, но, опять же, вы должны спросить себя, применимо ли это в вашем случае? Если в вашем приложении должны быть разные экземпляры одного и того же типа объекта, вы не можете использовать его (например, если вы хотите издеваться над классом только в половине вашего приложения).

Конечно, действительно здорово иметь все варианты. У вас могут быть getters / setter, конструкторы, и когда инициализация опущена, значения по умолчанию берутся из одноэлементной фабрики. Но иметь слишком много опций, когда они не нужны, не удивительно, это вызывает беспокойство, так как программист должен выяснить, какое соглашение, опцию и шаблон использовать. Я определенно не хочу принимать десятки проектных решений только для того, чтобы запустить простой CRUD.

Если вы посмотрите на другие рамки, то увидите, что серебряной пули нет. Часто одна структура использует различные методы в зависимости от контекста. В контроллерах DI - действительно простая вещь, посмотрите на $ helpers, переменные $ components CakePHP, которые инструктируют вводить соответствующие переменные в класс контроллера. Для самого приложения синглтон по-прежнему хорош, так как всегда есть только одно приложение. Свойства, которые реже изменяются / высмеиваются, вводятся с использованием общедоступных свойств. В случае MVC подклассирование также является вполне приемлемым вариантом: точно так же, как AppController, AppView, AppModel в CakePHP. Они вставляются в иерархию классов между фреймворками и всеми вашими классами Controller, View и Model. Таким образом, вы можете объявить глобальные переменные для вашего основного типа классов.

В Java из-за динамических загрузчиков классов и рефлексии у вас есть гораздо больше возможностей для выбора. Но, с другой стороны, вам необходимо поддерживать гораздо больше требований: параллельные запросы, общие объекты и состояния между рабочими потоками, серверами распределенных приложений и т. Д.

Вы можете ответить на вопрос, что подходит именно вам, если вы знаете, что вам нужно в первую очередь. Но на самом деле, зачем вы пишете просто еще один новый фреймворк?

1 голос
/ 05 ноября 2011

Вы также можете использовать ControllerFatory, в который вы передадите свое приложение или Маршрутизатор / Диспетчер

То есть вы можете вызвать $ controllerFactory-> createController ($ name);

Ваше приложение не имеет представления о том, как создавать ваши контроллеры на фабрике. Поскольку вы можете добавить свою собственную ControllerFactory в свой контейнер DI, вы можете управлять всеми зависимостями, которые вы хотите, в зависимости от контроллера.

class ControllerFactory {
    public function __construct(EvenDispatcher $dispatcher,
                                Request $request,
                                ResponseFactory $responseFactory, 
                                ModelFactory $modelFactory, 
                                FormFactory $formFactory) {
        ...
    }

    public function createController($name = 'Default') {
        switch ($name) {
            case 'User':
              return new UserController($dispatcher, 
                                        $request, 
                                        $responseFactory->createResponse('Html'), 
                                        $modelFactory->createModel('User'),
                                        $formFactory->createForm('User'),...);

              break;
            case 'Ajax':
              return new AjaxController($dispatcher, 
                                        $request, 
                                        $responseFactory->createResponse('Json'), 
                                        $modelFactory->createModel('User'));
              break;
            default:
                 return new DefaultController($dispatcher, $request, $responseFactory->createResponse('Html'));
        }
    }

} 

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

class App {
    public function __construct(Router $router,Request $request, ControllerFactory $cf, ... ) {
      ...
    }

    public function execute() {
        $controllerName = $this->router->getMatchedController();
        $actionName $this->router->getMatchedAction();

        $controller = $cf->createController($controllerName);

        if(is_callable($controller, $actionName)) {
            $response = $controller->$action(request);
            $response->send();     
        }
    }
}

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

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

Я знаю, что этот ответ уже был одобрен, но это мои 2 цента по теме

Есть хорошая статья на эту тему

http://miller.limethinking.co.uk/2011/07/07/dependency-injection-moving-from-basics-to-container/

1 голос
/ 19 января 2011

Насколько я понимаю, ваш класс Application должен быть диспетчером. Если это так, я бы предпочел использовать конструктор контроллера для передачи экземпляра приложения, чтобы контроллер знал, кто его вызывает. Позже, если вы захотите иметь другой экземпляр Application в зависимости от того, вызывается ли код из CLI, вы можете иметь ApplicationInterface, который будет реализован Application \ Http и Application \ Cli, и все будет легко поддерживать.

Вы могли бы также реализовать некоторый шаблон фабрики, чтобы получить хорошую реализацию DI. Например, проверьте метод createThroughReflection здесь: https://github.com/troelskn/bucket/blob/master/lib/bucket.inc.php

Надеюсь, это имеет смысл.

С уважением, Ник

1 голос
/ 11 февраля 2010

Как насчет рефакторинг ?

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

1 голос
/ 06 февраля 2010

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

Скорее всего, у вас будет контроль над созданием контроллеров, поэтому вы можете обойтись без упомянутых $controller->setApplication($application), но при необходимости вы можете использовать статические методы и переменные (которые гораздо менее вредны для ортогональности приложения, чем Синглтоны); а именно Controller::setApplication() и доступ к статическим переменным через методы экземпляра.

например:

// defining the Application within the controller -- more than likely in the bootstrap
$application = new Application();
Controller::setApplication($application);

// somewhere within the Controller class definition
public function setContentType($contentType)
{
    self::$application->setContentType($contentType);
}

У меня есть привычка разделять статические свойства и свойства экземпляра и методы (где это необходимо, и все еще группировать свойства в верхней части определения класса). Я чувствую, что это менее громоздко, чем наличие синглетонов, так как классы все еще остаются довольно компактными.

...