В предыдущей статье цикла, мы убрали весь код из класса Framework, благодаря тому, что этот класс теперь наследуется от 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 // framework/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 // framework/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(); |
Такая краткость дает нам возможность использовать более одного фронт-контроллера для приложения. Зачем? Ну например, что бы иметь различные конфигурации для разработки и production. В разработческом окружении вам может потребоваться отображение ошибок в браузере.
1 2 |
ini_set('display_errors', 1); error_reporting(-1); |
Но вряд ли вы захотите включить эту настройку на боевом серваке. В общем, два разных фронт-контроллера дают вам возможность немного варьировать конфигурацию приложения для разного окружения.
Однако, перенос кода из фронт-контроллера в класс фреймворка несет в себе несколько проблем:
- У нас больше нет возможности создавать своих слушателей, ведь объект EventDispatcher теперь не доступен вне фреймворка (Вообще эту проблему можно обойти, добавив метод Framework::getEventDispatcher()).
- Теперь мы не можем использовать свою реализацию UrlMatcher или ControllerResolver
- Исходя из предыдущего пункта, нам стало сложнее тестировать наш фреймворк, т.к. мы больше не можем заменить зависимости заглушками.
- Мы не можем изменить кодировку, которая передается в ResponseListener (Эту проблему можно было бы обойти, передавая кодировку параметром конструктора).
В нашем прошлом коде таких проблем не было, потому что мы использовали внедрение зависимостей. Все зависимости наших объектов передавались в их конструкторах (например диспетчер, благодаря чему мы имели полный контроль над его созданием и настройкой).
Значит ли это, что мы должны жертвовать гибкостью, кастомизацией и простотой тестирования ради возможности не копипастить кучу кода при добавлении нового фронт-контроллера? Конечно не значит! Решить эти и многие другие проблемы нам поможет Symfony2 dependency injection container.
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/" } } } |
Создайте новый файл для описания конфигурации контейнера:
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 // framework/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; |
Задача файла — описать ваши объекты и их зависимости. На этом этапе ни один объект не создается. Здесь просто статическое описание нужных вам объектов и способа их создания. Объекты будут созданы по-требованию, когда вы обратитесь к ним через контейнер, либо когда они потребуются контейнеру для создания других объектов.
Как видите, все объекты связанны именами — уникальными строками, которые идентифицируют каждый из них. Имена дают нам возможность получить объект и сослаться на него при определении другого объекта.
Осталось собрать все это во фронт-контроллере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php // framework/web/front.php require_once __DIR__.'/../vendor/.composer/autoload.php'; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\DependencyInjection\Reference; $routes = include __DIR__.'/../src/app.php'; $sc = include __DIR__.'/../src/container.php'; $request = Request::createFromGlobals(); $response = $sc->get('framework')->handle($request); $response->send(); |
Так как созданием всех объектов теперь управляет контейнер внедрения зависимостей, код фреймворка снова стал предельно прост:
1 2 3 4 5 6 7 8 9 10 11 |
<?php // framework/src/Simplex/Framework.php namespace Simplex; use Symfony\Component\HttpKernel\HttpKernel; class Framework extends HttpKernel { } |
А так можно добавить нового слушателя во фронт-контроллер:
1 2 3 4 |
$sc->register('listener.string_response', 'Simplex\StringResponseListener'); $sc->getDefinition('dispatcher') ->addMethodCall('addSubscriber', array(new Reference('listener.string_response'))) ; |
Контейнер можно конфигурировать с помощью параметров. Давайте добавим параметр, определяющий работаем ли мы в режиме отладки:
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%')) ; |
Теперь, перед тем как использовать этот объект, нужно задать параметр 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 и многое-многое другое.
Это (наконец-то) последняя статья моего цикла про создание фреймворка на компонентах Symfony2. К сожалению, множество тем остались не рассмотренными, но надеюсь, что дал вам достаточно информации для самостоятельной работы, и вы сможете лучше понять, как устроен фреймворк Symfony2.
К содержанию >>
Оригинал статьи на английском языке >>
Исходный код из статьи >>