• Название:

    A year with symfony ru (1)

  • Размер: 5.09 Мб
  • Формат: PDF
  • или
  • Название: Один год с Symfony
  • Автор: Dmitry Bykadorov и Matthias Noback

Один год с Symfony
Перевод книги “A year with Symfony” от Matthias Noback
Dmitry Bykadorov и Matthias Noback
Эта книга предназначена для продажи на http://leanpub.com/a-year-with-symfony-ru
Эта версия была опубликована на 2016-11-30

Это книга с Leanpub book. Leanpub позволяет авторам и издателям участвовать в так
называемом Lean Publishing - процессе, при котором электронная книга становится
доступна читателям ещё до её завершения. Это помогает собрать отзывы и пожелания для
скорейшего улучшения книги. Мы призываем авторов публиковать свои работы как
можно раньше и чаще, постепенно улучшая качество и объём материала. Тем более, что с
нашими удобными инструментами этот процесс превращается в удовольствие.
© 2016 Dmitry Bykadorov и Matthias Noback

Оглавление
От переводчика . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1

Предисловие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

2

Введение . . . . . . . . . . . . . . .
Благодарности . . . . . . . . . .
Кому предназначена эта книга .
Соглашения . . . . . . . . . . . .
Обзор содержания книги . . . .

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

4
5
6
7
8

I От запроса до ответа . . . . . . . . . . . . . . . . . . . . . . . .
1 HttpKernelInterface . . . . . . . . . . . . . . . . . . . . . . . .
1.1 Загрузка ядра . . . . . . . . . . . . . . . . . . . . . . . .
1.2 От Kernel до HttpKernel . . . . . . . . . . . . . . . . . .
2 События, приводящие к ответу . . . . . . . . . . . . . . . .
2.1 Ранний ответ . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Определение контроллера для запуска . . . . . . . . .
2.3 Возможность замены контроллера . . . . . . . . . . .
2.4 Сбор аргументов для выполнения контроллера . . . .
2.5 Выполнение контроллера . . . . . . . . . . . . . . . . .
2.6 Вход в слой представления (view) . . . . . . . . . . . .
2.7 Фильтрация ответа . . . . . . . . . . . . . . . . . . . . .
3 Обработка исключений . . . . . . . . . . . . . . . . . . . . .
3.1 Примечательные слушатели события kernel.exception
4 Подзапросы . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1 Когда используются подзапросы? . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

10
11
12
15
17
17
19
21
22
23
24
26
28
29
31
31

II Приёмы внедрения зависимостей . . . . . . . . . . .
5 Что такое бандл (bundle) . . . . . . . . . . . . . . . .
6 Приёмы создания сервисов . . . . . . . . . . . . . . .
6.1 Обязательные зависимости . . . . . . . . . . . .
6.2 Необязательные (опциональные) зависимости
6.3 Коллекции сервисов . . . . . . . . . . . . . . . .
6.4 Делегирование создания . . . . . . . . . . . . .
6.5 Создание сервисов вручную . . . . . . . . . . .
6.6 Класс Configuration . . . . . . . . . . . . . . . . .
6.7 Динамическое добавление тагов . . . . . . . . .

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

33
34
35
35
39
41
48
50
52
54

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

ОГЛАВЛЕНИЕ

6.8 Используем паттерн Стратегия для загрузки сервисов . . . . . . . . . . .
6.9 Загрузка и конфигурирование дополнительных сервисов . . . . . . . . .
6.10 Настраиваем какой сервис использовать . . . . . . . . . . . . . . . . . .
6.11 Полностью динамическое определение сервисов . . . . . . . . . . . . .
7 Приёмы создания параметров . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.1 Файл parameters.yml . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2 Определение и загрузка параметров . . . . . . . . . . . . . . . . . . . . .
7.3 Определяем параметры в расширениях контейнера . . . . . . . . . . . .
7.4 Переопределение параметров при помощи компилятора (compiler pass)
III Структура проекта . . . . . . . . .
8 Организация слоёв приложения
8.1 Тонкие контроллеры . . . .
8.2 Обработчики форм . . . . .
8.3 Доменные менеджеры . . .
8.4 События . . . . . . . . . . .
Состояния и контекст . . . . . . .
9.1 Контекст безопасности . .
9.2 Запрос . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

56
58
60
62
64
64
65
68
69

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

71
72
72
73
75
77
83
83
86

IV Соглашения по конфигурированию . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Настройка конфигурации приложения . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Соглашения по конфигурированию . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
V Безопасность . . . . . . . . . . .
Введение . . . . . . . . . . . . .
Аутентификация и сессии . . .
Проектирование контроллеров
Проверка ввода . . . . . . . . . .
Экранирование вывода . . . . .
Будучи скрытным… . . . . . . .

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

. 94
. 95
. 96
. 97
. 98
. 99
. 100

VI Используем аннотации . . . . . . . . . . . . . . . . . . .
Введение . . . . . . . . . . . . . . . . . . . . . . . . . . .
Аннотация - это лишь Value Object . . . . . . . . . . . .
Приемлемые случаи для использования аннотаций . .
Используем аннотации в вашем Symfony приложении
Проектирование для повторного использования . . . .
Заключение . . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

. 101
. 102
. 103
. 104
. 105
. 106
. 107

VII Быть Symfony разработчиком . . . . . . . . . . . . . . . . . . . . . . . . .
Код для повторного использования имеет слабые связи . . . . . . . . . . .
Код для повторного использования должен быть переносимым . . . . . .
Код для повторного использования должен быть расширяемым . . . . . .
Код для повторного использования должен быть прост в использовании
Код для повторного использования должен быть надёжен . . . . . . . . .

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

. 108
. 109
. 110
. 111
. 112
. 113

ОГЛАВЛЕНИЕ

Заключение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114

От переводчика
Книгу “Один год с Symfony” написал разработчик из Голландии - Matthias Noback. Книга в
английском варианте доступна на сайте leanpub.
Маттиас, по завершению работы над книгой, сделал её доступной бесплатно, так что вы
можете обратиться к первоисточнику. Сам же я об этой книге я узнал случайно, из какой-то
рассылки, как раз когда она стала бесплатной. Да, она про Symfony2, но она описывает и
более общиее принципы разработки нежели просто версию одного фреймворка, так что я
полагаю, что эта книга еще долго будет актуальна. Во всяком случае для тех, кто использует
Symfony3 она также “must read”.
Касательно перевода: некоторые фразы и конструкции я перевел, как мне кажется, в более
литературном виде. Некоторые, привычные разрабочикам термины, я не стал переводить
и просто транслитерировал, например: фреймворк, бандл. Имена же, наоборот, оставил без
перевода на случай, если вы захотите загуглить - кто же это такой. Небольшие соглашения
по наименованиям:





framework - фреймворк;
bundle - бандл;
controller - контроллер;
router (route) - маршрутизатор (маршрут)

Мои примечания будут расположены в тексте в таком виде: (@dbykadorov: текст примечания). Если же примечание будет большое, это будет отдельный абзац, начинающийся
с @dbykadorov. Также я намерен проверять все примеры на Symfony 3.2, так что, если
будут какие-то расхождения в работе ныне актуальной версии Symfony с той, которую
использовал Маттиас (2.3) - я укажу на это и предложу также вариант для 3.2.
Если у вас есть предложение как улучшить перевод - пишите мне или делайте pull-request.
Я надеюсь, перевод и книга вам поравятся.
Happy coding!
Dmitry Bykadorov

1

Предисловие
От Luis Cordova
Большинство open source проектов имеют свою историю и серьёзные основания для их
появления и развития. PHP фреймворк Symfony2 (а теперь и 3) активно развивается посление несколько лет. Многие разработчики, попробовав использовать этот фреймфорк,
испытывали и испытывают сложности, разбираясь в нюансах его функционирования. И,
хотя большинство из них так или иначе преодолевают все трудности, в конце концов у них
остаётся много сомнений, касательно того как же всё-таки правильно разрабатывать в стиле
Symfony.
У Symfony1 основной документацией была книга, котороя освещала основые особенности
и практики в использовании этого фреймворка. У Symfony2 также есть книга, которая является основной документацией для него (@dbykadorov: на начало августа 2016 года это уже
не так - был произведен крупный рефакторинг структуры документации, с целью сделать
её более дружелюбной для новичков. Подробнее читайте тут. И да, я надеюсь что кто-то
читает предисловия). Приложением к книге идёт “Книга Рецептов” - Cookbook, которая
более детально раскрывает некоторые аспекты практического использования фреймворка.
Тем не менее, за прошедшие годы, далеко не все аспекты использования и инженерные
практики были отражены в этих книгах, однако это не значит, что они менее важны и
востребованы разработчиками, которые хотят знать ответы не только на вопрос “как”, но и
“почему?”. Разработчики также испытывают необходимость в изучении “лучших практик”,
которые постигаются только в ежедневном использовании Symfony на реальных проектах. В
сети есть блоги о разработке на Symfony, но они разрознены и требуют от читателей знания
множества вещей, в том числе и экспериментальных фич и особенностей фреймворка. Чего
всем этим страждущим знаний не хватало до сих пор - это авторитетного технического
заключения, представленного в виде книги и указывающего на путь в стиле Symfony.
Matthias работает с Symfony уже много лет, отлично понимает его изнутри и отвечает на
наши вопросы “почему”. Эту миссию я не доверил бы никому кроме Сертифицированного
Разрабочика Symfony. Изредка я чувствую, что понимаю Symfony настолько хорошо, что
могу использовать его в своём арсенале. Во время чтения этой книги - был как раз такой
момент. Другой раз такое ощущение у меня было, когда я читал Kris Wallsmith - разъяснения
о том как работает Symfony Security Component. Я считаю, эта книга “Один год с Symfony”
должна быть в арсенале кажого разработка, который стремится к глубокому пониманию
фреймворка.
Matthias в этой книге раскрыл мне много секретов, так что я думаю вы тоже будете частенько подглядывать в неё, чтобы посмотреть ту или иную рекомендацию, и узнать, что надо
делать в том или ином случае. Matthias также написал о таких вещах, о которых я даже не
думал, что здесь их найду и он сделал это в очень доходчивой манере, с примерами кода.
Я очень надеюсь, что вам понравится эта книга.
Ваш друг из сообщества symfony,
2

Предисловие

Luis Cordova (@cordoval)

3

Введение
Один год с Symfony. На самом деле, для меня это был даже не год, а почти 6 лет. Начинал я с
symfony 1 (именно так, с маленькой буквы и отдельно стоящая единичка), потом продолжил
с Symfony2. Symfony2 - это то, что можно охарактеризовать как “взрослый” фреймворк, с его
помощью вы можете делать весьма продвинутые вещи. И когда вы захотите сделать чтонибудь продвинутое, вам даже не обязательно устанавливать весь фреймворк, вы можете
воспользоваться одим или несколькими из его компонентов.
Начало работы с Symfony2 означало для меня следующее: изучение множества вещей о
программировании в общем и применение многих вещей, о которых я узнал из книг к
любому коду, который я написал с тех пор. Symfony2 сделал это: воодушевил меня делать
вещи правильно.
В то же время я много писал о Symfony2, внося вклад в его документацию (конкретно
- некоторые статьи из Cookbook и документацию на Security и Config компоненты), я
запустил свой блог со статьями о PHP в общем, Symfony2, его компонентах и сопутствующих
фреймворках и библиотеках, таких как Silex и Twig. И я стал Сертифицированным разработчиком Symfony2 во время самой первой экзаменационной сессии в Париже, на Symfony Live
Conference в 2012 году.
Всё это нашло отражение в книге, которую вы сейчас читаете - “Один год с Symfony”.
Она содержит многие из лучших практик, которые я и мои уважаемые коллеги из IPPZ
разработали, трудясь над крупными приложениями на Symfony2. Она также наделит вас
более глобокими познаниями, которые вам потребуются, когда вы копнёте чуть глубже, чем
просто написание контроллеров или шаблонов.

4

Введение

5

Благодарности
Прежде чем я продолжу, позвольте мне поблагодарить несколько человек, которые помогли
мне закончить эту книгу. В первую очередь, Luis Cordova, который следовал по моим стопам,
с тех пор как я впервые начал писать о Symfony2 в 2011. Он провел исчерпывающий
анализ первых черновиков. Мои коллеги из IPPZ также предоставили мне очень ценные
замечания,воодушевляя меня делать некоторые вещи более понятными, а другие - более
интересными: Dennis Coorn, Matthijs van Rietschoten и Maurits Henneke. Работая с ними два
года, разделяя с ними опасения по поводу поддерживаемости, читабельности, повторного
использования и прочих насущных вопросов, таких как смехотворность (@dbykadorov:
здесь речь шла о всяких “*bilities”, например “laughability”, не уверен в корректности
перевода) я получил массу положительных эмоций. Также хочу поблагодарить Lambert
Beekhuis, организатора митапов датской юзергруппы Symfony2, за то что дал мне очень
ценные советы касательно моего английского.

Введение

6

Кому предназначена эта книга
Я написал эту книгу для разработчков, которые хорошо знают PHP, но с Symfony2 знакомы несколько недель, может быть месяцев. Я предполагаю, что вы прочли официальную
документацию Symfony2 и уже знакомы с основами создания приложения на Symfony2. Я
также полагаю, что вы уже знаете базовую структуру приложения (стандартную структуру
директорий, как создать или подключить бандл), как создать контроллер и сконфигурировать маршрутизатор для него, как создавать формы и Twig шаблоны.
Я также полагаю, что вы успели поработать с какой-либо библиотекой для взаимодействия
с базами данных, например Doctrine ORM, Doctrine MongoDB ODM, Propel, и так далее. Тем
не менее, в этой книге для упрощения я буду использовать только Doctrine. Если вы используете другую библиотеку для сохранения объектов, вы вероятно сможете разобраться, как
применить идеи изложенные в этой книге к коду, написанному под вашу библиотеку.

Введение

7

Соглашения
Так как эта книга только про Symfony2, с данного момента я буду писать просто “Symfony”
- это выглядит более элегантно. Всё что я скажу о Symfony, будет относиться к версии 2.
Я написал и протестировал примеры кода для этой книги на Symfony 2.3. Тем не менее,
они могут быть вполне применимы к Symfony 2.1.* и 2.2.* и, возможно, к Symfony 2.0.*.
(@dbykadorov: версия 3 конечно имеет отличия от версии 2, но большинство сказанного,
особенно базовые принципы разработки типа “low coupling” - будут также справедливы и
для неё. Я постараюсь отметить эти отличия в ходе перевода и протестировать все примеры
на Symfony 3.2)
В этой книге я покажу примеры кода самого фреймворка Symfony. Для удобства отображения на странице и большей читабельности, время от времени я модифицировал его.

Введение

8

Обзор содержания книги
Первая часть этой книги называется “путешествие от запроса до ответа”. Она проведёт вас
от точки входа в приложение Symfony во фронт-контроллере, до последнего вздоха, который фреймворк делает перед тем, как отправить ответ клиенту. Я покажу как внедриться в
этот процесс и модифицировать его или же изменить результаты промежуточных шагов.
Следующая часть называется “Шаблоны внедрения зависимостей”. Она содержит коллекцию шаблонов, которые являются решениями периодически возникающих проблем, возникающих приложение создании или модификации сервисов, основанных на конфигурации бандла. Я покажу вам много практических примеров, которые вы сможете использовать для создания расширений, конфигурацонных классов и проходов компилятора
(compiler passes) для ваших бандлов.
Третья часть будет посвящена структуре проекта. Я предложу различные способы как сделать ваши контроллеры более понятными, путём делегирования действий обработчикам
форм (form handlers), доменным менеджерам (domain managers) и слушателям событий
(event listeners). Мы также посмотрим на состояния и как избежать их на сервисном уровне
вашего приложения.
Далее последует небольшое интермеццо о соглашениях по конфигурированию. Эта часть
должна будет помочь вам наладить конфигурирование вашего приложения. Эта глава воодушевит вас принять и использовать некоторые полезные соглашения по конфигурированию.
Пятая часть очень важна, так как она касается любого более-менее серьёзного приложения,
использующего пользовательские сессии и уязвимые данные, например пароли пользователей. Эта часть будет о безопасности. В идеале здесь должны были бы быть затронуты
все компоненты Symfony (в конце концов и сам фреймворк прошел аудит безопасности)
и Twig, но, к сожалению, это не возможно. Вы всегда должны быть начеку и заботиться о
безопасности вашего приложения. Эта часть книги содержит различные советы о том, как
быть с безопасностью приложения, на что обратить внимание, когда вы можете положиться
на фреймворк и когда вам нужно контролировать безопасность самостоятельно.
Шестая целиком будет посвящшена аннотациям. Когда Symfony2 впервые вышел в релиз в
2011 году, он представил всем аннотации как революционный способ конфигурирования
приложения через док-блоки классов, методов и свойств. Первая глава этой части разъясняет как аннотации работают. После этого вы узнаете как создавать свои собственные
аннотации и как можно использовать аннотации для того, чтобы воздействовать на ответ,
который генерируется для запроса.
Заключительная часть будет о том, как быть Symfony-разработчиком, хотя, в сущности,
эта часть будет вдохновлять вас не быть только разработчиком Symfony, но писать код
как можно менее зависимый от Symfony или от любого другого фреймворка. Это означает
разделение кода на повторно используемый и специфичный для конкретного проекта, а
затем выделение повторно используемого кода в библиотеки и бандлы. Я буду обсуждать
и прочие идеи, который сделают ваш код красивым, чистым и дружелюбным к другим
проектам.

Введение

Наслаждайтесь!

9

I От запроса до ответа

10

I От запроса до ответа

11

1 HttpKernelInterface
Symfony знаменит благодаря своему HttpKernelInterface:
1

