У нашего фреймворка до сих пор нету одной важной характеристики любого хорошего фреймворка – гибкости. Гибкость – возможность легко добавлять hook’и в цикл фреймворка для того чтобы изменить обработку запроса.

О каких хукам мы говорим? Например, аутентификация или кэш. Для того чтобы была гибкость, хуки должны быть “вставил и заработало” (прости меня учительница по русскому языку); они будут разными для разных приложений. Большиство софта работают по одной концепции, например Drupal или WordPress. В некоторых языках, даже есть стандарт: WSGI в Python или Rack в Ruby.

В PHP такого стандарта нету, мы будем использовать хорошо всем знакомый паттерн Observer, который позволяет съэмитировать похожее поведение для нашего фреймворка; компонент Symfony2 EventDispatcher представляет собой легковесную реализацию этого паттерна:

Как же это работает? Диспетчер, это центральный объект, который контролирует события системы, информирует слушателей о событиях. Иначе говоря: ваш код вызывает какое-нибудь событие, диспетчер уведомляет всех слушателей что такое-то событие случилось, и каждый слушатель делает что хочет на основании этого события.

Для примера сделаем слушателя, который будет добавлять код Google Analytics во все ответы.

Чтобы этого добиться фреймворд должен отслеживать событие перет отправкой Response:

Теперь при обработке каждого Request, отправляется объект ResponseEvent:

Последний шаг это создание диспетчера во фронт-контроллере и регистрация слушателей для события response:

Слушатель только доказывает концепцию, вы должны добавить код Google Analytics перед тэгом body.

Как вы могли заметить, addListener() связывает callback PHP с событием response; название события должно совпадать с тем, что мы используем при вызове dispatch().

В слушателе, мы добавляем код Google Analytics только если ответ не перенапрявляет, если запрос имеет формат HTML, и если заголовок ответа “content type” HTML (эти условия показываеют как легко можно манипулиривать данными Request и Response в вашем коде).

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

В зависимости от того куда вы добавили этот кусок кода: до или после предыдущего слушателя, вы можете получить ошибку или правильное значение Content-Length. Иногда порядок слушателя не имеет значения, и все они регистрируются с некоторым приоритетом 0. Чтобы указать диспетчеру какой случатель выполнять раньше измените приоритет на положительное число, отрицательное число используется для низко приоритетных слушателей. В нашем случае мы хотим чтобы слушатель Content-Length выполнялся последним, так что изменим приоритет скажем на -255:

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

Отрефакторим код, переместив слушателя Google Analytic в свой собственный класс:

Тоже самое сделаем и со вторым слушателем:

Теперь наш фронт-контроллер должен выглядеть как-то так:

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

Подписчик знает все о событиях и передает эту информацию диспетчеру через метод getSubscribedEvents(). Давайте посмотрим на новую версию GoogleListener:

И конечно же ContentLengthListener:

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

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