Symfony2, високопроизводителна PHP рамка, използва шаблон Dependency Injection Container, където компонентите осигуряват интерфейс за инжектиране на зависимости за DI-контейнера. Това позволява на всеки компонент да не се интересува от други зависимости. Класът ‘Kernel’ инициализира DI-контейнера и го инжектира в различни компоненти. Но това означава, че DI-контейнерът може да се използва като Service Locator.
Symfony2 дори има класа ‘ContainerAware’ за това. Мнозина поддържат мнението, че Service Locator е анти-модел в Symfony2. Лично аз не съм съгласен. Това е по-опростен модел в сравнение с DI и е добър за прости проекти. Но моделът Service Locator и DI-контейнерът, комбинирани в един проект, определено са анти-шаблон.
В тази статия ще се опитаме да изградим приложение на Symfony2, без да прилагаме модел на Service Locator. Ще следваме едно просто правило: само конструкторът на DI-контейнери може да знае за DI-контейнера.
В модела за инжектиране на зависимости DI-контейнерът дефинира зависимости на услугите и услугите могат да дадат само интерфейс за инжектиране. Има много статии за Инжектиране на зависимост , и вероятно сте прочели всички. Така че нека не се фокусираме върху теорията и просто да разгледаме основната идея. DI може да бъде от 3 вида:
В Symfony структурата на инжектиране може да бъде дефинирана с помощта на прости конфигурационни файлове. Ето как могат да бъдат конфигурирани тези 3 вида инжекции:
services: my_service: class: MyClass constructor_injection_service: class: SomeClass1 arguments: ['@my_service'] method_injection_service: class: SomeClass2 calls: - [ setProperty, '@my_service' ] property_injection_service: class: SomeClass3 properties: property: '@my_service'
Нека създадем нашата основна структура на приложението. Докато сме готови, ще инсталираме компонент DI-контейнер на Symfony.
$ mkdir trueDI $ cd trueDI $ composer init $ composer require symfony/dependency-injection $ composer require symfony/config $ composer require symfony/yaml $ mkdir config $ mkdir www $ mkdir src
За да накараме comloser autoloader да намери нашите собствени класове в папката src, можем да добавим свойството ‘autoloader’ във файла composer.json:
{ // ... 'autoload': { 'psr-4': { '': 'src/' } } }
И нека създадем нашия конструктор на контейнери и да забраним инжектирането на контейнери.
// in src/TrueContainer.php use SymfonyComponentDependencyInjectionContainerBuilder; use SymfonyComponentConfigFileLocator; use SymfonyComponentDependencyInjectionLoaderYamlFileLoader; use SymfonyComponentDependencyInjectionContainerInterface; class TrueContainer extends ContainerBuilder { public static function buildContainer($rootPath) { $container = new self(); $container->setParameter('app_root', $rootPath); $loader = new YamlFileLoader( $container, new FileLocator($rootPath . '/config') ); $loader->load('services.yml'); $container->compile(); return $container; } public function get( $id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE ) { if (strtolower($id) == 'service_container') { if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior ) { return; } throw new InvalidArgumentException( 'The service definition 'service_container' does not exist.' ); } return parent::get($id, $invalidBehavior); } }
Тук използваме компонентите Config и Yaml symfony. Подробности можете да намерите в официалната документация тук . Също така дефинирахме параметъра за root път ‘app_root’ за всеки случай. Методът get претоварва поведението по подразбиране на родителския клас и предотвратява връщането на контейнера „service_container“.
След това се нуждаем от входна точка за приложението.
// in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__));
Този е предназначен за обработка на http заявки. Можем да имаме повече входни точки за конзолни команди, cron задачи и други. Всяка входна точка трябва да получи определени услуги и трябва да знае за структурата на DI-контейнера. Това е единственото място, където можем да поискаме услуги от контейнера. От този момент ще се опитаме да изградим това приложение само с помощта на конфигурационни файлове на DI-контейнер.
HttpKernel (не ядрото на рамката с проблема с локатора на услуги) ще бъде нашият основен компонент за уеб частта на приложението. Ето типичен работен процес на HttpKernel:
Зелените квадрати са събития.
HttpKernel използва компонента HttpFoundation за обекти за заявка и отговор и компонент EventDispatcher за системата на събитията. Няма проблеми при инициализирането им с конфигурационни файлове на DI-контейнер. HttpKernel трябва да бъде инициализиран с EventDispatcher, ControllerResolver и по желание с RequestStack (за подзаявки) услуги.
Ето конфигурацията на контейнера за него:
# in config/events.yml services: dispatcher: class: SymfonyComponentEventDispatcherEventDispatcher
# in config/kernel.yml services: request: class: SymfonyComponentHttpFoundationRequest factory: [ SymfonyComponentHttpFoundationRequest, createFromGlobals ] request_stack: class: SymfonyComponentHttpFoundationRequestStack resolver: class: SymfonyComponentHttpKernelControllerControllerResolver http_kernel: class: SymfonyComponentHttpKernelHttpKernel arguments: ['@dispatcher', '@resolver', '@request_stack']
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' }
Както можете да видите, ние използваме свойството ‘factory’, за да създадем услугата за заявки. Услугата HttpKernel получава само обект Request и връща обект Response. Може да се направи в предния контролер.
// in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__)); $HTTPKernel = $container->get('http_kernel'); $request = $container->get('request'); $response = $HTTPKernel->handle($request); $response->send();
Или отговорът може да бъде дефиниран като услуга в конфигурацията, като се използва свойството ‘factory’.
какво е прототипирането в дизайна
# in config/kernel.yml # ... response: class: SymfonyComponentHttpFoundationResponse factory: [ '@http_kernel', handle] arguments: ['@request']
И тогава просто го вкарваме в предния контролер.
// in www/index.php require_once('../vendor/autoload.php'); $container = TrueContainer::buildContainer(dirname(__DIR__)); $response = $container->get('response'); $response->send();
Услугата за преобразуване на контролер получава свойството ‘_controller’ от атрибутите на услугата Request за разрешаване на контролера. Тези атрибути могат да бъдат дефинирани в конфигурацията на контейнера, но изглежда малко по-сложно, защото трябва да използваме обект ParameterBag вместо обикновен масив.
# in config/kernel.yml # ... request_attributes: class: SymfonyComponentHttpFoundationParameterBag calls: - [ set, [ _controller, AppControllerDefaultController::defaultAction ]] request: class: SymfonyComponentHttpFoundationRequest factory: [ SymfonyComponentHttpFoundationRequest, createFromGlobals ] properties: attributes: '@request_attributes' # ...
И тук е класът DefaultController с метода defaultAction.
// in src/App/Controller/DefaultController.php namespace AppController; use SymfonyComponentHttpFoundationResponse; class DefaultController { function defaultAction() { return new Response('Hello cruel world'); } }
С всички тези на място трябва да имаме работещо приложение.
Този контролер е доста безполезен, тъй като няма достъп до никоя услуга. В рамката на Symfony този проблем се решава чрез инжектиране на DI-контейнер в контролер и използването му като локатор на услуги. Няма да го направим. Така че нека да определим контролера като услуга и да инжектираме услугата за заявки в него. Ето конфигурацията:
# in config/controllers.yml services: controller.default: class: AppControllerDefaultController arguments: [ '@request']
# in config/kernel.yml # ... request_attributes: class: SymfonyComponentHttpFoundationParameterBag calls: - [ set, [ _controller, ['@controller.default', defaultAction ]]] request: class: SymfonyComponentHttpFoundationRequest factory: [ SymfonyComponentHttpFoundationRequest, createFromGlobals ] properties: attributes: '@request_attributes' # ...
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' }
И кодът на контролера:
// in src/App/Controller/DefaultController.php namespace AppController; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpFoundationResponse; class DefaultController { /** @var Request */ protected $request; function __construct(Request $request) { $this->request = $request; } function defaultAction() { $name = $this->request->get('name'); return new Response('Hello $name'); } }
Сега контролерът има достъп до услугата за заявки. Както можете да видите, тази схема има кръгови зависимости. Той работи, защото DI-контейнерът споделя услугата след създаването и преди инжектирането на метод и свойство. Така че, когато услугата на контролера се създава, услугата за заявки вече съществува.
Ето как работи:
как да изградя wordpress плъгин
Но това работи само защото първо се създава услугата за заявки. Когато получим услуга за отговор в предния контролер, услугата за заявки е първата инициализирана зависимост. Ако се опитаме първо да получим услугата на контролера, това ще доведе до грешка в кръгова зависимост. Тя може да бъде фиксирана чрез използване на метод или инжектиране на свойства.
Но има и друг проблем. DI-контейнерът ще инициализира всеки контролер със зависимости. Така че ще инициализира всички съществуващи услуги, дори ако не са необходими. За щастие контейнерът има ленива функционалност за зареждане. DI-компонентът на Symfony използва „ocramius / proxy-manager“ за прокси класове. Трябва да инсталираме мост между тях.
$ composer require symfony/proxy-manager-bridge
И го дефинирайте на етапа на изграждане на контейнера:
// in src/TrueContainer.php //... use SymfonyBridgeProxyManagerLazyProxyInstantiatorRuntimeInstantiator; // ... $container = new self(); $container->setProxyInstantiator(new RuntimeInstantiator()); // ...
Сега можем да определим мързеливи услуги.
# in config/controllers.yml services: controller.default: lazy: true class: AppControllerDefaultController arguments: [ '@request' ]
Така че контролерите ще предизвикат инициализация на зависими услуги само когато е извикан действителен метод. Също така, той избягва грешка в кръговата зависимост, защото услугата на контролера ще бъде споделена преди действителната инициализация; въпреки че все още трябва да избягваме циркулярни препратки. В този случай не трябва да инжектираме услугата на контролера в услугата за заявки или услугата за заявки в услугата на контролера. Очевидно се нуждаем от услуга за заявки в контролери, така че нека избягваме инжектиране в услугата за заявки на етапа на иницииране на контейнера. HttpKernel има система за събития за тази цел.
Очевидно искаме да имаме различни контролери за различни заявки. Така че се нуждаем от система за маршрутизиране. Нека инсталираме компонента за маршрутизиране на symfony.
$ composer require symfony/routing
Маршрутизиращият компонент има клас Router, който може да използва конфигурационни файлове за маршрутизация. Но тези конфигурации са само параметри ключ-стойност за класа Route. Рамката Symfony използва свой собствен преобразувател на контролер от FrameworkBundle, който инжектира контейнер в контролери с интерфейса ‘ContainerAware’. Точно това се опитваме да избегнем. Резолюторът на контролер HttpKernel връща обект на клас, както е, ако вече съществува в атрибута ‘_controller’ като масив с обект на контролер и метод на действие (всъщност преобразувателят на контролера ще го върне както е, ако е просто масив). Затова трябва да определим всеки маршрут като услуга и да инжектираме контролер в него. Нека добавим друга услуга на контролер, за да видим как работи.
# in config/controllers.yml # ... controller.page: lazy: true class: AppControllerPageController arguments: [ '@request']
// in src/App/Controller/PageController.php namespace AppController; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpFoundationResponse; class PageController { /** @var Request */ protected $request; function __construct(Request $request) { $this->request = $request; } function defaultAction($id) { return new Response('Page $id doesn’t exist'); } }
Компонентът HttpKernel има клас RouteListener, който използва събитие ‘kernel.request’. Ето една възможна конфигурация с мързеливи контролери:
# in config/routes/default.yml services: route.home: class: SymfonyComponentRoutingRoute arguments: path: / defaults: _controller: ['@controller.default', 'defaultAction'] route.page: class: SymfonyComponentRoutingRoute arguments: path: /page/{id} defaults: _controller: ['@controller.page', 'defaultAction']
# in config/routing.yml imports: - { resource: ’routes/default.yml' } services: route.collection: class: SymfonyComponentRoutingRouteCollection calls: - [ add, ['route_home', '@route.home'] ] - [ add, ['route_page', '@route.page'] ] router.request_context: class: SymfonyComponentRoutingRequestContext calls: - [ fromRequest, ['@request'] ] router.matcher: class: SymfonyComponentRoutingMatcherUrlMatcher arguments: [ '@route.collection', '@router.request_context' ] router.listener: class: SymfonyComponentHttpKernelEventListenerRouterListener arguments: matcher: '@router.matcher' request_stack: '@request_stack' context: '@router.request_context'
# in config/events.yml service: dispatcher: class: SymfonyComponentEventDispatcherEventDispatcher calls: - [ addSubscriber, ['@router.listener']]
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' } - { resource: 'routing.yml' }
Също така се нуждаем от генератор на URL в нашето приложение. Ето го:
# in config/routing.yml # ... router.generator: class: SymfonyComponentRoutingGeneratorUrlGenerator arguments: routes: '@route.collection' context: '@router.request_context'
Генераторът на URL адреси може да се инжектира в контролер и да предоставя услуги. Сега имаме основно приложение. Всяка друга услуга може да бъде дефинирана по същия начин, като конфигурационният файл се инжектира в определени контролери или диспечер на събития. Например, ето някои конфигурации за Twig и Doctrine.
Twig е механизмът за шаблони по подразбиране в рамката на Symfony2. Много компоненти на Symfony2 могат да го използват без никакви адаптери. Така че това е очевиден избор за нашето приложение.
$ composer require twig/twig $ mkdir src/App/View
# in config/twig.yml services: templating.twig_loader: class: Twig_Loader_Filesystem arguments: [ '%app_root%/src/App/View' ] templating.twig: class: Twig_Environment arguments: [ '@templating.twig_loader' ]
Доктрината е ORM, използвана в рамката на Symfony2. Можем да използваме всякакви други ORM, но компонентите на Symfony2 вече могат да използват много функции на Docrine.
$ composer require doctrine/orm $ mkdir src/App/Entity
# in config/doctrine.yml parameters: doctrine.driver: 'pdo_pgsql' doctrine.user: 'postgres' doctrine.password: 'postgres' doctrine.dbname: 'true_di' doctrine.paths: ['%app_root%/src/App/Entity'] doctrine.is_dev: true services: doctrine.config: class: DoctrineORMConfiguration factory: [ DoctrineORMToolsSetup, createAnnotationMetadataConfiguration ] arguments: paths: '%doctrine.paths%' isDevMode: '%doctrine.is_dev%' doctrine.entity_manager: class: DoctrineORMEntityManager factory: [ DoctrineORMEntityManager, create ] arguments: conn: driver: '%doctrine.driver%' user: '%doctrine.user%' password: '%doctrine.password%' dbname: '%doctrine.dbname%' config: '@doctrine.config'
#in config/services.yml imports: - { resource: 'events.yml' } - { resource: 'kernel.yml' } - { resource: 'controllers.yml' } - { resource: 'routing.yml' } - { resource: 'twig.yml' } - { resource: 'doctrine.yml' }
Също така можем да използваме конфигурационни файлове за картографиране на YML и XML вместо анотации. Просто трябва да използваме методите ‘createYAMLMetadataConfiguration’ и ‘createXMLMetadataConfiguration’ и да зададем път към папка с тези конфигурационни файлове.
Бързо може да стане много досадно да инжектирате всяка необходима услуга във всеки контролер поотделно. За да стане малко по-добър компонентът DI-контейнер има абстрактни услуги и наследяване на услуги. Така че можем да дефинираме някои абстрактни контролери:
# in config/controllers.yml services: controller.base_web: lazy: true abstract: true class: AppControllerBaseWebController arguments: request: '@request' templating: '@templating.twig' entityManager: '@doctrine.entity_manager' urlGenerator: '@router.generator' controller.default: class: AppControllerDefaultController parent: controller.base_web controller.page: class: AppControllerPageController parent: controller.base_web
// in src/App/Controller/Base/WebController.php namespace AppControllerBase; use SymfonyComponentHttpFoundationRequest; use Twig_Environment; use DoctrineORMEntityManager; use SymfonyComponentRoutingGeneratorUrlGenerator; abstract class WebController { /** @var Request */ protected $request; /** @var Twig_Environment */ protected $templating; /** @var EntityManager */ protected $entityManager; /** @var UrlGenerator */ protected $urlGenerator; function __construct( Request $request, Twig_Environment $templating, EntityManager $entityManager, UrlGenerator $urlGenerator ) { $this->request = $request; $this->templating = $templating; $this->entityManager = $entityManager; $this->urlGenerator = $urlGenerator; } } // in src/App/Controller/DefaultController // … class DefaultController extend WebController { // ... } // in src/App/Controller/PageController // … class PageController extend WebController { // ... }
Има много други полезни компоненти на Symfony като Form, Command и Assets. Те са разработени като независими компоненти, така че интегрирането им с помощта на DI-контейнер не би трябвало да представлява проблем.
DI-контейнерът също има система за етикети. Етикетите могат да бъдат обработвани от класовете на Compiler Pass. Компонентът Dispatcher на събитията има свой собствен компилаторен пропуск, за да опрости абонамента за слушатели на събития, но използва клас ContainerAwareEventDispatcher вместо клас EventDispatcher. Така че не можем да го използваме. Но ние можем да приложим наши собствени компилаторски пропуски за събития, маршрути, сигурност и всякакви други цели.
Например, нека внедрим тагове за системата за маршрутизиране. Сега, за да дефинираме маршрут, трябва да дефинираме услуга за маршрут в конфигурационен файл за маршрут в папката config / routes и след това да го добавим към услугата за събиране на маршрути във файла config / routing.yml. Изглежда несъвместимо, защото дефинираме параметрите на рутера на едно място, а името на рутера на друго място.
Със системата от тагове можем просто да дефинираме име на маршрут в етикет и да добавим тази услуга за маршрут към колекцията от маршрути, като използваме име на маркер.
Компонентът DI-контейнер използва класове за пропускане на компилатора, за да направи каквато и да е модификация на конфигурацията на контейнера преди действителната инициализация. Така че нека внедрим нашия клас на компилатор за система за маркери на рутера.
// in src/CompilerPass/RouterTagCompilerPass.php namespace CompilerPass; use SymfonyComponentDependencyInjectionCompilerCompilerPassInterface; use SymfonyComponentDependencyInjectionContainerBuilder; use SymfonyComponentDependencyInjectionDefinition; use SymfonyComponentDependencyInjectionReference; class RouterTagCompilerPass implements CompilerPassInterface { /** * You can modify the container here before it is dumped to PHP code. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { $routeTags = $container->findTaggedServiceIds('route'); $collectionTags = $container->findTaggedServiceIds('route_collection'); /** @var Definition[] $routeCollections */ $routeCollections = array(); foreach ($collectionTags as $serviceName => $tagData) $routeCollections[] = $container->getDefinition($serviceName); foreach ($routeTags as $routeServiceName => $tagData) { $routeNames = array(); foreach ($tagData as $tag) if (isset($tag['route_name'])) $routeNames[] = $tag['route_name']; if (!$routeNames) continue; $routeReference = new Reference($routeServiceName); foreach ($routeCollections as $collection) foreach ($routeNames as $name) $collection->addMethodCall('add', array($name, $routeReference)); } } }
// in src/TrueContainer.php //... use CompilerPassRouterTagCompilerPass; // ... $container = new self(); $container->addCompilerPass(new RouterTagCompilerPass()); // ...
Сега можем да модифицираме нашата конфигурация:
# in config/routing.yml # … route.collection: class: SymfonyComponentRoutingRouteCollection tags: - { name: route_collection } # ...
# in config/routes/default.yml services: route.home: class: SymfonyComponentRoutingRoute arguments: path: / defaults: _controller: ['@controller.default', 'defaultAction'] tags: - { name: route, route_name: 'route_home' } route.page: class: SymfonyComponentRoutingRoute arguments: path: /page/{id} defaults: _controller: ['@controller.page', 'defaultAction'] tags: - { name: route, route_name: 'route_page' }
Както можете да видите, получаваме колекции от маршрути по име на маркера, вместо по име на услуга, така че нашата система от маркери на маршрути не зависи от действителната конфигурация. Също така маршрутите могат да се добавят към всяка услуга за събиране с метод „добавяне“. Компилаторите могат значително да опростят конфигурациите на зависимостите. Но те могат да добавят неочаквано поведение към DI-контейнера, така че е по-добре да не модифицирате съществуваща логика като промяна на аргументи, извиквания на методи или имена на класове. Просто добавете нов над съществуващ, както направихме с помощта на тагове.
Сега имаме приложение, което използва само шаблон на DI контейнер и е изградено с помощта само на конфигурационни файлове на DI-контейнер. Както виждате, няма сериозни предизвикателства в изграждане на приложение на Symfony насам. И можете просто да визуализирате всичките си зависимости от приложенията. Единствената причина, поради която хората използват DI-контейнер като локатор на услуги, е, че концепцията за локатор на услуги е по-лесна за разбиране. И огромна кодова база с DI-контейнер, използван като локатор на услуги, вероятно е следствие от тази причина.
Можете да намерите изходния код на това приложение на GitHub .