namespace Symfony\Component\HttpKernel;

2
3
4

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

5
6
7
8
9

interface HttpKernelInterface
{
const MASTER_REQUEST = 1;
const SUB_REQUEST = 2;

10

/**
* @return Response
*/
public function handle(
Request $request,
$type = self::MASTER_REQUEST,
$catch = true
);

11
12
13
14
15
16
17
18
19

}

Реализация этого интерфейса должна содержать один метод и с его помощью иметь возможность каким либо образом превратить полученный запрос в ответ. Если вы взглянете
на любой из фронт-контроллеров в директории /web вашего Symfony проекта, вы можете
увидеть, что этот метод handle() играет главную роль в обработке веб-запроса - чего и
стоило ожидать:
1
2
3
4
5
6
7

// /web/app.php
$kernel = new AppKernel(’prod’, false);
// ...
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
// ...

Сначала создаётся экземпляр ядра AppKernel. Это класс - специфичный для вашего приложения и вы можете найти его в диретории app: /app/AppKernel.php. Он позволяет регистрировать ваши бандлы и изменять некоторые основные настройки, такие как распложение
директории с кэшем или указание какой конфигурационный файл нужно загрузить. Аргументы его конструктора - это наименование окружения и флаг активации режима отладки
в ядре (debug mode).

Окружение
Окружением (или именем окружения), может быть любая строка. В основном,
это способ определить, какой конфигурационный файл должен быть загружен
(например, config_dev.yml или же config_prod.yml). Эта проверка производится в
классе AppKernel:

I От запроса до ответа

1
2
3
4
5

12

public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader
->load(__DIR__.’/config/config_’.$this->getEnvironment().’.yml’);
}

Режим отладки
В режиме отладки у вас будут следующие возможности:
• Удобная и информативная страница ошибки, отображающая информацию
о запросе для дальнейшей отладки;
• Подробные сообщения об ошибках если страница ошибки из предыдущего
пункта не может быть отображена;
• Исчерпывающая информация о времени выполнения отдельных частей приложения (начальная загрузка, обращения к базе данных, рендеринг шаблонов и так далее).
• Расширенная информация о запросах (с использованием веб-профайлера и
сопутствущей панели).
• Автоматическая инвалидация кэша: эта функция позволяет не беспокоиться
о том что изменения в config.yml, routing.yml и прочих конфигурационных
файлах не будут учтены без пересборки всего сервисного контейнера или
сопоставителя маршрутов (routing matcher) для каждого запроса (однако,
это занимает больше времени).
Продолжаем разбирать процесс обработки запроса: далее создается объект Request, базирующийся на существующих суперглобальных массивах ($_GET, $_POST, $_COOKIE, $_FILES и
$_SERVER). Класс Request вместе с прочими классами компонента HttpFoundation предоставляет объектно-ориентированный интерфейс к этим суперглобальным массивам. Все
эти классы также покрыват разичные проблемные ситуации, которые могут возникать при
использовании разных версий PHP или же разных платформ. Всегда использовать объект
Request для получения различных данных о запросе, вместо того чтобы использовать
суперголобальные переменные, это разумный выбор (в контексте Symfony).
Итак, далее вызывается метод handle() экземпляра AppKernel. Его единственным аргументом является объект Request для текущего запроса. Аргументы для типа запроса
(“master”) и нужно ли перехватывать и обрабатывать исключения (да, перехватывать)
берутся по умолчанию. Результат метода handle() гарантированно будет экземпляром
класса Response (также являющегося частью компонента HttpFoundation). И, наконец, ответ
будет отправлен обратно клиенту, который сделал запрос, например браузеру.

1.1 Загрузка ядра
Как вы уже могли догадаться, вся магия происходит внутри метода handle() в ядре. Вы
можете найти реализацию этого метода в классе Kernel, который является родителем класса
AppKernel:

I От запроса до ответа

1

13

// Symfony\Component\HttpKernel\Kernel

2
3
4
5
6
7
8
9
10

public function handle(
Request $request,
$type = HttpKernelInterface::MASTER_REQUEST,
$catch = true
) {
if (false === $this->booted) {
$this->boot();
}

11

return $this->getHttpKernel()->handle($request, $type, $catch);

12
13

}

Во-первых, проверяется что ядро загружено, прежде чем произойдёт обращение к HttpKernel.
Процесс загрузки включает в себя:
• Инициализация всех зарегистрированных бандлов;
• Инициализация сервисного контейнера;
Бандлы и расширения контейнера
Бандлы широко известны среди разработчиков на Symfony как место, где размещается разрабатываемый вами код. Каждый бандл должен иметь имя, котрое отражает его
назначение. Например, у вас могут быть такие бандлы: BlogBundle, CommunityBundle,
CommentBundle, и так далее. Вы регистрируете ваши бандлы в AppKernel.php, добавляя их
к существующему списку:
1
2
3
4
5
6
7
8
9

class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
// ... тут прочие бандлы
new Matthias\BlogBundle()
);

10

return $bundles;

11

}

12
13

}

И это определённо хорошая идея - можно легко расширить функциональность вашего
проекта, добавив лишь одну строчку кода. Тем не менее, когда мы смотрим на ядро Kernel и
на то, как оно работает с бандлами, включая ваши, становится ясно, что бандлы прежде
всего понимаются как способ расширения сервисного контейнера, а не как библиотеки
кода. Вот почему вы найдёте директорию DependencyInjection внутри любого бандла, а
внутри неё класс {наименованиеБандла}Extension. В процессе инициализации сервисного
контейнера, каждый бандл имеет возможность зарегистрировать собственные сервисы в
сервисном контейнере, также можно добавить несколько параметров, и даже при необходимости модифицировать определения некоторых сервисов, прежде чем контейнер будет
скомпилирован и выгружен в директорию с кэшем:

I От запроса до ответа

1

14

namespace Matthias\BlogBundle\DependencyInjection;

2
3
4
5

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

6
7
8
9
10
11
12

class MatthiasBlogExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new XmlFileLoader($container,
new FileLocator(__DIR__.’/../Resources/config’));

13

// добавляем определения сервисов в контейнер
$loader->load(’services.xml’);

14
15
16

$processedConfig = $this->processConfiguration(
new Configuration(),
$configs
);

17
18
19
20
21

// добавляем параметр
$container->setParameter(
’matthias_blog.comments_enabled’,
$processedConfig[’enable_comments’]
);

22
23
24
25
26

}

27
28

public function getAlias()
{
return ’matthias_blog’;
}

29
30
31
32
33

}

Имя, возвращаемое методом getAlias() - это фактически ключ, по которому вы можете
устанавливать значения параметров (например в config.yml):
1
2

matthias_blog:
enable_comments: true

Подробнее о конфигурировании бандлов вы узнаете в следующем разделе - “Шаблоны
внедрения зависимостей”. Каждый корневой ключ в конфигурации соответствует определенному бандлу. В примере выше вы могли заметить, что matthias_blog это ключ
к настройкам, относящимся к MatthiasBlogBundle. Так что для вас не будет большим
сюрпризом, что это также справедливо для всех корневых ключей, которые вы можете
встретить в файле config.yml и прочих ему подобных: настройки, доступные по ключу
framework относятся к FrameworkBundle, ключ security (даже если он определен в
другом файле - security.yml) относится к SecurityBundle. Проще простого!
Создание сервисного контейнера
После того, как все бандлы подключили свои сервисы и параметры, создание контейнера
завершается процессом, которые имеет наименование “компиляция”. В ходе этого процесса всё ещё остаётся возможность внести последние изменения в определения сервисов или
изменить параметры. Также, это хороший момент для того, чтобы проверить правильность

I От запроса до ответа

15

и оптимизировать определения сервисов. После этого контейнер принимает свой окончательный вид и он дампится на диск в двух различных форматах: в XML файл с определениями всех обнаруженных сервисов и параметров и в PHP файл, готовый для использования в
качестве единого и единственного сервисного контейнера для вашего приложения. Оба эти
файла вы можете найти в директории с кэшем, соответствующей окружению с которым было загружено ядро, например, /app/cache/dev/appDevDebugProjectContainer.xml. XML файл
выглядит как любой другой файл с определениями сервисов, только намного больше:
1
2
3
4
5
6
7
8




kernel.controller





PHP файл содержит метод для каждого сервиса, который может быть запрошен. Вся логика
создания сервисов, такая как аргументы контроллера, вызовы методов после инициализации, можно найти в этом файле, и это пожалуй замечательное место для отладки определений ваших сервисов, если вдруг с ними что-то идёт не так:
1
2
3

class appDevDebugProjectContainer extends Container
{
// ...

4

protected function getEventDispatcherService()
{
$this->services[’event_dispatcher’] =
$instance = new ContainerAwareEventDispatcher($this);

5
6
7
8
9

$instance->addListenerService(’kernel.controller’, ...);

10
11

// ...

12
13

return $instance;

14

}

15
16

//...

17
18

}

1.2 От Kernel до HttpKernel
Теперь, когда ядро загружено (т.е. все бандлы инициализированы, их расширения зарегистрированы и сервисный контейнер инициализирован), раельная обработка запроса
ложится на плечи экземпляра класса HttpKernel:

I От запроса до ответа

1

16

// Symfony\Component\HttpKernel\Kernel

2
3
4
5
6
7
8
9
10

public function handle(
Request $request,
$type = HttpKernelInterface::MASTER_REQUEST,
$catch = true
) {
if (false === $this->booted) {
$this->boot();
}

11

return $this->getHttpKernel()->handle($request, $type, $catch);

12
13

}

Класс HttpKernel реализует интерфейс HttpKernelInterface и точно знает как конвертировать
запрос в ответ. Метод handle() выглядит так:
1
2
3
4
5
6
7
8
9
10
11

public function handle(
Request $request,
$type = HttpKernelInterface::MASTER_REQUEST,
$catch = true
) {
try {
return $this->handleRaw($request, $type);
} catch (\Exception $e) {
if (false === $catch) {
throw $e;
}

12

return $this->handleException($e, $request, $type);

13

}

14
15

}

Как вы можете видеть, основная часть работы выполняется приватным методом handleRaw(),
и блок try/catch здесь нужен для перехвата любых исключений. Когда аргумент $catch
имеет значение true (что является значением по умолчанию для “master”-запросов), каждое
исключение будет перезвачено. HttpKernel постарается найти кого-то, кто сможет создать
объект Response для (см. также главу “Обработка исключений”).

I От запроса до ответа

17

2 События, приводящие к ответу
Метод handleRaw() класса HttpKernel это замечательный пример кода, анализируя который становится ясно, что алгоритм обработки запроса сам по себе не является дерерминированным (@dbykadorov: т.е. допускает отклонения и изменения в процессе). Это означает,
что у вас есть несколько различных путей для внедрения в этот процесс, путём которого вы
можете либо полностью заменить или модифицировать ответ на промежуточных шагах его
формирования.

2.1 Ранний ответ
Как вы думаете, когда вы можете взять контроль над обработкой запроса? Ответ - сразу
же после начала его обработки. Как правило, HttpKernel пытается сгенерировать ответ, выполняя контроллер. Однако, любой слушатель (listener), который ожидает событие
KernelEvents::REQUEST (kernel.request) может сгенерировать полностью уникальный ответ:
1

use Symfony\Component\HttpKernel\Event\GetResponseEvent;

2
3
4
5
6

private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
$event = new GetResponseEvent($this, $request, $type);
$this->dispatcher->dispatch(KernelEvents::REQUEST, $event);

7

if ($event->hasResponse()) {
return $this->filterResponse(
$event->getResponse(),
$request,
$type
);
}

8
9
10
11
12
13
14
15

// ...

16
17

}

Как вы можете видеть, объект события тут - это экземпляр GetResponseEvent и он позволяет слушателям заменить объект Response на свой, используя метод события setResponse(),
например:
1
2

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;

3
4
5
6
7
8
9
10
11

class MaintenanceModeListener
{
public function onKernelRequest(GetResponseEvent $event)
{
$response = new Response(
’This site is temporarily unavailable’,
503
);

12

$event->setResponse($response);

13

}

14
15

}

I От запроса до ответа

18

Регистрация слушетелей событий (event listeners)
Диспетчер событий, используемый классом HttpKernel также доступен как
сервис event_dispatcher. Когда вы захотите автоматически зарегистрировать
какой-нибудь класс, как слушатель, Вам нужно будет создать сервис для него и
добавить ему тэг kernel.event_listener или kernel.event_subscriber (в
случае, если вы хотите реализовать интерфейс EventSubscriberInterface).
1
2
3
4
5


event=”kernel.request”
method=”onKernelRequest” />


Или:
1
2
3





Вы также можете указать приоритет вашего слушателя, что может дать ему
преимущество перед другими слушателями:
1
2
3
4
5
6


event=”kernel.request”
method=”onKernelRequest”
priority=”100” />


Чем выше приоритет, тем тем раньше слушатель события будет уведомлен.
Слушатели kernel.request, о которых вам нужно знать
Фреймворк содержит много слушателей события kernel.request. В основном, это слушатели, делающие некоторые приготовления, прежде чем дать ядру возможность вызвать
какой-либо контроллер. Например, один слушатель даёт возможность приложению использовать локали (например, локаль по умолчанию или же _locale из URI), другой обрабатывает
запросы фрагментов страниц.
Тем не менее, имеется два основных игрока, на стадии ранней обработке запроса: RouterListener
и Firewall. Слушатель RouterListener получает инвормаю о запрошенном пути из
запроса Request и пытается сопоставить его с одним из известных маршрутов. Он хранит
результат процесса сопоставления в объекте запроса в качестве атрибута, например, в виде
имени контроллера, который соответствует найденному маршруту:

I От запроса до ответа

1

19

namespace Symfony\Component\HttpKernel\EventListener;

2
3
4
5
6
7

class RouterListener implements EventSubscriberInterface
{
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();

8

$parameters = $this->matcher->match($request->getPathInfo());

9
10

// ...

11
12

$request->attributes->add($parameters);

13

}

14
15

}

Например, когда сопоставителя запросов (matcher) просят найти контроллер для пути
/demo/hello/World, а конфигурация маршрутов выглядит таким образом:
1
2
3
4

_demo_hello:
path: /demo/hello/{name}
defaults:
_controller: AcmeDemoBundle:Demo:hello

то параметры, возвращаемые методом match() будут являться комбинацией значений,
определённых в секции defaults:, а также значений переменных (типа {name}), которые
будут заменены на их значения из запроса:
1
2
3
4
5

array(
’_route’ => ’_demo_hello’,
’_controller’ => ’AcmeDemoBundle:Demo:hello’,
’name’ => ’World’
);

Эти данные сохраняются в объекте Request, в структуре типа parameter bag, имеющей
наименование attributes. Несложно догадаться, что в дальнейшем, HttpKernel проверит эти атрибуты и выполнит запрошенный контроллер.
Другой, не менее важный слушатель - это Firewall. Как уже было отмечено ранее, RouterListener
не предоставляет объект Response ядру HttpKernel, он лишь выполняет некоторые действия в начале процесса обработки запроса. Firewall же, напротив, иногда даже принудительно возвращает некоторые предопределённые экземпляры ответов, например, когда
пользователь не аутентифицирован, когда должен был бы, так как запрашивает защищенную страницу. Firewall (посредством сложного процесса) форсирует редирект на страницу логина (например), или устанавливает некоторые заголовки, которые обязуют пользователя ввести его логин и пароль и аутентифицироваться при помощи HTTP-аутентивикации.

2.2 Определение контроллера для запуска
Выше мы уже видели, что RouterListener устанавливает атрибут запроса, именуемый
_controller и содержащий некоторую ссылку на контроллер, который необходимо выполнить. Эта информация не известна HttpKernel. Вместо этого имеется специальный
объект - ControllerResolver, который ядро запрашивает, чтобы получить контроллер
для обработки текущего запроса:

I От запроса до ответа

1
2
3
4

20

private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие ”kernel.request”
...

5

if (false === $controller = $this->resolver->getController($request)) {
throw new NotFoundHttpException();
}

6
7
8
9

}

Резолвер является экземпляром класса, реализующего интерфейс ControllerResolverInterface:
1

namespace Symfony\Component\HttpKernel\Controller;

2
3

use Symfony\Component\HttpFoundation\Request;

4
5
6
7

interface ControllerResolverInterface
{
public function getController(Request $request);

8

public function getArguments(Request $request, $controller);

9
10

}

Позднее он будет использоваться для определения аргументов для контроллера, но его
первичной задачей является определение контроллера. Стандартный резолвер получает
контроллер из атрибута _controller обрабатываемого запроса:
1
2
3
4
5

public function getController(Request $request)
{
if (!$controller = $request->attributes->get(’_controller’)) {
return false;
}

6

...

7
8

$callable = $this->createController($controller);

9
10

...

11
12

return $callable;

13
14

}

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

Всё что может быть контроллером
ControllerResolver из компонента HttpKernel Component поддерживает: Массив вызываемых объектов (callable) ([объект, метод] или [класс, статический
метод]) - Вызываемые (invokable) объекты (объекты с магическим методом __invoke(), такие как анонимные функции, которые являются экземплярами
класса \Closure) - Классы вызываемых объектов - Обычные функции
Все прочие определения контроллеров, которые преставлены в виде строки,
должны следовать шаблону class::method. Также ControllerResolver из FrameworkBundle
добавляет дополнительные шаблоны для имён контроллеров:

I От запроса до ответа

21

• BundleName:ControllerName:actionName
• service_id:methodName
После создания экземпляра контроллера, ControllerResolver также проверяет, реализует ли данный контроллер интерфейс ContainerAwareInterface, и
если да, то вызывает его метод setContainer(), чтобы передать ему контейнер.
Вот почему контейнер по умолчанию доступен в стандартном контроллере.

