У нас все еще отсутствует очень важное качество любого хорошего фреймворка: расширяемость. Расширяемость — значит, что любой программист может без проблем внедрить свой функционал в наш фреймворк и изменить способ обработки запроса.
О каком внедрении функционала я говорю? Например аутентификация, или кэширование инстансов. Причем внедрение функционала должно быть plug-and-play. По такому принципу работают например Drupal и WordPress. Часто эта возможность даже предусмотрена на уровне языка программирования, например WSGY в Python или Rack в Ruby.
Так как в PHP ничего подобного на уровне языка не предусмотрено, мы будем использовать хорошо известный шаблон Наблюдатель (Observer). В Symfony2 компонент EventDispatcher реализует упрощенную версию этого шаблона.
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "require": { "symfony/class-loader": "2.1.*", "symfony/http-foundation": "2.1.*", "symfony/routing": "2.1.*", "symfony/http-kernel": "2.1.*", "symfony/event-dispatcher": "2.1.*" }, "autoload": { "psr-0": { "Simplex": "src/", "Calendar": "src/" } } } |
Как это работает? Диспетчер (dispatcher) — центральный объект системы, который уведомляет объекты-слушатели о произошедших событиях. Другими словами, ваш код сообщает диспетчеру о событии, диспетчер оповещает о событии всех слушателей, и каждый слушатель делает с этим событием что хочет.
Например, давайте создадим слушателя, который добавляет код Google Analytics ко всем ответам. Что бы сделать это, наш фреймворк должен передать событие прямо перед возвратом объекта response.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
<?php // framework/src/Simplex/Framework.php namespace Simplex; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\EventDispatcher\EventDispatcher; class Framework { protected $matcher; protected $resolver; protected $dispatcher; public function __construct(EventDispatcher $dispatcher, UrlMatcherInterface $matcher, ControllerResolverInterface $resolver) { $this->matcher = $matcher; $this->resolver = $resolver; $this->dispatcher = $dispatcher; } public function handle(Request $request) { try { $request->attributes->add($this->matcher->match($request->getPathInfo())); $controller = $this->resolver->getController($request); $arguments = $this->resolver->getArguments($request, $controller); $response = call_user_func_array($controller, $arguments); } catch (ResourceNotFoundException $e) { $response = new Response('Not Found', 404); } catch (\Exception $e) { $response = new Response('An error occurred', 500); } // dispatch a response event $this->dispatcher->dispatch('response', new ResponseEvent($response, $request)); return $response; } } |
Каждый раз как фреймворк обрабатывает Запрос, передается событие ResponseEvent.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?php // framework/src/Simplex/ResponseEvent.php namespace Simplex; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\EventDispatcher\Event; class ResponseEvent extends Event { private $request; private $response; public function __construct(Response $response, Request $request) { $this->response = $response; $this->request = $request; } public function getResponse() { return $this->response; } public function getRequest() { return $this->request; } } |
Последний шаг — создание Диспетчера во фронт-контроллере, и регистрация слушателя для события response.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<?php // framework/web/front.php require_once __DIR__.'/../vendor/.composer/autoload.php'; // ... use Symfony\Component\EventDispatcher\EventDispatcher; $dispatcher = new EventDispatcher(); $dispatcher->addListener('response', function (Simplex\ResponseEvent $event) { $response = $event->getResponse(); if ($response->isRedirection() || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')) || 'html' !== $event->getRequest()->getRequestFormat() ) { return; } $response->setContent($response->getContent().'GA CODE'); }); $framework = new Simplex\Framework($dispatcher, $matcher, $resolver); $response = $framework->handle($request); $response->send(); |
Как видите, addListener() ассоциирует PHP callback объект с событием (response). Название события должно быть тем же самым, что использовалось в вызове dispatch().
В Слушателе мы добавляем код Google Analytics только если ответ не переадресация, запрошенный формат HTML и тип данных ответа HTML (эти условия отлично демонстрируют насколько легко работать с данными Запроса и Ответа в коде).
Пока все хорошо, но давайте добавим еще одного слушателя к этому событию. Допустим, я хочу задать заголовок Content-Length в ответе, если он еще не задан.
1 2 3 4 5 6 7 8 |
$dispatcher->addListener('response', function (Simplex\ResponseEvent $event) { $response = $event->getResponse(); $headers = $response->headers; if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) { $headers->set('Content-Length', strlen($response->getContent())); } }); |
Правильная работа этого кода зависит от того, добавите вы его до регистрации предыдущего слушателя или после. Иногда приоритет слушателей очень важен, однако по-умолчанию все слушатели имеют приоритет — 0. Для того, что бы Диспетчер запустил слушателя раньше — измените приоритет на положительное число. Отрицательные числа используются для задания низкого приоритета. Так как мы хотим чтобы слушатель Content-Length запускался последним, изменим его приоритет на -255.
1 2 3 4 5 6 7 8 |
$dispatcher->addListener('response', function (Simplex\ResponseEvent $event) { $response = $event->getResponse(); $headers = $response->headers; if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) { $headers->set('Content-Length', strlen($response->getContent())); } }, -255); |
Давайте немного порефакторим код. Вынесем слушателя Google в отдельный класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php // framework/src/Simplex/GoogleListener.php namespace Simplex; class GoogleListener { public function onResponse(ResponseEvent $event) { $response = $event->getResponse(); if ($response->isRedirection() || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')) || 'html' !== $event->getRequest()->getRequestFormat() ) { return; } $response->setContent($response->getContent().'GA CODE'); } } |
Сделаем тоже самое с другим слушателем:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php // framework/src/Simplex/ContentLengthListener.php namespace Simplex; class ContentLengthListener { public function onResponse(ResponseEvent $event) { $response = $event->getResponse(); $headers = $response->headers; if (!$headers->has('Content-Length') && !$headers->has('Transfer-Encoding')) { $headers->set('Content-Length', strlen($response->getContent())); } } } |
Наш Front Controller должен выглядеть так:
1 2 3 |
$dispatcher = new EventDispatcher(); $dispatcher->addListener('response', array(new Simplex\ContentLengthListener(), 'onResponse'), -255); $dispatcher->addListener('response', array(new Simplex\GoogleListener(), 'onResponse')); |
Сейчас код, обернутый в классы, смотрится довольно прилично, однако, есть одно слабое место: приоритеты слушателей захардкодены во фронт-контроллер, вместо того, что бы находится в самих слушателях. Более того, название методов слушателей, также находится здесь, а значит их рефакторинг подразумевает изменение приложения, которое от них зависит. Конечно, есть решение: использовать вместо слушателей подписчиков.
1 2 3 |
$dispatcher = new EventDispatcher(); $dispatcher->addSubscriber(new Simplex\ContentLengthListener()); $dispatcher->addSubscriber(new Simplex\GoogleListener()); |
Подписчики знают обо всех событиях, которые им интересны и передают эту информацию Диспетчеру методом getSubscribedEvents(). Посмотрим на новую версию GoogleListener:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php // framework/src/Simplex/GoogleListener.php namespace Simplex; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class GoogleListener implements EventSubscriberInterface { // ... public static function getSubscribedEvents() { return array('response' => 'onResponse'); } } |
Соответственно новая версия ContentLengthListener:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php // framework/src/Simplex/ContentLengthListener.php namespace Simplex; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class ContentLengthListener implements EventSubscriberInterface { // ... public static function getSubscribedEvents() { return array('response' => array('onResponse', -255)); } } |
Что бы сделать наш фреймворк действительно гибким, не стесняйтесь добавлять больше событий, а что бы сделать его еще круче, добавляйте больше подписчиков. Конечно, cерия статей не про создание фреймворка на все случаи жизни, однако это поможет подогнать его под ваши нужды.
Ну и как обычно, вы можете остановиться здесь, или улучшать наш фреймворк дальше.
К содержанию >>
Оригинал статьи на английском языке >>
Исходный код из статьи >>