В последней части моей серии, мы опустошили класс Simplex\\Framework расширив его класс из Symfony HttpKernel. Смотря на этот пустой класс, вы возможно хотели бы перенести код из фронт-контроллера в него:
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 |
<?php // example.com/src/Simplex/Framework.php namespace Simplex; use Symfony\Component\Routing; use Symfony\Component\HttpKernel; use Symfony\Component\EventDispatcher\EventDispatcher; class Framework extends HttpKernel\HttpKernel { public function __construct($routes) { $context = new Routing\RequestContext(); $matcher = new Routing\Matcher\UrlMatcher($routes, $context); $resolver = new HttpKernel\Controller\ControllerResolver(); $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher)); $dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8')); parent::__construct($dispatcher, $resolver); } } |
Фронт-контроллер стал бы более лаконичен:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php // example.com/web/front.php require_once __DIR__.'/../vendor/.composer/autoload.php'; use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $routes = include __DIR__.'/../src/app.php'; $framework = new Simplex\Framework($routes); $framework->handle($request)->send(); |
Имея более лаконичный фронт-контроллер означает, что вы можете использовать более, чем для одного приложения. Почему это могло быть полезным? Это позволило бы вам иметь различные настройки для среды разработки и боевого окружения. В окружении разработки, вы, возможно, хотели бы видеть отчет об ошибках и чтобы они отображались в браузере, для упрощения отладки:
1 2 |
ini_set('display_errors', 1); error_reporting(-1); |
… но в тоже время вы бы не хотели переносить эту конфигурацию на боевое окружение. Имея 2 различный фронт-контроллера дает вам возможность иметь слегка различные конфигурации каждого из них.
Что ж, переместим код из фронт-контроллера в наш фреймворк, сделая фреймворк более настраиваемым, но в тоже время, это добавит нам парочку проблем:
- Мы не сможем зарегистрировать кастомного слушателя, т.к. диспетчер не доступен вне класса фреймворка (простым решением может быть предоставления метода Framework::getEventDispatcher());
- Мы потерям нашу гибкость; мы не сможем изменить реализации UrlMatcher или ControllerResolver;
- По сравнению с предыдущей версией, мы не сможем тестировать наш фреймворк также легко;
- Мы не сможем изменить кодировку передаваему ResponseListener (временным решением может стать передача в конструктор в виде аргумента).
Предыдущий код не имел тех же проблем, потому что мы использовали “внедрение зависимостей”; все зависимости нашего фрейморка были внедрены в своих конструкторах (например, диспетчер событий был внедрен во фреймворк, таким образом мы имели полный контроль на его созданием и настройкой).
Означает ли это что бы должка выбирать между гибкостью, настраиваемостью, легким тестированием и возможностью не копи-пастить часть кода из фронт-контроллера в каждом приложении? Как вы могли догадываться, есть решение. Мы можем решить все эти проблему, более того несколько других, используя Dependency Injection Container DIC (контейнер внедрений зависимостей):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "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.*", "symfony/dependency-injection": "2.1.*" }, "autoload": { "psr-0": { "Simplex": "src/", "Calendar": "src/" } } } |
Создадим новый файл для размещения конфигурации DIC:
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 |
<?php // example.com/src/container.php use Symfony\Component\DependencyInjection; use Symfony\Component\DependencyInjection\Reference; $sc = new DependencyInjection\ContainerBuilder(); $sc->register('context', 'Symfony\Component\Routing\RequestContext'); $sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher') ->setArguments(array($routes, new Reference('context'))) ; $sc->register('resolver', 'Symfony\Component\HttpKernel\Controller\ControllerResolver'); $sc->register('listener.router', 'Symfony\Component\HttpKernel\EventListener\RouterListener') ->setArguments(array(new Reference('matcher'))) ; $sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener') ->setArguments(array('UTF-8')) ; $sc->register('listener.exception', 'Symfony\Component\HttpKernel\EventListener\ExceptionListener') ->setArguments(array('Calendar\\Controller\\ErrorController::exceptionAction')) ; $sc->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher') ->addMethodCall('addSubscriber', array(new Reference('listener.router'))) ->addMethodCall('addSubscriber', array(new Reference('listener.response'))) ->addMethodCall('addSubscriber', array(new Reference('listener.exception'))) ; $sc->register('framework', 'Simplex\Framework') ->setArguments(array(new Reference('dispatcher'), new Reference('resolver'))) ; return $sc; |
Задача этого файла – настройка зависимостей ваших объектов. Не создается ни один объект на этом этапе конфигурации. Это чисто статическое описание объектов, которыми вы манипулируете и создаете. Объекты будут созданы по требованию, когда вы вызовите его из контейнера или когда контейнеру понадобиться создать какой-либо объект.
Например, чтобы создать слушателя маршрутов, мы говорим Symfony что имя класса Symfony\Component\HttpKernel\EventListener\RouterListeners, и что его конструктор принимает объект new Reference(‘matcher’). Как вы могли заметить, каждый объект имеет уникальное имя. Именя позволяют нам получать объекты и ссылать на них в других определения объектах.
По умолчанию, каждый раз когда вы получаете объект из контейнера, он возвращает тот же самый экземпляр. Это потому что объект управляет своими “глобальными” объектами.
Теперь фронт-контроллер выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php // example.com/web/front.php require_once __DIR__.'/../vendor/.composer/autoload.php'; use Symfony\Component\HttpFoundation\Request; $routes = include __DIR__.'/../src/app.php'; $sc = include __DIR__.'/../src/container.php'; $request = Request::createFromGlobals(); $response = $sc->get('framework')->handle($request); $response->send(); |
Т.к. теперь все объекты используют DIC, фреймворк может вернуться к предыдущей реализации:
1 2 3 4 5 6 7 8 9 10 11 |
<?php // example.com/src/Simplex/Framework.php namespace Simplex; use Symfony\Component\HttpKernel\HttpKernel; class Framework extends HttpKernel { } |
Если вы хотите использовать легковестный контейнер, например Pimple, простой DIC в 60 строк кода.
(60 строк кода было в 1 версии, сейчас Pimple уже дорос до 3, но объем кода практически не изменился).
Теперь, как мы регестрируем кастомных слушателей во фронт-контроллере:
1 2 3 4 |
$sc->register('listener.string_response', 'Simplex\StringResponseListener'); $sc->getDefinition('dispatcher') ->addMethodCall('addSubscriber', array(new Reference('listener.string_response'))) ; |
Кроме описания ваших объектов, DIC можно также настроить используя параметры. Давайте создадим, который так и делаем, если мы находимся в режиме отладки:
1 2 3 |
$sc->setParameter('debug', true); echo $sc->getParameter('debug'); |
Этот параметр, может быть использован при определении объекта. Давайте создадим настройку кодировки:
1 2 3 |
$sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener') ->setArguments(array('%charset%')) ; |
После этого изменения, вы должны установить кодировку до использования слушателя ответа:
1 |
$sc->setParameter('charset', 'UTF-8'); |
Вместо того, чтобы полагаться на соглашения как определять переменную $routes, давайте используем еще раз параметры:
1 2 3 |
$sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher') ->setArguments(array('%routes%', new Reference('context'))) ; |
Это относиться и к фронт-контроллеру:
1 |
$sc->setParameter('routes', include __DIR__.'/../src/app.php'); |
Мы очевидно только слегка прикоснулись к тому на что способен контейнер: от имени классов в качестве параметров, до переопределиния существующих классов, от поддержки возможности для демпинга контейнер до простого класса PHP, и т.д.
We have obviously barely scratched the surface of what you can do with the container: from class names as parameters, to overriding existing object definitions, from scope support to dumping a container to a plain PHP class, and much more.
DIC это крутая штука, которая может управлять любыми PHP классами.
Не говорите мне, что вы не хотите использовать DIC в вашем фреймворке. Если вы не любите его, не используйте. Это ваш фреймворк, не мой.
Это последняя часть моей серии статей о создании фреймворка используя компоненты Symfony2. Я знаю что многое осталось не рассмотренным, но я надеюсь это дало вам достаточно информации для начала и лучшего понимания, что находиться у Symfony2 под капотом.
Если вы хотете разобраться глубже, я настоятельно рекомендую вам посмотреть исходник мини-фреймворка Silex, особенно класс Application.
Удачи!