2.3 Возможность замены контроллера
Давайте же вернёмся в HttpKernel: контроллер теперь полностью доступен и почти готов к выполнению. Но даже если мы предположим, что controller resolver выполнил всё
что в его силах, для того чтобы подготовить контроллер к вызову перед его выполнением, сейчас имеется последний шанс заменить его каким-либо другим контроллером
(которым может быть любой callable элемент). Этот шанс нам предоставляет событие
KernelEvents::CONTROLLER (kernel.controller):
1

use Symfony\Component\HttpKernel\Event\FilterControllerEvent;

2
3
4
5
6
7

private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие ”kernel.request”
// определяем контроллер при помощи controller resolver
...

8

$event = new FilterControllerEvent($this, $controller, $request, $type);
$this->dispatcher->dispatch(KernelEvents::CONTROLLER, $event);
$controller = $event->getController();

9
10
11
12

}

Вызов метода setController() объекта класса FilterControllerEvent делает возможным замену контролера, который был подготовлен к исполнению:
1

use Symfony\Component\HttpKernel\Event\FilterControllerEvent;

2
3
4
5
6
7
8
9

class ControllerListener
{
public function onKernelController(FilterControllerEvent $event)
{
$event->setController(...);
}
}

Распространение событий (event propagation)
Когда вы переопределяете промежуточный результат, например, когда вы полностью заменяете контроллер после того как наступило событие kernel.filter_controller, вы можете захотеть, чтобы прочие слушатели этого события, которые будут вызваны после вашего - смогли бы провернуть тот же трюк. Вы можете
это сделать, вызвав метод события:

I От запроса до ответа

1

22

$event->stopPropagation();

Также удостоверьтесь, что ваш слушатель имеет более высокий приоритет и
будет вызван первым. См. также Регистрация слушетелей событий.
Некоторые примечательные слушатели события kernel.controller
Фреймворк сам по себе не имеет слушателей события kernel.controller. То есть только
сторонние бандлы, которые слушают это событие для того чтобы определить тот факт, что
контроллер был определён и что он будет выполнен.
Слушатель ControllerListener из бандла SensioFrameworkExtraBundle, к примеру,
выполняет кое-какую весьма важную работу прямо перед выполнением контроллера, а
иименно: он собирает аннотации типа @Template и @Cache и сохраняет их в виде атрибутов запроса с тем же именем, но с префиксом - подчёркиванием: _template и _cache.
Позднее, в процессе обработки запроса, эти аннотации (или конфигурации, как они названы в коде этого бандла) будут использованы для рендеринга шаблона или же для того, чтобы
установить заголовки, относящиеся к кэшированию.
ParamConverterListener из того же банла умеет конвертировать аргументы контролера,
например загружать сущность (entity) по парметру id, определённому в маршруте:
1
2
3
4
5
6
7

/**
* @Route(”/post/{id}”)
*/
public function showAction(Post $post)
{
...
}

Преобразователи параметров (Param converters)
Бандл SensioFrameworkExtraBundle укомплектован конвертером DoctrineParamConverter,
который помогает конверитровать пары имя/значение (например id), в сущности (ORM) или документы (ODM). Но вы также можете создать свои преобразователи параметров. Вам всего лишь нужно создать класс, реализующий
интерфейс ParamConverterInterface, создать определние сервиса для него
и присвоить ему таг request.param_converter. См. также документацю к
@ParamConverter.

2.4 Сбор аргументов для выполнения контроллера
После того как отработали листенеры, которые могли бы заменить контроллер, мы можем
быть уверены, что результирующий контроллер - тот что нам нужен. Следующий шаг - сбор
аргументов, которые будут использоваться для выполнения результирующего контроллера:

23

I От запроса до ответа

1
2
3
4
5
6

private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие ”kernel.request”
// определяем контроллер при помощи controller resolver
// событие ”kernel.controller”
...

7

$arguments = $this->resolver->getArguments($request, $controller);

8
9

}

Экземпляр controller resolver запрашивается для получения аргументов контроллера. Стандартный ControllerResolver компонента HttpKernel использует рефлексию (reflection)
и атрибуты из объекта Request, для того чтобы определить аргументы контроллера. Он
перебирает все параметры метода контроллера. Для определения каждого из аргументов
используется такая логика:

Логика controller resolver’а

2.5 Выполнение контроллера
Наконец, пришло время выполнить контроллер. Получаем ответ и двигаемся дальше:

I От запроса до ответа

1
2
3
4
5
6
7

24

private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие ”kernel.request”
// определяем контроллер при помощи controller resolver
// событие ”kernel.controller”
// используем controller resolver для того, чтобы получить аргументы контроллера
...

8

$response = call_user_func_array($controller, $arguments);

9
10

if (!$response instanceof Response) {
...
}

11
12
13
14

}

Как вы можете помнить из документации Symfony, контроллер должен возвращать объект
Response. Если же контроллер этого не сделал, какая-то другая часть приложения должна
иметь возможность конвертировать возвращаемое значение в объект Response тем или
иным образом.

2.6 Вход в слой представления (view)
Если вы решили вернуть из вашего контроллера объект Response, вы можете таким образом срезать угол и обойти шаблонизатор, например вернув уже готовую HTML разметку:
1
2
3
4
5
6
7
8
9

class SomeController
{
public function simpleAction()
{
return new Response(

Pure old-fashioned HTML


);
}
}

Тем не менее, когда вы вернёте что-нибудь другое (как правило - массив с переменными рендеринга для шаблона), возвращаемое значение нужно конвертировать в объект
Response, прежде чем он будет использован в качестве результата, которые будет отправлен на клиент (в браузер пользователя). Ядро HttpKernel не привязано ни к какому
конкретному шаблонизатору, типа Twig. Вместо этого оно использует диспетчер событий
(event dispatcher), чтобы позволить любому слушателю события KernelEvents::VIEW
(kernel.view) создать правильный ответ, основанный на значении, которое вернул контроллер (даже если он полностью проигнорирует это значение):

I От запроса до ответа

1

25

use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;

2
3
4
5
6
7
8
9
10

private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие ”kernel.request”
// определяем контроллер при помощи controller resolver
// событие ”kernel.controller”
// используем controller resolver для того, чтобы получить аргументы контроллера
// исполняем контроллер
...

11

$event = new GetResponseForControllerResultEvent(
$this,
$request,
$type,
$response
);
$this->dispatcher->dispatch(KernelEvents::VIEW, $event);

12
13
14
15
16
17
18
19

if ($event->hasResponse()) {
$response = $event->getResponse();
}

20
21
22
23

if (!$response instanceof Response) {
// тут ядру ОООООЧЧЧЕНЬ нужен ответ...

24
25
26

throw new \LogicException(...);

27

}

28
29

}

Слушатели этого события могут использовать метод setResponse() объекта события
GetResponseForControllerResultEvent:
1

use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;

2
3
4
5
6
7

class ViewListener
{
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$response = new Response(...);

8

$event->setResponse($response);

9

}

10
11

}

Примечательные слушатели события kernel.view
Слушатель TemplateListener из арсенала SensioFrameworkExtraBundle получает значение, которое вернул контроллер и использует его в частве переменных для рендеринга
шаблона, который должен быть указан при помощи аннотации @Template (храниться это
значение будет в атрибуте запроса _template):

I От запроса до ответа

1
2
3

26

public function onKernelView(GetResponseForControllerResultEvent $event)
{
$parameters = $event->getControllerResult();

4

// получаем движок шаблонизатора
$templating = ...;

5
6
7

$event->setResponse(
$templating->renderResponse($template, $parameters)
);

8
9
10
11

}

2.7 Фильтрация ответа
Ну и в самом конце, прямо перед тем, как вернуть объект Response в качестве финального
результата обработки текущего объекта запроса Request, будет уведомлен любой слушатель события KernelEvents::RESPONSE (kernel.response):
1
2
3
4
5
6
7

private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие ”kernel.request”
// определяем контроллер при помощи controller resolver
// событие ”kernel.controller”
// используем controller resolver для того, чтобы получить аргументы контроллера
// конвертируем рещультат, который вернул запрос в объект Response

8

return $this->filterResponse($response, $request, $type);

9
10

}

11
12
13
14

private function filterResponse(Response $response, Request $request, $type)
{
$event = new FilterResponseEvent($this, $request, $type, $response);

15

$this->dispatcher->dispatch(KernelEvents::RESPONSE, $event);

16
17

return $event->getResponse();

18
19

}

Слушатели события могут модифицировать объет оответа Response и даже полностью его
заменить:
1
2
3
4
5

class ResponseListener
{
public function onKernelResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();

6

$response->headers->set(’X-Framework’, ’Symfony2’);

7
8

// or

9
10

$event->setResponse(new Response(...));

11

}

12
13

}

I От запроса до ответа

27

Примечательные слушатели события kernel.response
Слушатель WebDebugToolbarListener из комплекта инструментов бандла WebProfilerBundle
внедряет HTML и JavaScript код в конце ответа, для того, чтобы панель профайлера отобразилась (как правило, в конце страницы).
Слушатель ContextListener из компонента Symfony Security Component сохраняет
сериализованную версию токена безопасонсти в сессии. Это позволяет ускорить процесс
аутентификации при следующем запросе. В компонент Security Component также входит
слушатель ResponseListener, который устанавливает cookie, который содержит информацию о remember-me. Содержимое этого cookie может быть использовано для авто-логина
пользователя, даже если оригинальная сессия уже была завершена.

I От запроса до ответа

28

3 Обработка исключений
Не исключено, что в процессе долгого путешествия от запроса до ответа, возникнет та или
иная ошибка. По умолчанию, ядро проинструктировано перехватывать любое исключение
и даже после этого оно пытается подобрать подходящий для него ответ Response. Как мы
уже видели, обработка каждого запроса обёрнута в блок try/catch:
1
2
3
4
5
6
7
8
9
10
11

public function handle(
Request $request,
$type = HttpKernelInterface::MASTER_REQUEST,
$catch = true
) {
try {
return $this->handleRaw($request, $type);
} catch (\Exception $e) {
if (false === $catch) {
throw $e;
}

12

return $this->handleException($e, $request, $type);

13

}

14
15

}

Когда переменная $catch имеет значение истина (true), вызывается метод handleException()
и ожидается, что он вернёт объект ответа. Этот метод отправляет событие KernelEvents::EXCEPTION
(kernel.exception) c экземпляром события GetResponseForExceptionEvent.
1

use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;

2
3
4
5
6

private function handleException(\Exception $e, $request, $type)
{
$event = new GetResponseForExceptionEvent($this, $request, $type, $e);
$this->dispatcher->dispatch(KernelEvents::EXCEPTION, $event);

7

// a listener might have replaced the exception
$e = $event->getException();

8
9
10

if (!$event->hasResponse()) {
throw $e;
}

11
12
13
14

$response = $event->getResponse();

15
16

...

17
18

}

Слушатели события kernel.exception могут:
• Установить объект ответа Response, соответствующий пойманному исключению.
• Заменить исходный объект исключения.
Если ни один из слушателей не вызвал метод события setResponse(), исключение будет
вызвано еще раз, но в этот раз оно не будет перехвачено автоматически. Таким образом,

I От запроса до ответа

29

если в настройка вашего интерпритатора PHP параметр display_errors имеет значение
истина (true), PHP просто отобразит ошибку как есть, без изменений (если отображение
ошибок у вас отключено - не будет выведено ничего). В случае же, если один из слушателей
установил объект ответа Response, класс HttpKernel проверяет этот объект на предмет
корректной установки http статус-кода:
1
2
3

// проверяем наличие специфического статус-кода
if ($response->headers->has(’X-Status-Code’)) {
$response->setStatusCode($response->headers->get(’X-Status-Code’));

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

$response->headers->remove(’X-Status-Code’);
} elseif (
!$response->isClientError()
&& !$response->isServerError()
&& !$response->isRedirect()
) {
// убеждаемся, что у нас действительно есть ответ
if ($e instanceof HttpExceptionInterface) {
// сохраняем HTTP статус-код и заголовки
$response->setStatusCode($e->getStatusCode());
$response->headers->add($e->getHeaders());
} else {
$response->setStatusCode(500);
}
}

Это очень удобно, так как мы можем форсировать любой статус-код, добавляя заголовок
X-Status-Code к объекту ответа Response (важно! это будет работать лишь для исключений, которые были перехвачены в HttpKernel), или же создав исключение, которое реализует интерфейс HttpExceptionInterface. В противном случае статус-код будет иметь
значение по-умолчанию - 500, что означает Internal server error. Это намного лучше,
чем то, что предлагает нам “чистый” PHP, который в случае ошибки вернул бы ответ со
статусом 200, что означает OK. Когда слушатель установил объект ответа, этот ответ не будет
обрабатываться каким-то иным образом, нежели обычный ответ, поэтому последний шаг в
этом процессе - фильтрация ответа. Если в ходе фильтрации ответа будет сгенерировано
другое исключение, это исключение будет проигнорировано и клиенту будет отправлен
неотфильтрованный ответ:
1
2
3
4
5

try {
return $this->filterResponse($response, $request, $type);
} catch (\Exception $e) {
return $response;
}

3.1 Примечательные слушатели события kernel.exception
Слушатель ExceptionListener компонента HttpKernel пытается самостоятельно обработать исключение, логгируя его (если доступен логгер) и выполняя контроллер, который
может отобразить страницу с информацией об ошибке. Как правило, это контроллер, который указан в файле конфигурации config.yml:

I От запроса до ответа

1
2
3

30

twig:
# points to Symfony\Bundle\TwigBundle\Controller\ExceptionController
exception_controller: twig.controller.exception:showAction

Другой важный слушатель - это ExceptionListener компонента Security. Этот слушатель проверяет, является ли исходное исключение экземпляром AuthenticationException
или же AccessDeniedException. В первом случае он начинает процесс аутентификации,
если это возможно. Во втором случае он пытается перелогинить пользователя (например
если тот использовал remember me опцию при логине), если же это не выходит, предоставляет возможность access denied handler разобраться с возникшей ситуацией.

I От запроса до ответа

31

4 Подзапросы
Вероятно вы знаете о том, что при вызове метода HttpKernel::handle() вторым параметром идёт агумент типа запроса - $type:
1
2
3
4
5
6
7

public function handle(
Request $request,
$type = HttpKernelInterface::MASTER_REQUEST,
$catch = true
) {
...
}

В интерфейсе HttpKernelInterface определены две константы, позволяющие определить тип запроса:
1. HttpKernelInterface::MASTER_REQUEST - главный запрос (мастер)
2. HttpKernelInterface::SUB_REQUEST - поздапрос
Для каждого запроса в вашем PHP-приложении, первый запрос, который обрабатывается
ядром - имеет тип мастер - HttpKernelInterface::MASTER_REQUEST. Это определено
неявно, так как аргумент $type не указывается во фронт-контроллерах (app.php и app_dev.php):
1

$response = $kernel->handle($request);

Многие слушатели ожидают события ядра, как это было описано ранее, но включаются в
работу лишь тогда, когда запрос имеет тип HttpKernelInterface::MASTER_REQUEST.
Например, Firewall не будит ничего делать в случае, если запрос имеет тип подзапроса:
1
2
3
4
5

public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}

6

...

7
8

}

4.1 Когда используются подзапросы?
Подзапросы используются для изоляции создания объекта ответа. Например, когда исключение перехвачено ядром, стандартный обработчик исключений пытается выполнить
назначенный для этого исключения контроллер (см. предыдущую часть, Обработка исключений). Для этого создаётся подзапрос:

I От запроса до ответа

1
2
3

32

public function onKernelException(GetResponseForExceptionEvent $event)
{
$request = $event->getRequest();

4

...

5
6

$request = $request->duplicate(null, null, $attributes);
$request->setMethod(’GET’);

7
8
9

$response = $event
->getKernel()
->handle($request, HttpKernelInterface::SUB_REQUEST, true);

10
11
12
13

$event->setResponse($response);

14
15

}

Также, каждый раз, когда вы рендерите контроллер через Twig-шаблон, создаётся и обрабатывается подзапрос:
1

{{ render(controller(’BlogBundle:Post:list’)) }}

Когда вы пишите собственный слушатель событий ядра…
Спросите себя, должен ли ваш слушатель реагировать на мастер-запрос, на подзапрос, или же на оба типа. И используйте условие для проверки, которое приведено выше.

II Приёмы внедрения зависимостей

33

II Приёмы внедрения зависимостей

34

5 Что такое бандл (bundle)
Как мы уже могли отметить в предыдущей главе, запуск Symfony-приложения означает
загрузку ядра и обработку запроса или выполнение команд. В свою очередь, загрузка ядра
означает: загрузку всех бандлов и регистрацию их расширений сервисного контейнера
(которые в любом банде расположены в директории DependencyInjection).
Обычно, расширение контейнера загружает файл services.xml (однако, формат может быть
и другим) и конфигурацию бандла, которая представлена двумя классами, как правило в
одном и том же пространстве имён - Configuration. Всё это вместе (бандл, расширение
контейнера и конфигурация) может быть использовано для того, чтобы связать бандл с
прочими частями приложения: вы можете определять параметры и сервисы, чтобы функции из вашего бандла были бы доступны также и в других частях приложения. Вы можете
двинуться ещё дальше и регистрировать дополнительные “этапы компилятора” (compiler
passes) для того, чтобы модифицировать сервисный контейнер, до того как он примет
окончательный вид.
После создания множества бандлов, я понял, что бОльшая часть моей работы, как разработчика Symfony-приложений состоит в написании кода в основном для трёх вещей:
бандлов, расширений и классов конфигурации (а также их compiler passes). Если вы уже
знаете, как писать хороший код, вам всё ещё нужно узнать, как создавать хорошие бандлы,
и это в основном означает, что вам нужно знать как создавать хорошие определения ваших
сервисов. Имеется много способов определния сервисов и далее в этой главе я расскажу
о большинстве из них. Зная все возможности, вы можете сделать правильный выбор,
применительно к конкретно взятой зитуации.
Не используйте команды генерации
Когда вы начинаете использовать Symfony, вероятно вы захотите использовать
команды, которые предоставляет SensioGeneratorBundle для создания бандлов, контроллеров, сущностей и форм. Я не рекомендую пользоваться ими. Классы, которые генерируют эти команды хорошо иметь перед глазами, как образец для создания ваших классов, но не автоматически, а вручную, так как они
содержат слишком много ненужного вам кода, или же кода, не нужного вам на
начальных этапах. Так что воспользуйтесь этими командами по одному разу,
посмотрите, как нужно действовать, а затем поймите сами, как добиться похожих целей без использования команд генерации. Понимание этих вещей быстро
сделает вас разработчиком, который хорошо понимает выбранный фреймворк.

