Статьи

Понимание конфигурации и контейнера службы Symfony Bundle

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

Все примеры конфигурации в этом посте находятся в YAML. Symfony также поддерживает другие форматы конфигурации (массивы XML и PHP), и они являются допустимыми параметрами. Я привык работать с YAML, потому что я думаю, что он более читабелен, чем XML, но вы получаете преимущество проверки схемы при использовании XML. Выбор формата конфигурации зависит от вас (или вашей проектной команды), и здесь нет правильного или неправильного варианта. Просто используйте тот, который вам удобнее всего.

Создание пакета

Bundle — это каталог, содержащий набор файлов (файлы PHP, таблицы стилей, скрипты Java, изображения и т. Д.), Которые реализуют одну функцию (блог, форум и т. Д.). В Symfony2 (почти) все живет внутри пакета.

или, другими словами, из документов

Пакет похож на плагин в другом программном обеспечении, но даже лучше. Основное различие заключается в том, что в Symfony2 все является пакетом, включающим в себя как базовую функциональность платформы, так и код, написанный для вашего приложения. Пакеты являются первоклассными гражданами в Symfony2. Это дает вам гибкость в использовании встроенных функций, упакованных в сторонние пакеты, или для распространения собственных пакетов. Это позволяет легко выбирать, какие функции включить в вашем приложении, и оптимизировать их так, как вы хотите.

Когда вы создаете новый пакет, либо путем его автоматической генерации ( php app/console generate:bundle --namespace=Acme/TestBundle ), либо вручную, вам необходим файл BundleNameBundle.php в корневом каталоге пакета. Класс в этом файле, хотя и в основном пустой, расширяет класс Symfony ядра Symfony\Component\HttpKernel\Bundle\Bundle и это то, что вам необходимо зарегистрировать в AppKernel registerBundles . Когда ядро ​​загружается, оно создает экземпляры каждого пакета и загружает расширение контейнера каждого пакета (используя методы в родительском классе Bundle ). Расширением контейнера является класс в файле BundleNameExtension.php внутри папки DependencyInjection пакета. Класс расширения контейнера загружает и управляет конфигурацией пакета. Весь класс расширения является необязательным, и комплект будет работать без него, но полезно знать, как это работает, чтобы лучше понять систему конфигурации.

Загрузка конфигурации пакета, простой способ

Как я уже говорил выше, класс расширения является необязательным, и существуют другие способы настройки комплекта. Самая простая форма конфигурации пакета — это настройка параметров и сервисов в основном файле конфигурации app/config/config.yml ( app/config/config.yml ). Это вполне допустимый вариант, хотя вам необходимо понять последствия этого, чтобы лучше понять, когда это действительно уместно. Наличие конфигурации пакета в главном файле конфигурации делает ваш пакет тесно связанным с текущим приложением и, следовательно, не очень переносимым. Это может быть хорошо, если пакет не имеет много служб или параметров, и вы знаете, что он будет использоваться только в текущем приложении. Тем не менее, я действительно не рекомендую использовать этот подход даже тогда, потому что вещи имеют тенденцию меняться, и в дальнейшем может потребоваться дополнительная конфигурация, и другие разработчики, вероятно, будут искать конфигурацию внутри пакета.

Еще один простой способ — создать отдельный файл конфигурации внутри пакета (например, Resources/config/services.yml ) и импортировать его в основную конфигурацию. В начале config.yml обычно уже есть несколько импортов, поэтому все, что вам нужно сделать, это добавить еще один импорт в список, который указывает на ваш файл конфигурации комплекта. Обратите внимание, что путь относительно основного файла конфигурации. Вот небольшой пример:

 imports : 
     -   {  resource :  parameters . yml } 
     -   {  resource :  security . yml } 
     -   {  resource :   ../../ src / Cvuorinen / ExampleBundle / Resources / config / services . yml } 

Загрузка конфигурации пакета, семантический способ