II Приёмы внедрения зависимостей

35

6 Приёмы создания сервисов
Сервис - это объект, зарегистрированный в сервисном контейнере с некоторым идентификатором (id), который может быть создан в любой момент через посредством этого
контейнера.

6.1 Обязательные зависимости
Многие объекты для выполнения своих функций нуждаются в других объектах, скалярных
значениях (например ключ API) и может быть даже в массивах значений (скаляров или
объектов). Эти объекты, скаляры и массивы называются зависимостями. Сначала мы рассмотрим как вы можете определить обязательные зависимости для ваших сервисов.
Обязательные параметры конструктора
Самый простой способ передать сервису его зависимости - указать их в качестве аргументов
конструктора:
1
2
3

class TokenProvider
{
private $storage;

4

public function __construct(TokenStorageInterface $storage)
{
$this->storage = $storage;
}

5
6
7
8
9

}

Определение сервиса для класа TokenProvider должно выглядеть таким образом:
1
2
3





4
5
6




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

Предположим у вас есть другой провайдер токенов, ExpiringTokenProvider, который
наследуется от TokenProvider, но имеет дополнительно свой аргумент конструктора $lifetime:

II Приёмы внедрения зависимостей

1
2
3

36

class ExpiringTokenProvider extends TokenProvider
{
private $lifetime;

4

public function __construct(TokenStorageInterface $storage, $lifetime)
{
$this->lifetime = $lifetime;

5
6
7
8

parent::__construct($storage);

9

}

10
11

}

Создавая определение сервиса для второго провайдера, вы можете просто скопировать
аргумент из определения сервиса token_provider:
1
2
3
4



3600


Однако, лучшим выходом из данной ситуации будет создание определения родительского
сервиса, которое вы сможете использовать для определения дочерних сервисов, предоставляя им необходимые базовые аргументы:
1
2
3





4
5
6
7

parent=”abstract_token_provider”>


8
9
10
11
12

parent=”abstract_token_provider”>
3600


Абстрактный сервис из примера выше имеет один аргумент и отмечен как абстрактный.
Сервисы провайдеров используют abstract_token_provider в качестве родителя. Сервис token_provider не имеет дополнительных аргументов, таким образом он лишь наследует первый аргумент конструктора от abstract_token_provider. Сервис expiring_token_provider также наследует первый аргумент token_storage, но также добавляет
свой дополнительный аргумент $lifetime.
Наследование свойств
Вне зависимости от того, является ли родительский сервис абстрактным или
нет, дочерний сервис наследует нижеперечисленные свойства родительского
сервиса:
• Класс
• Аргументы конструктора (в порядке их появления)

II Приёмы внедрения зависимостей

37

• Вызовы методов после создания экземпляра класса (@dbykadorov: судя по
всему имеются в виду call вызовы при использовании setter injection)
• Свойства, используемые для property injection (недостаки этого способа внедрения параметров описаны тут)
• Фабричный класс или сервис, фабричный метод
• Конфигуратор (штука весьма экзотичная, не рассматривается в данной книге)
• Файл (необходимый для создания сервиса)
• Признак, является ли создаваемый сервис публичным
Вызов обязательных set-методов (setters)
Иногда вы можете попасть в ситуациию, когда вы не захотите (или не сможете) переопределить конструктор сервиса и, следовательно, не сможете добавить дополнительные
параметры в него, или же, возможно, некоторые зависимости еще не определены в момент
создания класса сервиса. В таких случаях вы можете добавить в ваш класс set-метод, что
позволит внедрить зависимость сразу после создания сервиса (или, фактически, в любой
момент после создания):
1
2
3

class SomeController
{
private $container;

4

public function setContainer(ContainerInterface $container)
{
$this->container = $container;
}

5
6
7
8
9

public function indexAction()
{
$service = $this->container->get(’...’);
}

10
11
12
13
14

}

Так как контроллеру из примера выше для выполнения метода indexAction() необходим
экземпляр ContainerInterface, вызов setContainer является обязательным. Поэтому
в определении сервиса для него вы должны позаботиться о внедрении сервисного котейнера:
1
2
3
4
5







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

II Приёмы внедрения зависимостей

38

примере выше это привеёт к вызову метода get() от null, что, в свою очередь, вызовет
фатальную ошибку PHP. По моему мненю, этот недостаток перекрывает все достоинства
данного подхода, так как код получается “с душком”, который называется временнАя
связность (@dbykadorov: от слова “время”, имеется в виду, что работоспособность вашего
кода зависит от того был ли перед использованием сервиса вызван нужный set-метод или
вы забыли добавить его описание сервиса) или temporal coupling. Таким образом этот
тип внедрения делает ваш класс менее надёжным (@dbykadorov: о temporal coupling в
PHP можно дополнительно почитать, например, тут).
Для того, чтобы предотвратить серьёзные сбои (и чтобы помочь разработчику, который
будет исправлять проблему, если она всё-таки возникнет) вы можете ограничить доступ
к зависимому свойству через его геттер (метод getXxx()) и в нём проверять валидность
соответствующего значения, например вот так:
1
2
3
4
5
6

class SomeController
{
public function indexAction()
{
$service = $this->getContainer()->get(’...’);
}

7

private function getContainer()
{
if (!($this->container instanceof ContainerInterface)) {
throw new \RuntimeException(’Service container is missing’);
}

8
9
10
11
12
13

return $this->container;

14

}

15
16

}

ContainerAware
Компонент Symfony DependencyInjection содержит интерфейс ContainerAwareInterface
и абстрактный класс ContainerAware, которые вы можете использовать для
того, чтобы указать, что класс “осведомлён” (“aware” в оригинале) о сервисном
контейнере. Использование этих классов предоставит вам set-метод по имени
setContainer(), с помощью которого вы сможете передать сервисный контейнер в ваш класс. Контроллеры, которые реализуют интерфейс ContainerAwareInterface,
автоматически получают доступ к контейнеру при помощи этого set-метода.
Стандартный класс Controller из состава Symfony FrameworkBundle является
“осведомлённым-о-контейнере” - container-aware. См. также главу 1, раздел
2.2 - Определение контроллера для запуска.
Вызов методов в абстрактных сервисах
Когда вы создаёте container-aware сервис, у вас будет много дублируещего кода в его
определении. Разумным будет добавить вызов метода setContainer() в определение
абстрактного сервиса:

II Приёмы внедрения зависимостей

1
2
3
4
5

39







6
7
8
9

parent=”abstract_container_aware”>


Соглашение об именовании родительских сервисов
Определения родительских сервисов не обязательно должны быть абстрактными. Однако, когда вы опускаете атрибут abstract=”true”, определение родительского сервиса будет трактоваться как определение обычного сервиса (и
соответствующим образом валидироваться).
Если же вы хотите создать определение абстрактного сервиса - пометьте его таковым, присвоив трибуту abstract значение true и добавьте префикс abstract_к его id (по аналогии с абстрактныи классами, имена которых традиционно
начинаются с Abstract).
Если же у вас есть определение родительского сервиса, который также должен
сам быть самостоятельным сервисом, добавлять атрибут abstract не нужно, но,
возможно, будет полезным добавить префикс base_ к его id.

6.2 Необязательные (опциональные) зависимости
Иногда зависимости являются не обязатальными. Термин “не обязательные зависимости”
может вам показаться противоречивым, так как если вы реально от чего-то не зависите,
странно называть это “зависиомстями”. Однако, встречаются ситуации, когда один сервис
знает как использовать другой сервис, но в общем-то этот другой сервис не необходим
для выполнения его функций. Например, сервис может знать как использовать сервис
журналирования (логгер) для того, чтобы писать в лог какие-либо отладочные данные.
Необязательные аргументы конструктора
В случае, когда класс вашего сервиса знает как работать с логгером, он может иметь
необязательный аргумент конструктора для него:
1
2

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;

3
4
5
6
7

class AuthenticationListener
{
private $eventDispatcher;
private $logger;

8
9
10
11
12
13

public function __construct(
EventDispatcherInterface $eventDispatcher,
LoggerInterface $logger = null
) {
$this->eventDispatcher = $eventDispatcher;

II Приёмы внедрения зависимостей

$this->logger = $logger;

14

}

15
16

40

}

Для аргументов конструктора, которые должны быть либо экземплярами указанных классов, либо могут отсутствовать, вы можете использовать значение по умолчанию null. В
этом случае, в определении вашего сервиса в можете выбрать стратегию поведения в случае
их отсутствия:
1
2
3





Стратегия ignore на данный момент эквивалентна null-стратегии, в том смысле, что
конструктор будет вызван со значением null, вместо запрошенного сервиса, если тот недоступен. Есть еще стратегия exception, которая применяется по умолчанию. При её использовании будет вызвано исключение, если внедряемый сервис не будет найден.
Проверка необязательных зависимостей
Если вы хотите проверить, внедрена или нет необязательная зависимость, вы
должны написать примерно такой код:
1
2
3

if ($this->logger instanceof LoggerInterface) {
...
}

Это более надёжный способ проверки, чем сравнение с NULL:
1
2
3

if ($this->logger !== null) {
...
}

Думайте об этом в таком ключе: если что-то не является NULL, можем ли мы быть
уверены, что это logger?
Не обязательные вызовы set-методов
Также, как и в случае с обязательными зависимостями, иногда бывает удобно/необходимо
внедрить необязательные зависимости при помощи set-методов. Как правило, эта потребность возникает, если вы не хотите захлямлять сигнатуру конструктора:

II Приёмы внедрения зависимостей

1
2
3
4

41

class AuthenticationListener
{
private $eventDispatcher;
private $logger;

5

public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}

6
7
8
9
10

public function setLogger(LoggerInterface $logger = null)
{
$this->logger = $logger;
}

11
12
13
14
15

}

В определении сервиса вы можете добавить вызов метода setLogger() с сервисом журналирования в качестве аргумента. Вы также можете указать, что этот аргумент должен быть
проигнорирован, в случае, если соответствующий сервис не будет найден (что делает эту
зависимость по-настоящему необязательной):
1
2
3
4
5







Аргумент вызова метода setLogger() может иметь значение NULL, (когда сервис не
определён), но метод будет вызван в любом случае, таким образом вы должны иметь в виду,
что NULL является одним из валидных аргументов вызова метода setLogger().
Определение закрытых (non-public) зависимостей
Когда вы пишете код в тру-ООП стиле, у вас всегда всегда будет куча небольших
сервисов, каждый из которых выполняет только одну фиксированную функцию.
Сервисы более высокого уровня будут внедрять их в качестве зависимостей. Сервисы более низких уровней как правило не должны пересекаться между собой;
они лишь выполняют роль помошников для сервисов более высокого уровня.
Чтобы не допустить возможности использования низкоуровневых сервисов в
других частях приложения, например так:
1

$container->get(’low_level_service_id’);

вы должны пометить их как закрытые (non-public), для этого в опредении низкоуровневого сервиса нужно присвоить атрибуту public значение “false”:
1
2




6.3 Коллекции сервисов
В большинстве случаев вы будете внедрять зависимости через конструктор или аргументы
set-методов. Но иногда возникает необходимость внедрить в качестве зависиомсти целую
коллекцию сервисов, например, если вы хотите предоставить несколько альтернатив (стратегий) для достижения некоторых целей:

II Приёмы внедрения зависимостей

1
2
3

42

class ObjectRenderer
{
private $renderers;

4

public function __construct(array $renderers)
{
$this->renderers = $renderers;
}

5
6
7
8
9

public function render($object)
{
foreach ($this->renderers as $renderer) {
if ($renderer->supports($object) {
return $renderer->render($object);
}
}
}

10
11
12
13
14
15
16
17
18

}

Определение такого сервиса может выглядеть следующим образом:
1
2
3
4
5
6








Аргумент типа collection будет преобразован в массив, содержащий сервисы, id которых
перечислены в этой коллекции:
1
2
3
4

array(
0 => ...
1 => ...
)

Вы также можете для каждого элемента коллекции указать ключ при помощи атрибута key:
1
2
3
4
5
6
7
8
9
10



key=”domain_object” type=”service”
id=”domain_object_renderer” />
key=”user” type=”service”
id=”user_renderer” />



Значение атрибута key будет использовано в качестве ключа для соответствующих значений коллекции:

II Приёмы внедрения зависимостей

1
2
3
4

43

array(
’domain_object’ => ...
’user’ => ...
)

Вызов нескольких методов
Если вы включите “строгий” режим и перечитаете код класса ObjectRenderer, вы вероятно заметите, что нельзя доверять массиву $renderers в том, что он содержит только валидные рендереры (которые, к примеру, должны реализовывать интерфейс RendererInterface).
Следовательно, вы вероятно захотите выделить отдельный метод для добавления рендерера:
1
2
3

class ObjectRenderer
{
private $renderers;

4

public function __construct()
{
$this->renderers = array();
}

5
6
7
8
9

public function addRenderer($name, RendererInterface $renderer)
{
$this->renderers[$name] = $renderer;
}

10
11
12
13
14

}

Конечно же, если имя рендерера не имеет значения, можно убрать параметр $name. Важно
тут другое: когда кто-либо вызовет метод addRenderer и передаст ему в качестве аргумента объект, который не является реализацией интерфейса RendererInterface, этот вызов
не будет успешным, так как не будет соблюдено ограничение по типу аргумента. Определение сервиса также необходимо изменить, чтобы для каждого рендерера вызывался бы
метод addRenderer():
1
2
3
4
5
6
7
8
9
10



domain_object



user




Лучшее из двух миров
Возможно вас также заинтересует идея о том, как объединить походы для работы с коллекциями сервисов, описанные выше, что позволило бы разработчикам передавать как набор
рендереров по-умолчанию через аргумент конструктора и/или добавлять рендереры один
за другим, используя метод addRenderer():

II Приёмы внедрения зависимостей

1
2
3

44

class ObjectRenderer
{
private $renderers;

4

public function __construct(array $renderers)
{
foreach ($renderers as $name => $renderer) {
$this->addRenderer($name, $renderer);
}
}

5
6
7
8
9
10
11

public function addRenderer($name, RendererInterface $renderer)
{
$this->renderers[$name] = $renderer;
}

12
13
14
15
16

}

Метки сервисов (tags)
Мы уже умеем добавлять рендереры вручную, но что, если другие части вашего приложения
(например другие бандлы) должны иметь возможность регистрировать свои рендереры?
Наилучшим способом достичь этого будет использование меток (тагов) для сервисов:
1
2
3
4






У каждого тага есть имя, которое вы можете выбрать самостоятельно. Каждый таг также
может иметь дополнительные атрибуты (например alias в примере выше). Эти атрибуты
в дальнейшем позволят вам определять дальнейшее повение сервиса.
Для того, чтобы получить список всех сервисов с нужным тагом, вам нужно создать т.н.
compiler pass, например такой:
1

namespace Matthias\RendererBundle\DependencyInjection\Compiler;

2
3
4
5

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

6
7
8
9
10
11
12
13

class RenderersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
// получаем все сервисы, отмеченные определённым тагом из всего проекта
$taggedServiceIds
= $container ->findTaggedServiceIds(’specific_renderer’);

14
15
16

$objectRendererDefinition
= $container->getDefinition(’object_renderer’);

17
18

foreach ($taggedServiceIds as $serviceId => $tags) {

19
20
21

// сервисы могут иметь более одного тага с одни и тем же именем
foreach ($tags as $tagAttributes) {

22
23

// вызываем метод addRenderer() для того, чтобы зарегистрировать рендерер

II Приёмы внедрения зависимостей

$objectRendererDefinition
->addMethodCall(’addRenderer’, array(
$tagAttributes[’alias’],
new Reference($serviceId),
));

24
25
26
27
28

}

29

}

30

}

31
32

45

}

Созданный выше класс compiler pass нужно зарегистрировать в классе вашего бандла:
1
2

use Matthias\RendererBundle\DependencyInjection\Compiler\RenderersPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;

3
4
5
6
7
8
9
10

class RendererBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new RenderersPass());
}
}

Метод process() класса RenderersPass в первую очередь получает все сервисы с тегами,
которые имеют имя specific_renderer. В результате будет получен массив, ключами
котророго будут id сервисов, а значениями - массив их атрибутов. Так сделано потому, что
определние любого сервиса может иметь более одного тега с одним и тем же именем (но,
возможно, с другими атрибутами).
Потом, запрашивается определение сервиса object_renderer для класса ObjectRenderer,
после чего выполняется цикл по всем найденным тагам. В каждой итерации создаётся
экземпляр класса Reference, который ссылается на текущий (в данной итерации) сервис
рендерера (который, в свою очередь, помечен тагом specific_renderer) и вместе с
значением атрибута alias они используются в качестве аргументов для вызова метода
addRenderer().
Всё это вместе означает что когда сервис object_renderer будет запрошен, сначала будет
создан экземпляр класса ObjectRenderer. Но затем будет выполнено несколько вызовов метода addRenderer(), которые добавят рендереры, отмеченные тагом specific_renderer.
Вызов одного метода
Есть много возможностей обработки сервисов в compiler pass. Например, вы можете
собрать ссылки (references) на сервисы в массив и обработать их все разом, указав их в
качестве аргумента метода setRenderers():

II Приёмы внедрения зависимостей

1
2
3
4
5

46

class RenderersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$taggedServiceIds = ...;

6

$objectRendererDefinition = ...;

7
8

$renderers = array();

9
10

foreach ($taggedServiceIds as $serviceId => $tags) {
foreach ($tags as $tagAttributes) {
$name = $tagAttributes[’alias’];
$renderer = new Reference($serviceId);
$renderers[$name] = $renderer;
}
}

11
12
13
14
15
16
17
18

$objectRendererDefinition
->addMethodCall(’setRenderers’, array($renderers));

19
20

}

21
22

}

Замена аргумента конструктора
Если имеется возможность внедрения коллекции сервисов в виде конструктора - например
рендереры из примера выше - есть также другой способ сделать это: установить аргумент
конструктора напрямую:
1
2
3
4
5

class RenderersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$taggedServiceIds = ...;

6

$objectRendererDefinition = ...;

7
8

$renderers = array();

9
10

// получаем референсы на сервисы
...

11
12
13

$objectRendererDefinition->replaceArgument(0, $renderers);

14

}

15
16

}

Замена аргумента может быть выполнена только в случае, если этот аргумент определён
первым (например как пустой аргумент):
@dbykadorov: TODO - перепроверить, либо неправильно понял, либо это не всегда так
1
2
3





Передаём ID сервисов вместо референсов
Когда вы запрашиваете сервис object_renderer, все рендереры, переданные ему в качестве аргументов также будут созданы. В зависимости от цены (@dbykadorov: в плане

II Приёмы внедрения зависимостей

47

производительности) создания этих рендереров возможо было бы неплохо предусмотреть возможность их “ленивой” загрузки - lazy-loading. Этого можно добиться сделав
ObjectRenderer “осведомлённым о контейнере” - container-aware и внедряя id сервисов, вместо них самих:
1
2
3
4

class LazyLoadingObjectRenderer
{
private $container;
private $renderers;

5

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

6
7
8
9
10

public function addRenderer($name, $renderer)
{
$this->renderers[$name] = $renderer;
}

11
12
13
14
15

public function render($object)
{
foreach ($this->renderers as $name => $renderer) {
if (is_string($renderer)) {
// здесь $renderer - это id сервиса, строка
$renderer = $this->container->get($renderer);
}

16
17
18
19
20
21
22
23

// проверяем, является ли renderer экземпляром RendererInterface
...

24
25

}

26

}

27
28

}

Также compiler pass нужно модифицировать таким образом, чтобы он не передавал
ссылки на services, а только лишь их id:
1
2
3
4
5

class RenderersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$taggedServiceIds = ...;

6

$objectRendererDefinition = ...;

7
8

foreach ($taggedServiceIds as $serviceId => $tags) {
foreach ($tags as $tagAttributes) {
$objectRendererDefinition
->addMethodCall(’addRenderer’, array(
$tagAttributes[’alias’],
$serviceId,
)
);
}
}

9
10
11
12
13
14
15
16
17
18

}

19
20

}

Также не забудьте указать сервисный контейнер в качестве аргумента конструктора:

II Приёмы внедрения зависимостей

1
2
3

48





И конечно же любая из стратегий, описанных выше, может быть использована с этим классом, реализующим “ленивую” загрузку - вызов одного метода, вызов нескольких методов,
замена аргумента.
Перед тем, как вы решите изменить свой класс для использования сервисного контейнера напрямую, пожалуйста ознакомьтесь с разедами TODO: Уменьшаем связность кода с
фреймворком, особенно TODO: заметку быстродействии.

6.4 Делегирование создания
Вместо того, чтобы создавать сервисы заранее при помощи их определений с указанием
класса, аргументов конструктора и вызовов методов, вы можете не указывать все эти
детали, делегровав их заполнение фабричному методу (factory method) во время выполнения. Фабричные методы могут быть как статичными методами, так и методами объектов.
В первом случае вы можете указать имя класса и имя метода в качестве атрибутов при
определении сервиса:
1
2
3
4

factory-class=”Some\Factory” factory-method=”create”>
...


Когда сервис some_service будет запрошен в первый раз, он будет получен как результат вызова статического метода Some\Factory::create() с использованием указанных
аргументов. Результат будет сохранён в памяти, поэтому фабричный метод будет вызван
лишь раз (@dbykadorov: один раз в рамках текущего запроса, кэшироваться на диск такой
вызов не будет). В наши дни большинство фабричных методов не являются статическими,
что онзначает вызов фабричного метода у экземпляра фабричного класса. Следовательно,
этот экземпляр фабрики должен быть предварительно сам определён как сервис:
1
2
3
4
5


factory-service=”some_factory_service” factory-method=”create”>
...


6
7
8
9





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

II Приёмы внедрения зависимостей

49

только для случаев, когда сервис создаётся для PHP-классов из (не очень далёкого) прошлого, логика создания экземпляров которых часто спрятана внутри статических фабричных
классов(помните Doctrine_Core::getTable()?).
Мои возражения против фабричных классов со статическими фабричными методами основаны на том, что статический код - это глобальный код и выполнение этого кода может
иметь побочные эффекты, которые нельзя изолировать (например в тестовом сценарии).
Кроме того, любая зависимость такого статического фабричного метода по определнию
также должна быть статической, что также очень плохо для изоляции и не даст вам возможности подменить (часть) логики создания своим кодом.
Фабрики-объекты (или фабричные сервисы) лишь немногим лучше. Тем не менее, необходимость их использования скорее всего указывает не проблемы в дизайне (архитектуре)
приложения. Сервис не должен нуждаться в фабрике так как он создаётся один раз заранее определённым (и детерминированным (@dbykadorov - т.е. при одинаковых входных
данных получается одинаковый же результат)) способом и с момента создания он полностью готов к повторному использользованию любым другим объектом. Переменными по
отношению к сервису должны быть лишь аргументы методов, которые являются частью его
публичного интерфейса (см. также TODO: Состояние и Контекст).
Иногда всё-таки полезно…
Одним из частных случаев, когда использование фабричных сервиса и метода для получения сервиса оправдано - является случай с репозиториями Doctrine. Когда вам нужен
один из них, вы как правило можете внедрить entity manager в качестве аргумента
конструктора и позднее получить нужный репозиторий:
1

use Doctrine\ORM\EntityManager;

2
3
4
5
6
7
8

class SomeClass
{
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}

9

public function doSomething()
{
$repository = $this->entityManager->getRepository(’User’);

10
11
12
13

...

14

}

15
16

}

Но, используя фабричный сервис вы можете внедрить нужный репозиторий напрямую:

II Приёмы внедрения зависимостей

1
2
3
4
5
6
7

50

class SomeClass
{
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
}

Вот соответствующие определения сервисов для этого случая:
1
2
3





4
5
6
7
8

factory-service=”entity_manager” factory-method=”getRepository”>
User


Если взглянуть на аргументы конструктора SomeClass сразу становится ясно, что на ходе
ожидается репозиторий User, что намного более читабельно, нежели предыдущий пример,
где SomeClass ожидает EntityManager. Кроме того, класс сам по себе стал чище, а также
стало проще создать замену для репозитория, например, когда вы будете писать модульный
тест для этого класса. Вместо того, чтобы создать mock-объекты для менеджера сущности и
репозитория, теперь можно создать только один - для репозитория.

6.5 Создание сервисов вручную
Обычно вы создаёте сервисы, загружая их определения из файла:
1
2
3

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

4
5
6
7
8
9
10
11
12
13

class SomeBundleExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$locator = new FileLocator(__DIR__.’/../Resources/config’);
$loader = new XmlFileLoader($container, $locator);
$loader->load(’services.xml’);
}
}

Но некоторые сервисы не могут быть определены в конфигурационном файле. Они должны
создаваться динамически, потому что их имена, классы, аргументы, таги и прочие атрибуты
не фиксированы.
Определение
Создание сервиса вручную означает создание экземпляра класса “определения” Definition,
и (опционально) передача ему имени класса. Определение получит идентификатор при его
добавлении в ContainerBuilder:

II Приёмы внедрения зависимостей

1

51

use Symfony\Component\DependencyInjection\Definition;

2
3

$class = ...; // присваиваем значение имени класса для определения

4
5

$definition = new Definition($class);

6
7

$container->setDefinition(’the_service_id’, $definition);

Эквивалентом этого определения был бы такой XML:
1
2




Вы можете сделать определение закрытым (non-public), если оно существует лишь как
зависимость для других сервисов:
1

$definition->setPublic(false);

Аргументы
Когда для создания сервиса нужно передать в конструктор аргументы, вы можете задать их
все разом:
1

use Symfony\Component\DependencyInjection\Reference;

2
3
4
5
6
7
8
9
10

$definition->setArguments(array(
new Reference(’logger’) // ссылка на другой сервис
true // логическое (булево) значение,
array(
’table_name’ => ’users’
) // массив
...
));

Аргументы должны быть ссылками на другие сервисы, массивами или скалярами (или их
сочетаниями). Это требование - следствие того, что все определения сервисов в конечном
итоге будут сохранены в простом PHP файле. Ссылка на другой сервис будет создана с использованиме объекта Reference с указанием ID сервиса, который должен быть внедрён.
Вы также можете добавлять аргументы по одному, в том порядке, в котором они перечислены в конструкторе:
1
2
3

$definition->addArgument(new Reference(’logger’));
$definition->addArgument(true);
...

И, наконец, если вы модифицируете определение уже существующего сервиса с некоторым
списком аргументов, вы можете заменить их, используя численные индексы:

II Приёмы внедрения зависимостей

1

52

$definition->setArguments(array(null, null));

2
3

...

4
5
6

$definition->replaceArgument(0, new Reference(’logger’));
$definition->replaceArgument(1, true);

Соответствующий XML выглядел бы так:
1
2
3
4



true


Таги
Есть еще кое-что, что вы можете сделать, работая с объектом Definition: добавить таги
(метки). Таг состоит из его имени и массива атрибутов. Определение может иметь более
одного тага с одним именем:
1
2
3
4
5
6

$definition->addTag(’kernel.event_listener’, array(
’event’ => ’kernel.request’
);
$definition->addTag(’kernel.event_listener’, array(
’event’ => ’kernel.response’
);

XML определение в этом случае было бы таким:
1
2
3
4






Алиасы (псевдонимы)
Перед тем, как поговорить о том, что вам делать с вашими новыми знаниями, есть еще один
момент, который вам надо знать: как создавать алиасы для сервисов:
1

$container->setAlias(’some_alias’, ’some_service_id’);

Теперь, когда бы вы ни запросили сервис some_alias, фактически вы получите сервис
some_- service_id.

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

II Приёмы внедрения зависимостей

53

Большую часть времени вы будете использовать класс Configuration для того, чтобы
определять все возможные конфигурационные опции для вашего бандла (обратите внимание, компонент Config имеет слабые связи и вы можете также использовать всё сказанное
ниже в совершенно другом контексте). Имя класса и его пространство имён не имеют
особого значения, пока класс реализует интерфейс ConfigurationInterface:
1
2

use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;

3
4
5
6
7
8
9

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root(’name_of_bundle’);

10

$rootNode
->children()
// определяем узлы конфигурации
...
->end()
;

11
12
13
14
15
16
17

return $treeBuilder;

18

}

19
20

}

Тут у нас один публичный метод - getConfigTreeBuilder(). Этот метод должен возвращать экземпляр TreeBuilder, который вы должны использовать для того, чтобы описать все возможные настройки вашего приложения, а также правила для их проверки
на корректность (правила валидации). Создание конфигурационного дерева начинается к
определения корневого узла:
1

1 $rootNode = $treeBuilder->root(’name_of_bundle’);

Имя корневого узла должно быть именем бандла без суффикса “bundle”, в нижнем регистре
и с разделителем в виде подчерка. Например, имя корневого узла для MatthiasAccountBundle
будет matthias_account. Корневой узел - это всегда узел типа “массив”. Он может содержать любой дочерний узел, какой вы захотите:
1
2
3
4
5
6
7
8
9
10

$rootNode
->children()
->booleanNode(’auto_connect’)
->defaultTrue()
->end()
->scalarNode(’default_connection’)
->defaultValue(’default’)
->end()
->end()
;

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

II Приёмы внедрения зависимостей

54

этом случае будет значительно лучше и гибче. Подробнее об узлах конфигурации
вы можете узнать в документации компонента Config. Также обратите внимание
на классы конфигурации сторонних бандров (@dbykadorov: например - Friends
Of Symfony, FOS) и попробуйте последовать их примеру.
Как правило, вы будете использовать экземпляр класса Configuration в классе расширения
вашего бандла, для того, чтобы обработать заданный набор конфигурационных массивов.
Эти конфигурационные массивы собираются ядром, загружая все подходящие конфигурационные файлы (например, config_dev.yml, config.yml, parameters.yml, и т.д.).
1
2
3
4
5
6
7
8
9
10

class MatthiasAccountExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$processedConfig = $this->processConfiguration(
new Configuration(),
$configs
);
}
}

Метод processConfiguration() класса расширения создаёт экземпляр класса Processor
и финализирует конфигурационное дерево, загруженное из объекта Configuration. Затем он
просит processor обработать (выполнить валидацию и объединение) “сырых” конфигурационных массивов:
1
2
3
4
5

final protected function processConfiguration(
ConfigurationInterface $configuration,
array $configs
) {
$processor = new Processor();

6

return $processor->processConfiguration($configuration, $configs);

7
8

}

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

6.7 Динамическое добавление тагов
Допустим, вы хотите создать универсальный слушатель (event listener), который слушает
настраиваемый список событий, например kernel.request, kernel.response, и т.д. Вот
как мог бы выглядеть соответствующий класс Configuration:

II Приёмы внедрения зависимостей

1

55

use Symfony\Component\Config\Definition\ConfigurationInterface;

2
3
4
5
6
7
8

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root(’generic_listener’);

9

$rootNode
->children()
->arrayNode(’events’)
->prototype(’scalar’)
->end()
->end()
->end()
;

10
11
12
13
14
15
16
17
18

return $treeBuilder;

19

}

20
21

}

Он позволяет сконфигурировать список имён событий следующим образом:
1
2

generic_listener:
events: [kernel.request, kernel.response, ...]

Стандартный способ зарегистрировать слушатель заключается в добавлении тагов к сервису
слушателя в файле services.xml:
1
2
3
4






Но в нашем случае мы не знаем, какие события слушатель должен слушать, так что мы не
можем указать их явно в файле конфигурации. К счастью, как мы уже знаем, мы можем
добавлять таги к определению сервиса прямо на лету. Это можно провернуть в классе
расширения контейнера:
1
2
3
4
5
6
7
8

class GenericListenerExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$processedConfig = $this->processConfiguration(
new Configuration(),
$configs
);

9
10
11
12

// загружаем services.xml
$loader = ...;
$loader->load(’services.xml’);

13
14
15

$eventListener = $container
->getDefinition(’generic_event_listener’);

16
17

foreach ($processedConfig[’events’] as $eventName) {

II Приёмы внедрения зависимостей

// добавляем таги kernel.event_listener для каждого из указанных событий
$eventListener
->addTag(’kernel.event_listener’, array(
’event’ => $eventName,
’method’ => ’onEvent’
));

18
19
20
21
22
23

}

24

}

25
26

56

}

Есть ещё один шаг, который нужно выполнить, чтобы предотвратить “подвисание” сервиса
слушателя если ни одного события для него не сконфигурировано:
1
2
3

if (empty($processedConfig[’events’])) {
$container->removeDefinition(’generic_event_listener’);
}

6.8 Используем паттерн Стратегия для загрузки сервисов
Чсто бандлы предлагают разные способы выполнить то иную функцию. Например, бандл
предоставляющий функцию почтового ящика в каком-то виде, может иметь разные реализации хранилища, например, один менеджер хранения для Doctrine ORM, а другой
для MongoDB. Чтобы предоставить возможность выбора конкретного менеджер хранения,
давайте создадим класс конфигурации:
1

use Symfony\Component\Config\Definition\ConfigurationInterface;

2
3
4
5
6
7
8

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root(’browser’);

9

$rootNode
->children()
->scalarNode(’storage_manager’)
->validate()
->ifNotInArray(array(’doctrine_orm’, ’mongo_db’)
->thenInvalid(’Invalid storage manager’)
->end()
->end()
->end()
;

10
11
12
13
14
15
16
17
18
19
20

return $treeBuilder;

21

}

22
23

}

Затем нужно создать файлы с определениями сервисов для каждого их менеджеров хранения, один - doctrine_orm.xml:

II Приёмы внедрения зависимостей

1
2
3
4

57






И другой - mongo_db.xml:
1
2
3
4






Затем вы должны загрузить один из этих файлов при помощи вот такого кода в классе
расширения:
1
2
3
4
5
6
7
8

class MailboxExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$processedConfig = $this->processConfiguration(
new Configuration(),
$configs
);

9

// создаём XmlLoader
$loader = ...;

10
11
12

// загружаем только сервисы для выбранного менджера хранения
$storageManager = $processedConfig[’storage_manager’];
$loader->load($storageManager.’.xml’);

13
14
15
16