Как отмечено выше, класс расширения обрабатывает загрузку конфигурации пакета. Он создает экземпляр класса Configuration из файла Configuration.php также расположенного в папке DependencyInjection . Класс Configuration используется для проверки и обработки основной конфигурации (из файлов конфигурации в app/config/ ), связанной с комплектом. Если в основной конфигурации нет конфигурации, связанной с вашим комплектом, класс Configuration можно оставить как есть (подробнее об этом после следующего абзаца).

После обработки основной конфигурации класс расширения загружает конкретную конфигурацию пакета из папки Resources/config пакета, используя загрузчик, соответствующий типу файла конфигурации ( YamlFileLoader при использовании YAML). Любые службы, определенные в файле конфигурации пакетов, могут использовать параметры из основного файла конфигурации приложений (и глобальные параметры ) и будут добавлены в контейнер служб . После этого сервисы становятся доступны вашим контроллерам, командам CLI или любой другой части приложения, «осведомленной о контейнерах». Вам просто нужно вызвать метод get контейнера с именем сервиса, указанным в конфигурации сервиса.

Реальная сила семантической конфигурации заключается в том, что она может использоваться для обработки, объединения и проверки параметров конфигурации из основной конфигурации приложения и передачи их в конфигурацию службы связок. Это особенно полезно при создании пакетов, предназначенных для более широкой аудитории, таких как пакеты с открытым исходным кодом, но также может быть полезно для внутреннего распространения внутри компаний. Таким образом, вы можете предоставить несколько простых и хорошо задокументированных опций конфигурации (со значениями по умолчанию, где это необходимо), которые пользователи комплекта могут установить в главном файле конфигурации, а затем класс расширения комплектов может обработать и проверить их так, чтобы пользователь Комплектация не должна касаться какой-либо конфигурации внутри комплекта.

Загрузка конфигурации пакета, правильный путь?

Конечно, нет единственно правильного способа сделать это. Но в большинстве случаев легкий путь — это не тот путь, поскольку вся идея пакетов состоит в том, чтобы сделать их многократно используемыми в различных приложениях. Кроме того, довольно просто автоматически сгенерировать все необходимые файлы для семантической конфигурации с помощью команды Symfony CLI:

 $ app / console generate : bundle 

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

Структура файла конфигурации

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

Параметры — это любые статические значения, которые требуются вашему пакету. Такие вещи, как учетные данные для входа в систему, ключи API, имена хостов или URL-адреса внешних служб, вы, вероятно, поняли. Параметрами могут быть любые скалярные значения (например, строки, числа, логические значения) и массивы, и они указываются в разделе parameters в конфигурации. Также рекомендуется указывать имена классов обслуживания в качестве параметров, поскольку это позволяет расширять пакет и, например, переопределять определенные службы из другого пакета. Вы можете извлечь параметры из контейнера, вызвав метод getParameter с именем параметра в качестве аргумента, но большую часть времени вы будете передавать параметры в качестве аргументов службам в конфигурации.

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

По соглашению, параметры и сервисы имеют пространство имен с именем пакета (в случае змеи ), но это никак не применяется, поэтому вы можете называть их по своему усмотрению. Хотя я бы рекомендовал использовать пространство имен имен пакетов, чтобы избежать конфликтов с другими пакетами.

Вот небольшой пример файла services.yml из примера пакета:

 parameters : cvuorinen_example . greeter . class :   Cvuorinen \ExampleBundle\Service\Greeter cvuorinen_example . greeter . greeting :   "Hello" services : cvuorinen_example . greeter : 
         class :   % cvuorinen_example . greeter . class % arguments :   [% cvuorinen_example . greeter . greeting %] 

Здесь мы определяем один сервис (называемый cvuorinen_example.greeter ). У нас есть два параметра: первый содержит имя класса службы Greeter и задан в качестве параметра class службы, а другой — строковое значение, которое будет передано службе в качестве аргумента конструктора с использованием параметра arguments Массив сервиса.

Как видите, на указанные параметры можно ссылаться в конфигурации, заключив их в% -символы. Вы также можете ссылаться на параметры из основной конфигурации, а также из других пакетов (хотя имейте в виду, что это снова делает ваш пакет связанным с другим пакетом). Вы можете прочитать больше о параметрах в документации Symfony .