// делаем выбранный менеджер дооступным по умолчанию
$container->setAlias(
’mailbox.storage_manager’,
’mailbox.’.$storageManager.’.storage_manager’
);

17
18
19
20
21

}

22
23

}

В конце мы создаём удобный алиас, для того чтобы другие части приложения могли обращаться к сервису mailbox.storage_manager, не заботясь о том, какая именно схема хранения на самом деле используется. Тем не мене, этот способ не очень гибок: id каждого менеджера хранения должен соответствовать шаблонуmailbox.{storageManagerName}.storage_manager. Будет лучше определить алиас внутри файла с определением сервисов:

II Приёмы внедрения зависимостей

1
2
3

58





4
5
6
7
8

alias=”mailbox.doctrine_orm.storage_manager”>



Используя паттерн “стратегия” для загрузки сервисов, мы получаем следующие преимущества:
• Загружаются только те сервисы, которые реально будут использованы в данном конкретном приложении. Если у вас нет сервера MongoDB, то у вас не будет и сервисов,
которые от него зависят.
• Такая конфигурация открыта для расширения, так как вы можете добавить имя другого менеджера хранения в список в классе Configuration и затем добавить определения
сервисов и алиасов - всё готово.

6.9 Загрузка и конфигурирование дополнительных сервисов
Положим у вас есть бандл, который должен заниматься фильтрацией входных данных.
Вероятно вы предоставляете несколько различных сервисов, например сервисы для фильтрации данных html-форм, а также сервисы для фильтрации данных, сохранённых при
помощи Doctrine ORM. Соответственно должна быть возможность в любое время активировать или деактивировать любой из этих сервисов или целую коллекцию сервисов, так как
могут быть не применимы к вашей конкретной ситуации. Имеется удобный способ для того
чтобы при конфигурировании добиться такой возможности:
1
2
3
4
5
6

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root(’input_filter’);

7

$rootNode
->children()
->arrayNode(’form_integration’)
// будет активно по умолчанию
->canBeDisabled()
->end()
->arrayNode(’doctrine_orm_integration’)
// будет отключено по умолчанию
->canBeEnabled()
->end()
->end()
;

8
9
10
11
12
13
14
15
16
17
18
19
20

return $treeBuilder;

21

}

22
23

}

Имея такое дерево конфигурации, вы можете активировать или деактивировать отдельные
части бандла в config.yml:

II Приёмы внедрения зависимостей

1
2
3
4
5

59

input_filter:
form_integration:
enabled: false
doctrine_orm_integration:
enabled: true

В классе расширения вам остаётся только загрузить соответствующие сервисы:
1
2
3
4
5
6
7
8

class InputFilterExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$processedConfig = $this->processConfiguration(
new Configuration(),
$configs
);

9

if ($processedConfig[’doctrine_orm_integration’][’enabled’]) {
$this->loadDoctrineORMIntegration(
$container,
$processedConfig[’doctrine_orm_integration’]
);
}

10
11
12
13
14
15
16

if ($processedConfig[’form_integration’][’enabled’]) {
$this->loadFormIntegration(
$container,
$processedConfig[’form_integration’]
);
}

17
18
19
20
21
22
23

...

24

}

25
26

private function loadDoctrineORMIntegration(
ContainerBuilder $container,
array $configuration
) {
// загружаем сервисы и т.д.
...
}

27
28
29
30
31
32
33
34

private function loadFormIntegration(
ContainerBuilder $container,
array $configuration
) {
...
}

35
36
37
38
39
40
41

}

Каждая из отдельных частей бандла теперь может быть загружена независимо от других.
Подчищаем класс конфигурации
Одна или две части бандла можно легко поддерживать таким образом, как это описано
выше, но если развивать ваш бандл таким образом, вскоре класс Configuration будет содержать слишком много строк кода для одного метода. Можно слегка подчистить этот код,
задействовав метод append() в комбинации с несколькими приватными методами:

II Приёмы внедрения зависимостей

1
2
3
4
5

60

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();

6

$rootNode = $treeBuilder->root(’input_filter’);

7
8

$rootNode
->append($this->createFormIntegrationNode())
->append($this->createDoctrineORMIntegrationNode())
;

9
10
11
12
13

return $treeBuilder;

14

}

15
16

private function createDoctrineORMIntegrationNode()
{
$builder = new TreeBuilder();

17
18
19
20

$node = $builder->root(’doctrine_orm_integration’);

21
22

$node
->canBeEnabled()
->children()
// тут можно добавить дополнительные опции конфигурации
...
->end();

23
24
25
26
27
28
29

return $node;

30

}

31
32

private function createFormIntegrationNode()
{
...
}

33
34
35
36
37

}

6.10 Настраиваем какой сервис использовать
Вместо того, чтобы использовать паттерн “стратегия” для загрузки сервисов, вы также
можете разрешить разработчикам конфигурировать вручную сервисы, которые они хотят использовать. Например, если вашему бандлу нужен какой-либо сервис-шифратор
(encrypter) и бандл не содержит таковой, вы можете попросить разработчика указать ID
сервиса-шифратора:
1
2

matthias_security:
encrypter_service: my_encrypter_service_id

Класс конфигурации в таком случае будет выглядеть таким образом:

II Приёмы внедрения зависимостей

1
2
3
4
5
6

61

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root(’matthias_security’);

7

$rootNode
->children()
->scalarNode(’encrypter_service’)
->isRequired()
->end()
->end()
;

8
9
10
11
12
13
14
15

return $treeBuilder;

16

}

17
18

}

В классе расширения бандла, вам нужно создать алиас для сконфигурированного сервиса.
1
2
3
4
5
6
7
8

class MatthiasSecurityExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$processedConfig = $this->processConfiguration(
new Configuration(),
$configs
);

9

$container->setAlias(
’matthias_security.encrypter’,
$processedConfig[’encrypter_service’]
);

10
11
12
13

}

14
15

}

Таким образом, даже учитывая что id сервиса-шифратора может быть любым, теперь вы
всегда будете точно знать, как обратиться к нему из любого сервиса по известному вам и
вашему бадлу псевдониму:
1
2
3





Конечно же вы не можете быть уверены в том, что сконфигурированный вручную сервис
- это действительно валидный экземпляр шифратора. Вы не можете проверить это на
этапе конфигурации, поэтому придётся проверять это во время выполнения. Типичный
способ выполнить такую проверку - добавить подсказку типа (type-hint) в классы сервисов,
которые используют этот сервис-шифратор:

II Приёмы внедрения зависимостей

1
2
3
4
5
6
7

62

class EncryptedDataManager
{
public function __construct(EncrypterInterface $encrypter)
{
// здесь мы можем быть уверены, что $encrypter валидный
}
}

6.11 Полностью динамическое определение сервисов
Также встречаются ситуации, когда заранее вы практически ничего не знаете о сервисе,
который вам нужен, до того момента, когда у вас есть обработанная конфигурация. Положим, вы хотите, чтобы пользователи вашего бандла могли бы определять сервисы в виде
набора ресурсов. Эти ресурсы могут иметь тип файл или директория. Вы хотите создавать
эти сервисы на лету, так как они могут отличаться от приложения к приложению и вам
необходимо собирать их, используя особый таг - resource. Ваш класс Configuration для
данного случая может выглядеть примерно так:
1
2
3
4
5
6

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root(’resource_management’);

7

$rootNode
->children()
->arrayNode(’resources’)
->prototype(’array’)
->children()
->scalarNode(’type’)
->validate()
->ifNotInArray(
array(’directory’, ’file’)
)
->thenInvalid(’Invalid type’)
->end()
->end()
->scalarNode(’path’)
->end()
->end()
->end()
->end()
->end()
;

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

return $treeBuilder;

29

}

30
31

}

Пример конфигурации ресурсов:

II Приёмы внедрения зависимостей

1
2
3
4
5
6
7
8

63

resource_management:
resources:
global_templates:
type: directory
path: Resources/views
app_kernel:
type: file
path: AppKernel.php

Когда ресурсы определены таким образом, вы можете создавать определения сервисов для
них в расширении контейнера:
1
2
3
4
5
6
7
8

class ResourceManagementExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$processedConfig = $this->processConfiguration(
new Configuration(),
$configs
);

9

$resources = $processedConfig[’resources’];

10
11

foreach ($resources as $name => $resource) {
$this->addResourceDefinition($container, $name, $resource);
}

12
13
14

}

15
16

private function addResourceDefinition(
ContainerBuilder $container,
$name,
array $resource
) {
// определяем класс
$class = $this->getResourceClass($resource[’type’]);

17
18
19
20
21
22
23
24

$definition = new Definition($class);

25
26

// добавляем таг
$definition->addTag(’resource’);

27
28
29

$serviceId = ’resource.’.$name;

30
31

$container->setDefinition($serviceId, $definition);

32

}

33
34

private function getResourceClass($type)
{
if ($type === ’directory’) {
return ’Resource\Directory’;
} elseif ($type === ’file’) {
return ’Resource\File’;
}

35
36
37
38
39
40
41
42

throw new \InvalidArgumentException(’Type not supported’);

43

}

44
45

}

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

II Приёмы внедрения зависимостей

64

7 Приёмы создания параметров
Сервисный контейнер помимо сервисов содержит также и параметры. Параметры - это простые значения, которые могуть быть представлены в виду констант-скаляров или массивов
в любых сочетаниях. Таким образом, валидными параметрами будут: строка ‘matthias’, число 23, а также массив array(23 => ’matthias’), array(23 => array(’matthias’)),
и т.д.
Вы можете определять параметры с любым ключом, какой пожелаете. Тем не менее, желательно придерживаться следующего формата: name_of_your_bundle_without_bundle.parameter_name. Параметры можно определить в разных местах приложения.

7.1 Файл parameters.yml
Некоторые базовые параметры вашего приложения (у которых, вероятно нет значения поумолчанию) можно обнаружить в файле /app/config/parameters.yml. Параметры загружаются вместе с определениями сервисов и конфигурациями расширений контейнера.
По этой причине стандартный файл конфигурации config.yml начинается с таких строк:
1
2
3

imports:
- { resource: parameters.yml }
- { resource: security.yml }

4
5
6
7

framework:
secret: %secret%
...

Сначала импортируются файлы parameters.yml и security.yml. Файл parameters.yml
как правило начинается так:
1
2
3

parameters:
database_driver: pdo_mysql
...

А security.yml, в свою очередь, как правило начинается так:
1
2
3
4

security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
...

Эти файлы будут импортированы в том порядке, в котором они указаны. Таким образом,
config.yml мог бы выглядеть так:

II Приёмы внедрения зависимостей

1
2
3
4
5
6

65

parameters:
...
security:
...
framework:
...

Так как все конфигурационные массивы будут в конце концов объединены (merged), то в
config.yml можно переопределить любой из параметров, указанных в parameters.yml:
1
2
3

parameters:
... # загружено из parameters.yml
database_driver: pdo_sqlite

В файле config.yml можно даже создавать определения сервисов (их также можно создавать в любом конфигурационном файле, отвечающем за конфигурацию сервисного
контейнера):
1
2
3
4
5

parameters:
...
services:
some_service_id:
class: SomeClass

7.2 Определение и загрузка параметров
Значения, определённые в файлах config.yml и parameters.yml, а также в определениях
сервисов и их вргументоа могут содержать т.н. заполнители (или места подстановки aka
placeholders) для значений, которые могу быть определны в виде параметров. Когда сервисный контейнер компилируется, значения, содержащие заполнители также обрабатываются
и принимают свои окончательные значения путём замены заполнителей на соответствующие параметры. Например, в прмере выше мы определили параметр database_driver
в parameters.yml. В файле config.yml мы можем сослаться на этот параметр используя
заполнитель %database_driver%:
1
2
3

doctrine:
dbal:
driver: %database_driver%

При создании опредений сервисов, бандлы Symfony обычно применяют этот приём для
указания имени класса сервиса:

II Приёмы внедрения зависимостей

1
2
3
4
5

66



Symfony\Component\Form\FormFactory



6
7
8




Параметры для имени класса
Использование параметров для имён классов даёт возможность другим частям приложения
переопределять эти параметры, вместо того, чтобы напрямую манипулировать определениями сервисов. Если представить, что у всех сервисов классы будут определены через
параметры, то эти параметры в конце концов попали бы в контейнер и вы налету могли бы
выполнять вызов типа $container->getParameter(’form.factory.class’), чтобы
получить имя класса фабрики форм (но, вероятно, этого никогда не случится). Я считаю
этот подход избыточным и не рекомендую его использовать при создании ваших сервисов.
Когда вы захотите изменить определение сервиса - вы должны это делатьв соответствующем compiler pass:
1
2

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;

3
4
5
6
7
8

class ReplaceClassCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$myCustomFormFactoryClass = ...;

9

$container
->getDefinition(’form.factory’)
->setClass($myCustomFormFactoryClass);

10
11
12

}

13
14

}

Сборка значений параметров вручную
Когда вы используете параметры в ваших расширениях (или в компиляторе - compiler pass),
значения этих параметров ещё не определены. Например, ваш бандл может определять
параметр my_cache_dir, ссылающийся на параметр %kernel.cache_dir%, который в
свою очередь содержит расположение директории для кэша, используемой ядром:
1
2

parameters:
my_cache_dir: %kernel.cache_dir%/my_cache

Метод load() вашего расширения должен создать эту директорию, если она не существует:

II Приёмы внедрения зависимостей

1
2
3
4
5

67

class MyExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$myCacheDir = $container->getParameter(’my_cache_dir’);

6

...

7

}

8
9

}

Когда метод load() будет вызван, значение параметра my_cache_dir всё еще будет
иметь вид %kernel.cache_dir%/my_cache. К счастью, вы можете использовать метод
ParameterBag::resolveValue() для того, чтобы заменить все “заполнители” их реальными значениями:
1

$myCacheDir = $container->getParameter(’my_cache_dir’);

2
3

$myCacheDir = $container->getParameterBag()->resolveValue($myCacheDir);

4
5
6

// теперь вы можете создать директорию для кэширования
mkdir($myCacheDir);

Параметры ядра
Ядро добавляет следующие параметры к контейнеру, перед тем как загрузить
бандлы (пути указаны от корня проекта):
• kernel.root_dir - место расположения класса ядра (как правило /app)
• kernel.environment - наименование окружения (например, dev, prod, и
т.д.)
• kernel.debug - активирован или нет режим отладки (true или false)
• kernel.name - имя директории, где расположено ядро (как правило app)
• kernel.cache_dir - место расположения директории с кэшем (по умолчанию /app/cache в Symfony 2.х и /var/cache в 3.x)
• kernel.logs_dir - место расположения директории с логами (по умолчанию /app/logs в Symfony 2.х и /var/logs в 3.x)
• kernel.bundles - список активированных бандлов (например, array(’FrameworkBundle’
=> ’Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle’, ...),
• kernel.charset - кодировка, используемая для ответа (например, UTF-8)
• kernel.container_class - имя класса сервисного контейрера (например
в dev-окружении по умолчанию будет appDevDebugProjectContainer),
который располагается в kernel.cache_dir.
Ядро также добавляет в контейнер значения переменных окружения, которые
начинаются с SYMFONY__ (если таковые обнаружатся). Перед добавлением таких параметров удаляется префикс, двойной подчерк __ заменяется на точку ., имя приводится к нижнему регистру, таким образом, например, переменная окружения SYMFONY__DATABASE__USER станет параметром контейнера
database.user.

II Приёмы внедрения зависимостей

68

7.3 Определяем параметры в расширениях контейнера
В будущем в множество раз окажетесь в таких ситуациях:
• Вы хотите, чтобы разработчик предоставил некоторое значение вашему бандлу через
конфигурацию в config.yml.
• Затем вы вероятно захотите использовать это значение в качестве аргумента для
одного из сервисов вашего бандла.
Положим у вас есть бандл BrowserBundle и вы хотите, чтобы разработчик указал значение
таймаута для сервиса browser:
1
2

browser:
timeout: 30

Для этого класс конфигурации должен иметь следующий вид:
1
2
3
4
5
6

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root(’browser’);

7

$rootNode
->children()
->scalarNode(’timeout’)
->end()
->end()
;

8
9
10
11
12
13
14

return $treeBuilder;

15

}

16
17

}

Затем в расширении контейнера вашего бандла нужно обработать конфигурационные
значения из config.yml таким образом:
1
2
3
4
5
6
7
8

class BrowserExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
// загружаем определения сервисов
$fileLocator = new FileLocator(__DIR__.’/../Resources/config’);
$loader = new XmlFileLoader($container, $fileLocator);
$loader->load(’services.xml’);

9

// обрабатываем конфигурацию
$processedConfig = $this->processConfiguration(
new Configuration(),
$configs
);

10
11
12
13
14

}

15
16

}

Сервис browser определён в services.xml:

II Приёмы внедрения зависимостей

1
2
3

69


%browser.timeout%


Значение таймаута переданное через config.yml (30) будет доступно в методе расширения
load() в виде $processedConfig[’timeout’], таким образом, вам остаётся лишь создать параметр browser.timeout в контейнере и присвоить ему переданное значение:
1

$container->setParameter(’browser.timeout’, $processedConfig[’timeout’]);

7.4 Переопределение параметров при помощи компилятора (compiler pass)
Иногда вам может потребоваться проанализировать и заменить параметр, определённый в другом бандле. Например, вам может потребоваться модифицировать иерархию
ролей пользователя, которая определена в security.yml, и доступна в виде параметра
security.role_hierarchy.roles. Вот как выглядит стандартная иерархия:
1
2
3
4
5
6
7
8
9
10

array (
’ROLE_ADMIN’ => array (
0 => ’ROLE_USER’,
),
’ROLE_SUPER_ADMIN’ => array (
0 => ’ROLE_USER’,
1 => ’ROLE_ADMIN’,
2 => ’ROLE_ALLOWED_TO_SWITCH’,
),
)

Положим, у вас есть другой механизм для определения иерархии ролей (к примеру, вы
можете загружать её из другого файла настроек), в этом случае вы можете модифицировать
или заменить иерархию ролей через отдельный compiler pass:
1
2
3
4
5

class EnhanceRoleHierarchyPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$parameterName = ’security.role_hierarchy.roles’;

6

$roleHierarchy = $container->getParameter($parameterName);

7
8

// модифицируем иерархию ролей
...

9
10
11

$container->setParameter($parameterName, $roleHierarchy);

12

}

13
14

}

Не забывайте регистрировать ваши проходы компилятора в классе вашего бандла:

II Приёмы внедрения зависимостей

1
2
3
4
5
6
7

class YourBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new EnhanceRoleHierarchyPass());
}
}

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

70

III Структура проекта
В предыдущих частях мы познакомились с тем, что происходит внутри ядра, когда оно
создаёт ответ на каждый переданный ему запрос. Мы также основательно познакомились со
всеми способами, с помощью которых вы можете создавать бандлы с гибкой конфигурацией. С этим багажом знаний, вы можете помещать ваш код в сервисы и делать его доступным
для других частей приложения. Когда же речь заходит о структуре всего приложения, то тут
всё еще есть вопросы. Как не допустить того, что весь код приложения сосредотачивался в
контроллерах? Как писать код, пригодный для повторного использования? И как, наконец,
писать код, который может работать как в web так и в командной строке?
Эти вопросы мы и рассмотрим в этой главе.

71

III Структура проекта

72

8 Организация слоёв приложения
8.1 Тонкие контроллеры
Во многих Symfony приложениях, очень часто контроллеры выглядят вот так:
1

namespace Matthias\AccountBundle\Controller;

2
3
4
5

use Symfony\Bundle\FrameworkBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Matthias\AccountBundle\Entity\Account;

6
7
8
9
10
11

class AccountController extends Controller
{
public function newAction(Request $request)
{
$account = new Account();

12

$form = $this->createForm(new AccountType(), $account);

13
14

if ($request->isMethod(’POST’)) {
$form->bind($request);

15
16
17

if ($form->isValid()) {
$confirmationCode = $this
->get(’security.secure_random’)
->nextBytes(4);
$account
->setConfirmationCode(md5($confirmationCode));

18
19
20
21
22
23
24

$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($account);
$entityManager->flush();

25
26
27
28

$this->sendAccountConfirmationMessage($account);

29
30

return $this->redirect($this->generateUrl(’mailbox_index’));

31

}

32

}

33
34

return array(
’form’ => $form->createView(),
);

35
36
37

}

38
39

private function sendAccountConfirmationMessage(Account $account)
{
$message = \Swift_Message::newInstance()
->setSubject(’Confirm account’)
->setFrom(’noreply@matthias.com’)
->setTo($account->getEmailAddress())
->setBody(’Welcome! ...’);

40
41
42
43
44
45
46
47

$this->get(’mailer’)->send($message);

48

}

49
50

}

Если взглянуть на контроллер newAction, вы можете увидеть там форму AccountType,
связанную с классом Matthias\AccountBundle\Entity\Account. После привязки дан-

III Структура проекта

73

ных и валидации формы генерируется код подтверждения и объект аккаунта сохраняется в
базу. После этого создаётся и высылается подтверждение по электронной почте.
В этом контроллере происходит много всего, и в результате мы имеем следующее:
1. Нет возможности разделить код, который можно повторно использовать от кода,
специфичного для данного конкретного проекта. Положим вы хотите повторно использовать часть логики создания аккаунта в будущих ваших проектах. В данном
случае это может быть достигнуто лишь копипастингом кода из этого контроллера
в новый проект. Это называется иммобильность (неподвижность, недвижимость) когда код не может быть легко перенесён в другое приложение.
2. Также у нас нет возможности повторно использовать логику содания аккаунта в какойлибо другой части этого приложения, так как весь код размещён внутри одного контроллера. Представьте, что вам нужно создать консольную команду, для импорта CSV
файла, который содержит данные пользовательских аккаунтов из предыдущей версии
приложения. В этом случае у вас нет возможности проделать эту процедуру без копипасты (опять?!) части кода из контроллера в другой класс. Я называю это контроллероцентризмом - когда код формируется преимущественно вокруг контроллеров.
3. Код очень тесно свзан с двумя другими библиотеками: SwiftMailer и Doctrine ORM.
Нет возможности запустить этот код без любой из этих библиотек, несмотря на то, что
есть много альтернатив для них обеих. Это состояние называется “тесными связями”
и это, как правило, такой код не является хорошим.
Для того, чтобы иметь возможность повторного использования кода в других приложениях,
или же для того, чтобы иметь возможность спользовать код в других частях того же приложения, или же для того, чтобы можно было легко сменить реализацию менеджера хранения,
вам нужно разделить код на несколько классов, каждый из которых занимется только одной
задачей.

8.2 Обработчики форм
Первым нашим шагом будет следущий: делегируем обработку формы специальному обработчику. Этот обработчик будет простым классом, который будет обрабатывать нашу
форму и выполнять все действия связанные с этим. Результатом первого рефакторинга
будет CreateAccountFormHandler:
1

namespace Matthias\AccountBundle\Form\Handler;

2
3
4
5
6
7

use
use
use
use
use

Symfony\Component\HttpFoundation\Request;
Symfony\Component\Form\FormInterface;
Doctrine\ORM\EntityManager;
Matthias\AccountBundle\Entity\Account;
Symfony\Component\Security\Core\Util\SecureRandomInterface;

8
9
10
11
12
13

class CreateAccountFormHandler
{
private $entityManager;
private $secureRandom;

III Структура проекта

74

public function __construct(
EntityManager $entityManager,
SecureRandomInterface $secureRandom
) {
$this->entityManager = $entityManager;
$this->secureRandom = $secureRandom;
}

14
15
16
17
18
19
20
21

public function handle(FormInterface $form, Request $request)
{
if (!$request->isMethod(’POST’)) {
return false;
}

22
23
24
25
26
27

$form->bind($request);

28
29

if (!$form->isValid()) {
return false;
}

30
31
32
33

$validAccount = $form->getData();

34
35

$this->createAccount($validAccount);

36
37

return true;

38

}

39
40

private function createAccount(Account $account)
{
$confirmationCode = $this
->secureRandom
->nextBytes(4);

41
42
43
44
45
46

$account
->setConfirmationCode(md5($confirmationCode));

47
48
49

$this->entityManager->persist($account);
$this->entityManager->flush();

50
51

}

52
53

}

Определение сервиса для этого обработчика будет следующим:
1
2
3
4
5

class=”Matthias\AccountBundle\Form\Handler\CreateAccountFormHandler”>




Как вы можете видеть, метод handle() возвращает значение true, если он смог выполнить
всё, что должен был, и false, если что-то пошло не так при обработке формы и форма
должна быть отображена опять. Используя этот простой механизм, мы слегка “похудеем”
наш контроллер:

III Структура проекта

1
2
3
4
5

75

class AccountController extends Controller
{
public function newAction(Request $request)
{
$account = new Account();

6

$form = $this->createForm(new AccountType(), $account);

7
8

$formHandler = $this
->get(’matthias_account.create_account_form_handler’);

9
10
11

if ($formHandler->handle($form, $request)) {
$this->sendAccountConfirmationMessage($account);

12
13
14

return $this->redirect($this->generateUrl(’mailbox_index’));

15

}

16
17

return array(
’form’ => $form->createView(),
);

18
19
20

}

21
22

}

Обработчики форм должны быть как можно более простыми и не должны создавать исключений, которые так или иначе были предназначены для предоставления пользователю
информации о возникших проблемах. Любую обратную связть с пользователем в обработчике формы вы должны осуществлять путём добавления ошибок к обрабатываемой форме
и возвращая false, что будет означать наличие проблемы при обработке формы:
1

use Symfony\Component\Form\FormError;

2
3
4
5
6

public function handle(FormInterface $form, Request $request)
{
if (...) {
$form->addError(new FormError(’У нас возникла проблемка...’));

7

return false;

8

}

9
10

}

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

8.3 Доменные менеджеры
Обработчик формы (и, возможно, класс формы) - это замечательные кандидаты на повторное использование. Тем не менее, в нашем лучае в нём всё ещё происходит много разных
действий. Полагая, что обязанностью обработчика формы является “просто обработать
форму”, окажется, что создание кода подтверждения тут лишнее. Также, обращение к слою

III Структура проекта

76

хранения данных (в нашем случае это Doctrine ORM) - это тоже лишнее для простого
обработчика форм.
Решением этой проблемы является делегирование задач относящихся к доменной модели
специализированным доменным менеджерам. Эти менеджеры могут работать напрямую
со слоем хранения данных. Давайте создадим класс менеджера для задач, связанных с
аккаунтом и назовём его AccountManager. Он может выглядеть таким образом:
1

namespace Matthias\AccountBundle\DomainManager;

2
3
4
5
6
7

use
use
use
use
use

Symfony\Component\HttpFoundation\Request;
Symfony\Component\Form\FormInterface;
Doctrine\ORM\EntityManager;
Matthias\AccountBundle\Entity\Account;
Symfony\Component\Security\Core\Util\SecureRandomInterface;

8
9
10
11
12

class AccountManager
{
private $entityManager;
private $secureRandom;

13

public function __construct(
EntityManger $entityManager,
SecureRandomInterface $secureRandom
) {
$this->entityManager = $entityManager;
$this->secureRandom = $secureRandom;
}

14
15
16
17
18
19
20
21

public function createAccount(Account $account)
{
$confirmationCode = $this
->secureRandom
->nextBytes(4);

22
23
24
25
26
27

$account
->setConfirmationCode(md5($confirmationCode));

28
29
30

$this->entityManager->persist($account);
$this->entityManager->flush();

31
32

}

33
34

}

Теперь обработчик формы будет просто использовать AccountManager для того чтобы
собственно создать аккаунт:
1
2
3

class CreateAccountFormHandler
{
private $accountManager;

4
5
6
7
8

public function __construct(AccountManager $accountManager)
{
$this->accountManager = $accountManager;
}

9
10
11
12

public function handle(FormInterface $form, Request $request)
{
...

13
14

$validAccount = $form->getData();

III Структура проекта

77

15

$this->accountManager->createAccount($validAccount);

16

}

17
18

}

Ниже представлены определния соответствующих сервисов для обработчика формы и доменного менеджера:
1
2
3
4

class=”Matthias\AccountBundle\Form\Handler\CreateAccountFormHandler”>



5
6
7
8
9
10

class=”Matthias\AccountBundle\DomainManager\AccountManager”>




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






Создание и сохранение объектов
Создание связей между объектами (например, между двумя пользователями)
Дублирования объектов
Удаления объектов


8.4 События
Как вы могли отметить выше, внутри контроллера, после содания нового аккаунта, отправлялось письмо с подтверждением. Подобные действия лучше выносить из контроллеров.
Отправка писем - это далеко не единственное действие, которое может потребоваться
после создания нового аккаунта. Возможно потребуется заполнить для нового пользователя
некоторые настройки по-умолчанию, или же отправить уведомление владельцу сайта, что
в его продукте зарегистрирован новый пользователь.
Это прекрасный случай воспользоваться событийно-ориентированным (event-driven)
подходом: в нашей ситуации внутри AccountManager происходит одно из базовых событий, (а именно, “создан новый аккаунт”). Другие части приложения должны иметь возможность отреагировать на это событие. В этом случае должен существовать как минимум
слушатель события, который отправил бы письмо с подтверждением аккаунта новому
пользователю.
Для обработки такого специализированного события, использующего некоторые данные,
нужно создать новый класс события, который должен наследоваться от базового класса
Event:

III Структура проекта

1

78

namespace Matthias\AccountBundle\Event;

2
3

use Symfony\Component\EventDispatcher\Event;

4
5
6
7

class AccountEvent extends Event
{
private $account;

8

public function __construct(Account $account)
{
$this->account = $account;
}

9
10
11
12
13

public function getAccount()
{
return $this->account;
}

14
15
16
17
18

}

Затем нужно придумать имя для нового события - назовём его matthias_account.new_account_created. Как правило, хорошей практикой является хранение этого имени в виде
константы в отдельном классе где-то в вашем бандле:
1

namespace Matthias\AccountBundle\Event;

2
3
4
5
6

class AccountEvents
{
const NEW_ACCOUNT_CREATED = ’matthias_account.new_account_created’;
}

Теперь нам необходимо модифицировать AccountManager, чтобы отправить наше новое
событие matthias_account.new_account_created:
1

namespace Matthias\AccountBundle\DomainManager;

2
3
4
5

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Matthias\AccountBundle\Event\AccountEvents;
use Matthias\AccountBundle\Event\AccountEvent;

6
7
8
9

class AccountManager
{
...

10
11

private $eventDispatcher;

12
13
14
15
16
17

public function __construct(
...
EventDispatcherInterface $eventDispatcher
) {
...

18

$this->eventDispatcher = $eventDispatcher;

19
20

}

21
22
23
24

public function createAccount(Account $account)
{
...

25
26

$this->eventDispatcher->dispatch(

III Структура проекта

AccountEvents::NEW_ACCOUNT_CREATED,
new AccountEvent($account)

27
28

);

29

}

30
31

79

}

Не забудьте добавить сервис event_dispatcher в качестве аргумента к определению
сервиса AccountManager:
1
2
3
4
5
6

class=”Matthias\AccountBundle\DomainManager\AccountManager”>





Слушатель события matthias_account.new_account_created будет выглядеть следующим образом:
1

namespace Matthias\AccountBundle\EventListener;

2
3
4
5

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Matthias\AccountBundle\Event\AccountEvents;
use Matthias\AccountBundle\Event\AccountEvent;

6
7
8
9

class SendConfirmationMailListener implements EventSubscriberInterface
{
private $mailer;

10

public static function getSubscribedEvents()
{
return array(
AccountEvents::NEW_ACCOUNT_CREATED => ’onNewAccount’
);
}

11
12
13
14
15
16
17

public function __construct(\SwiftMailer $mailer)
{
$this->mailer = $mailer;
}

18
19
20
21
22

public function onNewAccount(AccountEvent $event)
{
$this->sendConfirmationMessage($event->getAccount());
}

23
24
25
26
27

private function sendConfirmationMessage(Account $account)
{
$message = \Swift_Message::newInstance();

28
29
30
31

...

32
33

$this->mailer->send($message);

34

}

35
36

}

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

III Структура проекта

1
2
3
4
5

80

class=”Matthias\AccountBundle\EventListener\SendConfirmationMailListener”>




Лучшие практики использования слушаетелей событий
Слушатель события должен именоваться от выполняемого им действия, а не от
события, которое он слушает. Таким образом, вместо того, чтобы назвать слушатель NewAccountEventListener, вы должны назвать его SendConfirmationMailListener.
Это также поможет другим разработчикам, если они захотят найти место в коде,
где отправляется сообщение с подтверждением регистрации.
Также, когда должно выполниться другое действие при возникновении события,
как, например, отправка еще одного сообщения, но уже владельцу сайта, вам
нужно создать другой слушатель для этого события, вместо того, чтобы добавлять код в существующий слушатель. Включение или отключение конкретного
слушателя - это простая операция, что уменьшает сложность поддержки кода,
потому что вы не сможете изменить поведение системы случайно.
События уровня хранения (persistence)
Как вы можете помнить, AccountManager (доменный менеджер) генерирует код подтверждения для учётной записи прямо перед тем, как сохранить его:
1
2
3
4

class AccountManager
{
private $entityManager;
private $secureRandom;

5

public function __construct(
EntityManger $entityManager,
SecureRandomInterface $secureRandom
) {
$this->entityManager = $entityManager;
$this->secureRandom = $secureRandom;
}

6
7
8
9
10
11
12
13

public function createAccount(Account $account)
{
$confirmationCode = $this
->secureRandom
->nextBytes(4);

14
15
16
17
18
19

$account
->setConfirmationCode(md5($confirmationCode));

20
21
22

$this->entityManager->persist($account);
$this->entityManager->flush();

23
24

}

25
26

}

Это не очень хороший подход. Повторю ещё раз: учётная запись может быть создана
где-то ещё и она может не иметь кода подтверждения. С точки зрения “ответственности

III Структура проекта

81

(responsibility)”, если мы посмотрим на зависимости AccountManager, станет непонятно,
почему там должен быть объект, реализующий интерфейс SecureRandomInterface и
возникнет вопрос: зачем ему это, если в его обязанности входит лишь создание учётной
записи?
Эту логику нужно вынести куда-то в другое место, ближе к реальному сохранению новой
учётной записи. Большинство реализаций слоя хранения данных поддерживает что-то типа
событий или поведений (behaviors), при помощи которых вы можете внелриться в процесс
сохранения, обновления или удаления объектов.
Doctrine ORM это реализуется черезк подписчики события:
1
2

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;

3
4
5
6

class CreateConfirmationCodeEventSubscriber implements EventSubscriber
{
private $secureRandom;

7

public function __construct(SecureRandomInterface $secureRandom)
{
$this->secureRandom = $secureRandom;
}

8
9
10
11
12

public function getSubscribedEvents()
{
return array(
’prePersist’
);
}

13
14
15
16
17
18
19

public function prePersist(LifecycleEventArgs $event)
{
// this will be called for *each* new entity

20
21
22
23

$entity = $event->getEntity();
if (!($entity instanceof Account)) {
return;
}

24
25
26
27
28

$this->createConfirmationCodeFor($entity);

29

}

30
31

private function createConfirmationCodeFor(Account $account)
{
$confirmationCode = $this
->secureRandom
->nextBytes(4);

32
33
34
35
36
37

$account
->setConfirmationCode(md5($confirmationCode));

38
39

}

40
41

}