Вы можете проверить и отладить свою конфигурацию с помощью команды CLI:

 $ app / console container : debug 

Это создаст и объединит конфигурации пакета и распечатает все зарегистрированные сервисы, или сообщение об ошибке в случае, если что-то не так с конфигурацией. Существует также опция --parameters чтобы распечатать все параметры.

Собираем все вместе

Теперь, когда у нас есть рабочая конфигурация сервиса, давайте посмотрим, как это можно использовать в контроллере.

Во-первых, вот наш очень простой сервис Greeter:

 namespace   Cvuorinen \ExampleBundle\Service ; 

 class   Greeter 
 { 
     public   function  __construct ( $greeting ) 
     { $this -> greeting =  $greeting ; 
     } 

     public   function  greet ( $name ) 
     { 
         return  $this -> greeting .   ' '   .  $name ; 
     } 
 } 

В этом посте я остановлюсь на конфигурации службы, поэтому я не буду подробно описывать конфигурацию маршрутизатора. Вы можете прочитать об этом в официальной документации, если вы с ней не знакомы. Но предположим, что у нас есть маршрут с шаблоном /hello/{name} , который указывает на DefaultController внутри нашего примера пакета. Тогда наш контроллер может выглядеть примерно так:

 namespace   Cvuorinen \ExampleBundle\Controller ; 

 use   Symfony \Bundle\FrameworkBundle\Controller\Controller ; 
 use   Symfony \Component\HttpFoundation\Response ; 

 class   DefaultController   extends   Controller 
 { 
     public   function  indexAction ( $name ) 
     { $greeter =  $this -> get ( 'cvuorinen_example.greeter' ); 

         return   new   Response ( $greeter -> greet ( $name ) 
         ); 
     } 
 } 

Как видите, в контроллере нет логики, он только извлекает службу из контейнера, вызывает метод этой службы и передает параметр из запроса. Контроллеру не нужно знать, как создать службу Greeter, контейнер внедрения зависимости делает всю работу за него. Это позволяет нам сохранять контроллеры «худыми» и всю бизнес-логику и управление зависимостями в других местах.

Теперь, предположим, что мы хотели изменить приветствие, все, что нам нужно сделать, это изменить параметр внутри файла конфигурации. Это может показаться не таким уж большим делом в этом примере, но в реальном приложении вы, вероятно, будете использовать одни и те же сервисы во многих местах приложения. С этой точки зрения преимущества очевидны.

Как насчет зависимостей !?

Хорошо, если вы обратили внимание, вы, возможно, заметили, что наш пример на самом деле не вводил никаких зависимостей, только простой параметр. Это правда, я хотел сохранить первый пример как можно более простым. А что если нам нужно сделать многоязычное приложение? Нам нужно будет использовать какую-то систему перевода, и сервису Greeter она понадобится для перевода приветствия.

Во-первых, давайте изменим класс обслуживания следующим образом:

 namespace   Cvuorinen \ExampleBundle\Service ; 

 use   Symfony \Component\Translation\TranslatorInterface ; 

 class   Greeter 
 { 
     public   function  __construct ( $greeting ,   TranslatorInterface  $translator ) 
     { $this -> greeting =  $greeting ; $this -> translator =  $translator ; 
     } 

     public   function  greet ( $name ) 
     { 
         return  $this -> translator -> trans ( $this -> greeting )   .   ' '   .  $name ; 
     } 
 } 

Тогда давайте изменим конфигурацию, чтобы внедрить службу переводчика следующим образом:

 parameters : cvuorinen_example . greeter . class :   Cvuorinen \ExampleBundle\Service\Greeter cvuorinen_example . greeter . greeting :   "Hello" services : cvuorinen_example . greeter : 
         class :   % cvuorinen_example . greeter . class % arguments : 
             -   % cvuorinen_example . greeter . greeting % 
             -   @translator 