Вы можете зарегистрировать этот подписчик, использовав метку doctrine.event_subscriber:

III Структура проекта

1
2
3

82





Вообще говоря событий там имеется больше, например, postPersist, preUpdate, preFlush,
и т.д. Эти события позволяют вам внедряться в любую стадию жизненного цикла ваших
сущностей. В частности, событие preUpdate может быть очень удобным, для того, чтобы
определять что кто-то изменил значение какого-либо поля:
1

use Doctrine\ORM\Event\PreUpdateEventArgs;

2
3
4
5
6
7
8
9
10

class CreateConfirmationCodeEventSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(
’preUpdate’
);
}

11

public function preUpdate(PreUpdateEventArgs $event)
{
$entity = $event->getEntity();
if (!($entity instanceof Account)) {
return;
}

12
13
14
15
16
17
18

if ($event->hasChangedField(’emailAddress’)) {
// create a new confirmation code
$confirmationCode = ...;
$event->setNewValue(’confirmationCode’, $confirmationCode);
}

19
20
21
22
23
24

}

Как вы можете видеть, слушатели события preUpdate получают особый объект события.
Вы можете использовать его для проверки полей, которые были изменены и для того, чтобы
изменить что-то ещё.
Подводные камни событий Doctrine
Ниже приведено несколько неочевидных вещей, связанных с использованием
событий Doctrine: - Событие preUpdate отправляется только тогда, когда значение какого-либо поля было изменено, то есть не обязательно каждый раз
когда вы выполняете метод flush() у вашего менеджера сущностей. - Событие
prePersist отправляется только в том случае, если сущность ранее не была
сохранена (persisted) ранее. - В некоторых ситуациях вы можете выполнить
изменения в объекте сущности слишком поздно и вам нужно будет вручную
запросить у UnitOfWork пересчитать изменения:
1
2
3
4
5
6

$entity = $event->getEntity();
$className = get_class($entity);
$entityManager = $event->getEntityManager();
$classMetadata = $entityManager->getClassMetadata($className);
$unitOfWork = $entityManager->getUnitOfWork();
$unitOfWork->recomputeSingleEntityChangeSet($classMetadata, $entity);

III Структура проекта

83

Состояния и контекст
Сервисы можно разделить на две группы:
1. Статические сервисы
2. Динамические сервисы
Большинство сервисов, определённых в сервисном контейнере принадлежат к первой категории. Статический сервис выполняет одну и ту же работу каждый раз будучи вызванным.
Получая на входе одни и те же значения такой сервис должен выдать одинаковый результат
раз за разом. Примером такого сервиса могут быть сервис отправки email пользователю или
сохранение объекта при помщи менеджера сущностей.
Во вторую категорию попадают сервисы, которые могут изменяться: когда вы используете
динамический сервис, вы не можете заранее знать, работет ли он так, как вы ожидаете,
так как результат его работы может зависеть от текущего запросаили, в случае Symfonyприложения: от факта, что ядро обрабатывает запрос.

9.1 Контекст безопасности
@dbykadorov: ВАЖНО! Отличия в версиях 2.6 и страше
Вообще говоря данный раздел уже изрядно устарел. Начиная с версии Symfony 2.6
security.context объявлен устаревшим и в 3.0 был удалён. Обязанности контекста безопасности разделили другие сервисы, прежде всего security.token_storage и security.authorization_checker
1
2
3
4

// Symfony 2.5
$user = $this->get(’security.context’)->getToken()->getUser();
// Symfony 2.6
$user = $this->get(’security.token_storage’)->getToken()->getUser();

5
6
7
8
9

//
if
//
if

Symfony 2.5
(false === $this->get(’security.context’)->isGranted(’ROLE_ADMIN’)) { ... }
Symfony 2.6
(false === $this->get(’security.authorization_checker’)->isGranted(’ROLE_ADMIN’)) { ... }

Используются они схожим образом, так что кардинальных отличий не будет, но
имейте это в виду.
Одним из очевидных примеров динамического сервиса является контекст безопасности
(security context). Вернёт ли он вам объект пользователя, когда вы выполните вызов методов
->getToken()->getUser(), зависит от многих факторов, прежде всего от запроса и от
того, смог ли файрвол определить - залогинен пользователь или нет. Кроме того, многие
сервисы зависят от security context’а, например, вот этот:

III Структура проекта

1

84

use Symfony\Component\Security\Core\SecurityContextInterface;

2
3
4
5
6

class UserMailer
{
private $securityContext;
private $mailer;

7

public function __construct(
SecurityContextInterface $securityContext,
\SwiftMailer $mailer
) {
$this->securityContext = $securityContext;
$this->mailer = $mailer;
}

8
9
10
11
12
13
14
15

public function sendMailToCurrentUser($subject, $body)
{
$token = $this->securityContext->getToken();
if (!($token instanceof TokenInterface)) {
// we are not behind a firewall
return;
}

16
17
18
19
20
21
22
23

$user = $token->getUser();
if (!($user instanceof User)) {
// no logged in user
return;
}

24
25
26
27
28
29

$message = \Swift_Message::newInstance()
->setTo($user->getEmailAddress())
->setSubject($subject)
->setBody($messageBody);

30
31
32
33
34

$this->get(’mailer’)->send($message);

35

}

36
37

}

Определение этого сервиса выглядит вот так:
1
2
3
4






Внутри контроллера вы можете использовать этот сервис для того, чтобы отправить письмо
текущему пользователю (обратите внимание на аннотацию @Secure - она позволяет быть
уверенным, что тут пользователь будет залогинен и у него будет как минимум роль ROLE_USER):

III Структура проекта

1
2

85

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use JMS\SecurityExtraBundle\Annotation\Secure;

3
4
5
6
7
8
9
10
11
12
13

class SomeController extends Controller
{
/**
* @Secure(”ROLE_USER”)
*/
public function sendMailAction()
{
$this->get(’user_mailer’)->sendMailToCurrentUser(’Hi’, ’there!’);
}
}

Хотите знать, почему этот код является плохим? Ну… вот вам как минимум 2 причины:
1. Сервис user_mailer может быть использован только для отправки писем текущему
юзеру. В то же время, ничего специфичного относительно текущего пользователя (по
сравнению с прочими) тут не выполняется. Метод sendMailToCurrentUser($subject,
$body) может быть легко заменен на sendMailTo(User $user, $subject, $body).
Это делает ваш класс более общим и пригодным для повторного использования.
2. Сервис user_mailer может быть использован только лишь в запросах, которые обслужиаются внутри какого-либо файрволла (TODO: найти когда файрвол перестал
быть необходим для вызова функций безопасности (ранее вне файрвола кидалось
исключение)). Если запросить сервис user_mailer из консольной команды, он будет
совершенно бесполезен, так как в этом случае файрвол не будет определён. Решение
первой проблемы решит и эту без дополнительных усилий с вашей стороны.
Итак, давайте изменим класс UserMailer, чтобы решить указанные проблемы:
1
2
3

class UserMailer
{
private $mailer;

4

public function __construct(\SwiftMailer $mailer)
{
$this->mailer = $mailer;
}

5
6
7
8
9

public function sendMailToUser(User $user, $subject, $body)
{
$message = \Swift_Message::newInstance()
->setTo($user->getEmailAddress())
->setSubject($subject)
->setBody($body);

10
11
12
13
14
15
16

$this->mailer->send($message);

17

}

18
19

}

В результате у нас класс тал намного меньше, а код - чище. Теперь сервис user_mailer
ничего не знает о пользователе, которому должен отправить письмо (что также хорошо):
поэтому вам каждый раз нужно будет передавать объект класса User, который может быть,
а может и не быть текущим пользователем.

III Структура проекта

86

Контроллер является отличным местом для определения текущего пользователя, так как мы
точно знаем, что контроллер выполняется лишь во время обработки запроса, и мы можем
быть уверены, что он защищён файрволлом, если мы заранее явно сконфигурировали это.
В этом случае нам будет доступен “текущий пользователь” (предполагается что файрволл
корретно настроен) и мы можем переписать наш контроллер следующим образом:
1
2
3
4
5
6
7
8

class SomeController extends Controller
{
/**
* @Secure(”ROLE_USER”)
*/
public function sendMailAction()
{
$user = $this->getUser();

9

$this->get(’user_mailer’)->sendMailTo($user, ’Hi’, ’there!’);

10

}

11
12

}

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

9.2 Запрос
Токен пользователя добавляется в контекст безопасности один раз для каждого запроса,
но только для мастер-запроса. Но другие сервисы, которые вы создаёте, могут зависеть от
текущего объекта запроса, который отличается от мастер-запроса или под-запроса. Этот
объект запроса доступен в виде сервиса request, который является динамическим. В
какой-то момент времени (фактически, перед тем как ядро начнёт обработку запроса) этот
сервис запсроса будет недоступен и не сможет быть использован любым другим объектом в
качестве зависимости. В то же время, может быть ситуация, когда существует более одного
сервиса запроса, а именно - когда ядро обрабатывает подзапрос.
В сервисном контейнере эта проблема решается при помощи т.н. сфер действия (scopes)
Контейнер может входить в сферы действия и покидать их; в момент перехода между
сферами действия либо восстанавливается предыдущий сервис, либо для новой сферы
действия предоставляется новый. В случае с запросом, есть несколько состояний, в которых
может быть контейнер:
1. Контейнер находится в сфере действия container, сервис запроса не определён.
2. Ядро обрабатывае главный запрос, оно переводит контейнер в сферу действия request
и создаёт сервис запроса.
3. Ядро обрабатывает подзапрос, оно переводит контейнер в сферу действия request,
но уже для другого запроса и устанавливает подзапрос в качестве сервиса запроса.
4. Ядро завершает обработку подзапроса и выводит контейнер из сферы действия request
для подзапроса. Восстанавливается предыдущий сервис запроса.

III Структура проекта

87

5. Ядро завершает обработку главного запроса и выводит контейнер из сферы действия
запроса. Сервис запроса опять будет неопределён.
Таким оразом, когда бы вы ни выполнили вызов $container->get(’request’) вы “всегда” получите текущий объект Request. Когда вам нужно предоставить сервис запроса
одному из ваших объектов, для этого имеется несколько способов, которые я, тем не менее,
категорически не рекомендую использовать.
@dbykadorov: ВАЖНО! Отличия в версиях 2.8 и страше
В Symfony, начиная с версии 2.8 принцип сфер действия - scopes помечен как
устаревший. В current ветке документации нет более такой статьи, поэтому
ссылка ведёт на документацию версии 2.8.
Также изменения коснулись и сервиса запроса, который выше предлагается получать, например так: $container->get(’request’). Эти изменения - часть
процесса рефакторинга работы с запросом, который начался с версии 2.4 введением сервиса request_stack (см. тут), который позволил разрешить неопределённость с запросами, которую ранее пытались разрешить при помощи scopes.
Таким образом, начиная с версии 2.8 рекомендуется (а с версии 3.0 другого
варианта и нет) получать текущий запрос через request_stack:
1
2
3
4
5
6
7
8

// Где-то в контроллере
...
public function foo () {
$request = $this
->get(’request_stack’)
->getCurrentRequest();
// ... делаем с запросом что нам требуется
}

В принципе, Маттиас как раз далее и предлагает альтернативы для этой ситуации, актуальные на момент написания им книги, для Symfony 2.3 и настоятельно
не рекомендует писать ваш код, зависимым от сервиса запроса.
Сама по себе необходимость иметь доступ к запросу для выполнения каких-либо действий
не является чем-то экстремально плохим. Но вы не должны зависеть от сервиса запроса.
Положим вам нужно сделать что-то типа счётчика посещений страницы:
1

use Symfony\Component\HttpFoundation\Request;

2
3
4
5

class PageCounter
{
private $counter;

6
7
8
9
10

public function __construct(Request $request)
{
$this->request = $request;
}

11
12
13

public function incrementPageCounter()
{

III Структура проекта

88

$uri = $this->request->getPathInfo();

14
15

// каким-либо образом инкрементим счётчик посещений для этого URI
...

16
17

}

18
19

}

Определение этого сервиса будет таким:
1
2
3





Так как вы зависите от сервиса запроса, весь ваш сервис должен иметь сферу действия
request.
В котроллере вызов счётчика в этом случае будет выглядеть так:
1
2
3
4
5
6
7

class InterestingController extends Controller
{
public function indexAction()
{
$this->get(’page_counter’)->incrementPageCounter();
}
}

Несомненно, сервис page_counter имеет возможность получить URI текущей страницы,
так как у него есть весь объект Request. Но:
1. Статистика может быть получена лишь при выполнении текущего запроса. Когда ядро
завершит обработку запроса, сервис запроса будет установлен в null и наш счётчик
посещений будет бесполезен.
2. Класс PageCounter тесно связан с объектом класса Request. Счётчику же, на самом деле, требуется лишь URI. И этот URI не обязательно может быть получен из
объекта Request. Будет лучше, если мы передадим его в качестве аргумента метода
incrementPageCounter(): php public function incrementPageCounter($uri)
{ ... }
Эти две пробемы могут казаться надуманными, но в один прекрасный день вам может
потребоваться импортировать посещения, собранные каким-либо другим способом и придётся вызывать метод incrementPageCounter() “вручную”. В этому случае вы только
порадуетесь, что ваш счётчик не привязан классу Request.
Избегаем зависимостей от текущего запроса
Имеется две главных стратегии, как можно избежать зависмости от текущего запроса:

III Структура проекта

89

Используем слушатель (event listener)

Как вы вероятно можете помнить из первой главы - ядро всегда отправляет событие
kernel.request для каждого запроса, который оно обрабатывает. В этото момент вы можете использовать слушатель для выполнения действий, которые вы бы хотели выполнять
для каждого запроса, например, инкрементить счётчик посещений страницы, или может
быть остановить дальнейшее выполнение, вызвав исключение.
1
2
3

class PageCounterListener
{
private $counter;

4

public function __construct(PageCounter $counter)
{
$this->counter = $counter;
}

5
6
7
8
9

public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();

10
11
12
13

$this->counter->incrementPageCounter($request->getPathInfo());

14

}

15
16

}

Предоставлять объект запроса во время выполнения

Когда вам необходим весь объект запроса целиком, всегда лучше начать его использование
там, где вы точно уверены, что этот объект существует (и вы можете его получить) и в этом
месте передать запрос в нужный сервис:
1
2
3
4
5
6
7

class SomeController extends Controller
{
public function indexAction(Request $request)
{
$this->get(’page_counter’)->handle($request);
}
}

Поиск совпадений для запросов (request matcher)
Во многих ситуациях, в которых вам будет нужен объект запроса для вашего
сервиса, вы, вероятно, хотите его использовать для выполнения каких-либо
сравнений. В этом случае вы можете абстрагироваться от логики сравнения при
помощи обнаружителя совпадений запроса (request matcher):
1
2

use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpFoundation\Request;

3
4
5
6
7
8
9
10

class MyRequestMatcher implements RequestMatcherInterface
{
public function matches(Request $request)
{
return $request->getClientIp() === ’127.0.0.1’;
}
}

III Структура проекта

90

Или используя стандартный RequestMatcher:
use Symfony\Component\HttpFoundation\RequestMatcher;

1
2

$matcher = new RequestMatcher();
$matcher->matchIp(’127.0.0.1’);
// $matcher->matchPath(’^/secure’);
...

3
4
5
6
7

if ($matcher->matches($request)) {
...
}

8
9
10

Использование только нужных значений

Перед тем, как передать в сервис весь объект запроса, всегда спрашивайте себя: а нужна
ли мне вся эта информация целиком? Или же мне нужна только её конкретная часть? Если
это так, отвязывайте зависимость от запроса и передавайте в качестве аргументов лишь
нужные вам данные:
Изменим это:
1
2
3
4
5

class AccessLogger
{
public function logAccess(Request $request)
{
$ipAddress = $request->getClientIp();

6

// log the IP address
...

7
8

}

9
10

}

На это:
1
2
3
4
5
6
7

class AccessLogger
{
public function logAccess($ipAddress)
{
...
}
}

Это сделает ваш код пригодным для повторного использования в других проектах, даже
таких, которые не используют компонент Symfony HttpFoundation. См. также раздел
книги уменьшаем связность с фреймворком.

IV Соглашения по конфигурированию

91

IV Соглашения по конфигурированию

Настройка конфигурации приложения

92

IV Соглашения по конфигурированию

Соглашения по конфигурированию

93

V Безопасность

94

V Безопасность

Введение

95

V Безопасность

Аутентификация и сессии

96

V Безопасность

Проектирование контроллеров

97

V Безопасность

Проверка ввода

98

V Безопасность

Экранирование вывода

99

V Безопасность

Будучи скрытным…

100

VI Используем аннотации

101

VI Используем аннотации

Введение

102

VI Используем аннотации

Аннотация - это лишь Value Object

103

VI Используем аннотации

Приемлемые случаи для использования аннотаций

104

VI Используем аннотации

Используем аннотации в вашем Symfony приложении

105

VI Используем аннотации

Проектирование для повторного использования

106

VI Используем аннотации

Заключение

107

VII Быть Symfony разработчиком

108

VII Быть Symfony разработчиком

Код для повторного использования имеет слабые связи

109

VII Быть Symfony разработчиком

Код для повторного использования должен быть переносимым

110

VII Быть Symfony разработчиком

Код для повторного использования должен быть расширяемым

111

VII Быть Symfony разработчиком

112

Код для повторного использования должен быть прост в использовании

VII Быть Symfony разработчиком

Код для повторного использования должен быть надёжен

113

Заключение

114