В остальном это точно так же, как и в предыдущем примере, единственное отличие — в разделе arguments конфигурации службы greeter. Здесь мы добавили еще один аргумент конструктора, на этот раз это другой сервис. На сервисы можно ссылаться в конфигурации, добавив к ним префикс @ -character. Мы используем сервис Symfony Translator по умолчанию, который называется «переводчик». Некоторые базовые службы не имеют пространства имен в своих @cvuorinen_example.translator , но вы также можете добавить собственную службу переводчиков, например, с помощью @cvuorinen_example.translator .

Впрыскивать все вещи

До сих пор я рассмотрел только «базовый» метод инъекции, конструктор инъекций . Служебный контейнер Symfony также можно использовать с инжекцией сеттера и инжекцией свойства .

Внедрение сеттера означает, что у класса обслуживания есть отдельный метод сеттера для зависимости (потому что он может быть необязательным, например). Это можно настроить, добавив параметр calls в конфигурацию службы, которая представляет собой массив вызовов методов, которые будут выполнены после создания службы. Внедрение свойства означает, что класс имеет открытые свойства, и зависимость может быть установлена ​​в него извне. Это может быть достигнуто с помощью параметра properties в конфигурации сервиса. Более подробную информацию о различных типах впрыска можно найти в документации .

Конфигурация службы также может использовать другую службу в качестве фабрики для создания службы. Фабрика может быть другим сервисом внутри вашего собственного пакета, но вы также можете использовать эту функцию с некоторыми существующими сервисами. Одним из примеров является то, что если вашей службе требуется определенный репозиторий Doctrine, вместо внедрения службы диспетчера сущностей вы можете использовать службу диспетчера сущностей в качестве фабрики для определения класса репозитория как службы. Таким образом, вашему классу обслуживания нужно только добавить зависимость к классу репозитория, а не весь менеджер сущностей. Это делает сервис более переносимым и более легко тестируемым, поскольку вам нужно будет только смоделировать класс репозитория.

Услуги также могут быть объявлены частными. Это означает, что они не будут доступны из контейнера с использованием метода get, их можно использовать только в качестве аргументов, введенных в другие службы. Это можно сделать, установив public: false в конфигурации сервиса.

Вот последний пример, который показывает, как мы можем определить хранилище сущностей как сервис, используя менеджер сущностей Doctrine в качестве фабрики, и внедрить его в другой сервис, используя внедрение сеттера . Служба репозитория сущностей также объявляется частной, поэтому она не будет доступна через контейнер.

 parameters : cvuorinen_example . item_repository . class :   Doctrine \ORM\EntityRepository cvuorinen_example . item_repository . entity :   "CvuorinenExampleBundle:Item" cvuorinen_example . items . class :   Cvuorinen \ExampleBundle\Service\Items services : cvuorinen_example . item_repository : 
         class :   % cvuorinen_example . item_repository . class % 
         public :   false factory_service :  doctrine . orm . entity_manager factory_method :  getRepository arguments :   [% cvuorinen_example . item_repository . entity %] cvuorinen_example . items : 
         class :   % cvuorinen_example . items . class % calls : 
             -   [ setRepository ,   [ @cvuorinen_example . item_repository ]] 

Вывод

Это оказалось довольно длинным постом, и в конфигурации пакета Symfony и контейнере внедрения зависимостей есть еще много чего, что я не рассмотрел. Надеюсь, это поможет вам немного лучше понять систему комплектов Symfony и побудит вас больше использовать внедрение зависимостей. Я надеюсь затронуть более сложные темы в следующих статьях, таких как переопределение конфигурации пакета (наследование пакета), совместное использование параметров между пакетами и определение контроллеров как сервисов.

А пока вы можете больше узнать о контейнере службы и внедрении зависимостей из документации Symfony.

Последняя вещь. Даже если сам сервисный контейнер является сервисом, на который можно ссылаться в конфигурации и внедрить в другие сервисы, не делайте этого. Это действительно анти-паттерн внедрения зависимостей, так как сервисы снова будут отвечать за свои собственные зависимости, а также сервисы будут тесно связаны с самим контейнером.