Сервіс контейнером

12/07/2016 0 symfony, розробка

Сучасні PHP додатки наповнені об’єктами. Один об’єкт може полегшувати процес відправлення e-mail повідомлень, а інший - зберігати інформацію у базі даних. У вашому додатку ви можете створити об’єкт, який робить інвентаризацію товару, або об’єкт який обробляє сторонні дані. Тут варто звернути увагу на те, що сучасні додатки виконують багато функцій і складаються з векиї кількості об’єктів, які у свою чергу реалізують функції.  

У цьому розділі ми поговоримо про особливий об’єкт у Symfony, який дозволяє створювати шаблони, організовувати і отримувати багато об’єктів вашого додатку. Цей об’єкт, сервіс контейнером дасть вам можливість стандартизувати та централізувати спосіб створення об’єктів у вашому додатку. Контейнер полегшує життя розробника, працює надзвичайно швидко, і звертає вашу увагу на архітектуру, яка представляє незалежний, готовий до повторного використання, код. Оскільки, усі класи ядра Symfony використовують контейнер, ви навчитеся, як розширити, налаштувати і використовувати об’єкти у Symfony. Більшою мірою, сервіс контейнером визначає швидкість і наскільки широко буде використовуватись Symfony.  

Загалом наваштувати і користуватись Symfony - просто. В кінці цього розділу, ви будете легко  створювати власні об’єкти за допомогою контейнера і налаштовувати об’єкти зі сторонніх пакетів. Ви навчитеся писати більш придатний для тестування і менш заплутаний код, бо сервіс контейнером допомагає написати гарний код, без витрати зусиль.  

Більше інформації, яка стосується цієї теми, ви зможете прочитати у фокументації  DependencyInjection component documentation.

Що таке сервіс?

Сервіс - це будь-який PHP-об’єкт, який виконує “глобальні” завдання.  Це цілеспрямовано-родове ім’я використовується у комп’ютерних науках для опису об’єктів, які були створені зі спеціальною метою (наприклад, надсилання повідомлень). Кожен сервіс використовується у всіх куточках вашого додатку тоді, коли вам потрібна його допомога. Вам не потрібно робити чудеса, щоб створити такий сервіс: просто згенеруйте певний код, який виконає поставлене завдання. Вітаємо, ви тільки що створили сервіс!  

Як правило, PHP об’єкт - це сервіс, якщо він глобально використовується у вашому додатку. Сервіс Mailer використовується глобально, відсилаючи повідомлення, в той час як об’єкти Message, які сервіс доставляє, не є сервісами. Подібно до цього, об’єкт   Product не є сервісом, але об’єктом, що зберігає об’єкти Product до бази даних і є сервіс.

У чому ж тоді справа? Перевага використання “сервісів” полягає в тому, що ви починаєте роздумувати про відокремлення кожної частинки функціоналу у ваш додаток у формі сервісів. Оскільки кожен сервіс виконує свій тип роботи, ви матимете доступ до кожного з сервісів, і використовувати їхній функціонал у будь-який час. Кожен сервіс можна легко протестувати і налаштувати, адже він відділений від їншого функціоналу у вашому додатку. Ця ідея носить назву Сервісно-орієнтована архітектура і не є унікальною суто для Symfony, чи PHP. Структурування додатку з набором незалежних сервіс-класів - це відомий і провірений об’єктно-орієнтовний метод. Усі ці вміння - ключ для того, щоб стати хорошим розробником у будь якій мові програмування.

Що ж таке сервіс контейнером?

Сервіс контейнером (або контейнер впровадження залежностей)- це звичайний PHP об’єкт, який керує створенням сервісів (тобто, об’єктів).

Наприклад, у нас є простий PHP клас, що доставляє email-и. Без сервісу контейнером, вам потрібно створювати об’єкт, як тільки виникає така потреба:

use AppBundle\Mailer;

$mailer = new Mailer('sendmail');
$mailer->send('ryan@example.com', ...);

Це дуже просто. Наш уявний клас Mailer дозволяє налаштувати метод, що використовується для відправки повідомлень (наприклад, sendmail, smtp, і т. д.). А що, якщо буде потрібно використати сервіс ще десь? Звісно, ви не захочете налаштовувати об’єкт Mailer кожного разу, як тільки він буде вам потрібний. Що коли вам потрібно буде змінити transport з sendmail на smtp у вашому додатку? Потрібно було б обшукати кожен куток додатку, де ви створювали сервіс Mailer і вносити зміни.

Створюємо і налаштовуємо сервіси у контейнері

Сервіс контейнером створить об’єкт Mailer замість вас. Для цього, вам потрібно “навчити” контейнер, яким чином створити сервіс Mailer. Це робиться за допомогою конфігурації, яка вказується у YAML, XML чи PHP:

YAML

# app/config/services.yml
services:
    app.mailer:
        class:        AppBundle\Mailer
        arguments:    [sendmail]
XML
<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="app.mailer" class="AppBundle\Mailer">
            <argument>sendmail</argument>
        </service>
    </services>
</container>

PHP

// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;

$container->setDefinition('app.mailer', new Definition(
    'AppBundle\Mailer',
    array('sendmail')
));

Під час ініціалізації Symfony, фреймворк створює сервіс контейнером, використовуючи   налаштування додатку (pp/config/config.yml за замовчуванням). Файл, який буде завантажений, визначається методом AppKernel::registerContainerConfiguration(), що завантажує файл, який відноситься до спеціального середовища (наприклад, config_dev.yml для середовища dev, або config_prod.yml для prod).

Тепер у вас є доступ до об’єкта класу AppBundle\Mailer. Контейнер доступний будь-якому традиційному контролеру Symfony, де ви матимете доступ до сервісів контейнером за допомогою методу get():

class HelloController extends Controller
{
    // ...

    public function sendEmailAction()
    {
        // ...
        $mailer = $this->get('app.mailer');
        $mailer->send('ryan@foobar.net', ...);
    }
}

Коли ви дасте запит з контейнера на сервіс app.mailer, контейнер створить об’єкт, і поверне його вам. Це ще одна перевага використання сервісу контейнером. Сервіс ніколи не буде створений, якщо він не потрібний. Якщо ви визначаєте сервіс і ніколи не використовувати його за запитом, він не створюється. Це економить пам'ять і збільшує швидкість вашого додатку. Сервіси, що ніколи не використовуються, ніколи і не створюються.  

Як бонус, сервіс Mailer створюється лише один раз, і ви отримуєте його, які тільки відсилаєте запит. Зазвичай, це саме те, що вам потрібно (така робота більш гнучка і потужна), пізніше ви дізнаєтесь, як налаштувати сервіс, з багатьма об’єктами у статті з довідника "How to Define Non Shared Services".

У цьому прикладі, контролер розширює базовий контролер Symfony, і він дає вам доступ до сервісу контейнером. Тепер ви можете використати метод get, щоб розмістити і витягнути з сервісу контролером сервіс app.mailer. Ви також можете визначити ваші контролери у якості сервісів. Це більш складний і не важливий аспект, проте саме від дозволяє включити стільки сервісів у ваш контролер, скільки потрібно.   

Параметри сервісу

Створення нових сервісів (тобто, об’єктів) через контролер — проста справа.  Параметри роблять визначені сервіси більш організованими і гнучкими.

YAML

# app/config/services.yml
parameters:
    app.mailer.transport:  sendmail

services:
    app.mailer:
        class:        AppBundle\Mailer
        arguments:    ['%app.mailer.transport%']

XML

<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="app.mailer.transport">sendmail</parameter>
    </parameters>

    <services>
        <service id="app.mailer" class="AppBundle\Mailer">
            <argument>%app.mailer.transport%</argument>
        </service>
    </services>
</container>

PHP

// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;

$container->setParameter('app.mailer.transport', 'sendmail');

$container->setDefinition('app.mailer', new Definition(
    'AppBundle\Mailer',
    array('%app.mailer.transport%')
));

Результат отримуємо такий самий — різниця в тому, як ви визначите сервіс. Коли поставити знак (%) з обох боків рядка app.mailer.transport, контейнер шукатиме параметер з таким іменем. Коли контейнер створений, він шукає значення кожного параметру і використовує визначення сервісу.

Якщо ви будете використовувати рядок, що починається зі знаку @, у якості значення параметра (наприклад, короткий пароль до електронної пошти) у файлі YAML, для того щоб вийти — скористайтесь ще одним знаком @ (це підходить тільки до формату  YAML):

# app/config/parameters.yml
parameters:
    # This will be parsed as string '@securepass'
    mailer_password: '@@securepass'

Якщо ви використовуєте “%” у середині параметра або аргумента, у якості складової рядка, тоді використовуйте ще один такий самий знак для, того щоб вийти.

<argument type="string">http://symfony.com/?foo=%%s&amp;bar=%%d</argument>

Метою параметрів є подача інформації до сервісів. Звичайно, немає нічого поганого з визначенням сервісу без використання будь-яких параметрів. Але параметри мають кілька переваг:

  • поділ і організація всіх опцій сервісів під єдиним ключем параметрів;

  • значення параметрів можуть бути використані в декількох визначеннях сервісів;

  • при створенні служби у пакеті (про це ми розповімо далі), використання параметрів дозволяє вам легко налаштувати сервіси у вашому додатку.

Будете використовувати параметри, чи ні — залежить від вас. Високоякісні сторонні пакети будуть завжди використовувати параметри, адже вони впливають на сервіс у контролері, і лише в цьому випадку вам буде лекго його налаштувати. Однак, для отримання послуг у вашому додатку, вам параметри не знадобляться.  

Параметри масивів

Параметри також включають значення масиву. Див Array Parameters.

Імпортуємо інші ресурси конфігурації контейнера

У цій частині розділу, називатимемо файли конфігурації сервісу — ресурсами. Таким чином, ми підкреслимо, що неважливо зо усі ці ресурси налаштування є файлами (наприклад: YAML, XML, PHP), Symfony дуже гнучка, і конфігурацію можна завандажити з будь-якого джерела (наприклад з бази даних, або через зовнішні веб сервіси).

Сервіс контейнером побудований за допомогою лише одного ресурсу конфігурації  (app/config/config.yml за замовчуванням). Усі інші сервіси налаштування (включно з ядром Symfony і налаштуванням сторонніх пакетів) потрібно імпортувати з цього файлу. Це дасть вам абсолютну гнучкість у роботі з сервісами у вашому додатку.  

Зовнішню конфігурацію сервісу можна імпортувати двома способами. Перший метод, зазвичай використовують для імпортування конфігурації сервісу зі створеного пакету, за допомогою директиви imports. Другий метод — більш складний — пропонує більш гнучкий шлях, і викорристовують його для налаштування стороннього пакету. Якщо ви бажаєте дізнатись як працюють ці методи — наступний підрозділ для вас.  

Імпортування конфігурації за допомогою imports

Отже, ви розмістили визначення сервіс контейнер app.mailer на пряму з файлу конфігурації додатка (наприклад, app/config/config.yml). Звичайно, якщо клас Mailer cзнаходиться у пакеті AcmeHelloBundle, краще буде, якщо ви розмістите також цей рядок — app.mailer — всередині пакету.  

Спочатку, перемістіть визначення контейнеру app.mailer у новий файл ресурсу контейнера всередину AcmeHelloBundle. Якщо директорії Resources чи Resources/config не існують, створіть їх.  

YAML

# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
    app.mailer.transport: sendmail

services:
    app.mailer:
        class:        AppBundle\Mailer
        arguments:    ['%app.mailer.transport%']

XML

<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="app.mailer.transport">sendmail</parameter>
    </parameters>

    <services>
        <service id="app.mailer" class="AppBundle\Mailer">
            <argument>%app.mailer.transport%</argument>
        </service>
    </services>
</container>

PHP

// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;

$container->setParameter('app.mailer.transport', 'sendmail');

$container->setDefinition('app.mailer', new Definition(
    'AppBundle\Mailer',
    array('%app.mailer.transport%')
));

Саме по собі визначення не змінилось, лише його локація. Звичайно сервіс контейнером “не знає” про новий файл ресурса. Нащастя, ви можете легко імпортувати новий ресурсний файл за допомогою ключа imports у конфігурації додатку.

YAML

# app/config/config.yml
imports:
    - { resource: '@AcmeHelloBundle/Resources/config/services.yml' }

XML

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <imports>
        <import resource="@AcmeHelloBundle/Resources/config/services.xml"/>
    </imports>
</container>

PHP

// app/config/config.php
$loader->import('@AcmeHelloBundle/Resources/config/services.php');

Через цей спосіб, в якому вирішуються параметри, ви не можете використовувати ці параметри для динамічного створення шляхів у imports. Це означає, що наступний код не працюватиме:

YAML

# app/config/config.yml
imports:
    - { resource: '%kernel.root_dir%/parameters.yml' }

XML

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <imports>
        <import resource="%kernel.root_dir%/parameters.yml" />
    </imports>
</container>

PHP

// app/config/config.php
$loader->import('%kernel.root_dir%/parameters.yml');

Директива imports дозволяє вашому додатку включити налаштування сервісу контейнером з будь-якої локації (зазвичай з пакетів). Локація resource, для файлів — абсолютний шлях до ресурсного файла. Спеціальний синтаксис @AcmeHelloBundle “вирішує” шлях директорії пакета AcmeHelloBundle. Це допомагає специфікувати шлях до ресурсу, і пізхніше ви вже не будете турбуватися, чи ви перемістили AcmeHelloBundle до іншої директорії.

Імпортуємо конфігурацію через розширення контейнера  

Коли ви працюватимете з Symfony, ви зазвичай будете використовувати директиву  imports для імпортування конфігурації контейнера з пакетів, які ви створили спеціально для вашого додатку.

Сторонні пакети конфігурації контейнера, включаючи сервіси ядра Symfony, зазвичай завантажують іншим, більш гнучким методом, який легко налаштувати у вашому додатку.  

Все це працює таким чином. Всередині, кожен пакет визначає свої сервіси, так як ви це вже побачили у попередніх підрозділах. А саме, пакет використовує один або більше ресурсних файлів налаштування (зазвичай XML), для визначення параметрів і сервісів до певного пакету. Однак, замість того щоб імпортувати кожен з цих ресурсів напряму з конфігурації вашого додатку за допомогою директиви imports, ви можете просто використати розширення сервісу контейнером всередині пакету, який виконує частину вашої роботи.   

Розширення сервісу контейнером — це  PHP клас створений, автором пакету, з наступною метою:

  • імпортуйте усі ресурси сервісу контейнером, потрібні для налаштування сервісів для пакету;

  • створіть семантичну, зрозумілу конфігурацію, щоб пакет можна було б налаштувати без залучання простих параметрів конфігурації сервісу контейнером певного пакету.

Іншими словами, розширення сервісу контейнером налаштовує сервіси для пакету замість вас. Це розширення надає високоякісний інтерфейс для налаштування пакету.   

Розглянемо FrameworkBundle — ядро пакету Symfony Framework — як приклад. Якщо у налаштуванні вашого додатку буде наступний код, ви отримаєте розширення сервісу контейнером всередині  FrameworkBundle:

YAML

# app/config/config.yml
framework:
    secret:          xxxxxxxxxx
    form:            true
    csrf_protection: true
    router:        { resource: '%kernel.root_dir%/config/routing.yml' }
    # ...

XML

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony
        http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

    <framework:config secret="xxxxxxxxxx">
        <framework:form />
        <framework:csrf-protection />
        <framework:router resource="%kernel.root_dir%/config/routing.xml" />
        <!-- ... -->
    </framework>
</container>

PHP

// app/config/config.php
$container->loadFromExtension('framework', array(
    'secret'          => 'xxxxxxxxxx',
    'form'            => array(),
    'csrf-protection' => array(),
    'router'          => array(
        'resource' => '%kernel.root_dir%/config/routing.php',
    ),

    // ...
));

Коли конфігурація оброблена, контейнер шукає розширення, яке може обробляти директиву налаштування фреймфорку. Розширення у пакеті FrameworkBundle, викликається і тоді завантажується кофігурація сервісу для FrameworkBundle.  Якщо ви повністю видалите ключ фреймворка з файлу конфігурації програми, основні сервіси послуги Symfony не будуть завантажені. Справа в тому, що процесом керуєте ви: Symfony Framework — не маг, і не виконує дії, якими ви не контролюєте.  

Звичайно, ви можете зробити набагато більше, ніж просто "активувати" розширення у FrameworkBundle. Кожне розширення дозволяє легко налаштувати пакет, не турбуючись про те, як визначати внутрішні послуги.

У цьому випадку, розширення дозволяє налаштувати error_handler, csrf_protection, конфігурацію router і багато іншого. Усередині, FrameworkBundle використовує параметри, вказані тут, щоб визначити і налаштувати пов’язані з ними сервіси. Пакет “піклується” про створення всіх необхідних параметрів і сервісів для сервісу контейнером, в той же час дозволяючи легко налаштувати більшу частину конфігурації. У якості бонусу, більшість розширень сервісу контейнером проводять валідацію, вказуючи вам про пропущені опції або типи даних.  

Під час завантаження, або налаштування пакету, підглядайте у документацію, як саме налаштувати і встановити сервіси для пакету. Усі доступні опції для ядра пакетів можна знайти у Reference Guide.

Зазвичай, сервіс контейнером розпізнає параметри, сервіси і директиви imports. Розширення сервісу контейнером, опрацьовує інші директиви.  

Якщо ви хочете виділити зручну конфігурацію у ваших пакетах, читайте розділ "How to Load Service Configuration inside a Bundle" cookbook recipe.

Реферування сервісів

До цього моменту, працювати з базовим сервісомп app.mailer було просто: для його роботи потрібен лише один аргумент на його конструктор, який легко налаштовується. Пізніше ви побачите, реальна потужність контейнера реалізується, коли вам потрібно буде створити сервіс, який залежатиме від інших сервісів у  контейнері.

Припустимо, що у вас є новий сервіс, NewsletterManager, який допомагає керувати підготовкою та доставкою повідомлення електронної пошти до набору адрес. Звичайно, сервіс app.mailer чудово надсилає повідомлення по електронній пошті, тому ви можете використовувати його всередині NewsletterManager, що справитися з доставкою повідомлень. Тобто клас може виглядати наступним чином:

// src/AppBundle/Newsletter/NewsletterManager.php
namespace AppBundle\Newsletter;

use AppBundle\Mailer;

class NewsletterManager
{
    protected $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

Якщо ви не хочете використовувати сервіс контейнером, просто створіть новий  NewsletterManager з середини контролера:

use AppBundle\Newsletter\NewsletterManager;

// ...

public function sendNewsletterAction()
{
    $mailer = $this->get('app.mailer');
    $newsletter = new NewsletterManager($mailer);
    // ...
}

Цей підхід досить добрий, проте, що якщо пізніше вам знадобиться другий або третій аргумент конструктора для NewsletterManager? А що, коли ви вирішите зробити рефакторинг вашого коду і зманити назву класу? В обох випадках, вам потрібно віднайти, де саме інстанційований NewsletterManager, і тоді змінити його. Звичайно, сервіс контейнером, пропонує більш привабливе рішення:  

YAML

# app/config/services.yml
services:
    app.mailer:
        # ...

    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        arguments: ['@app.mailer']

XML

<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="app.mailer">
        <!-- ... -->
        </service>

        <service id="app.newsletter_manager" class="AppBundle\Newsletter\NewsletterManager">
            <argument type="service" id="app.mailer"/>
        </service>
    </services>
</container>

PHP

// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

$container->setDefinition('app.mailer', ...);

$container->setDefinition('app.newsletter_manager', new Definition(
    'AppBundle\Newsletter\NewsletterManager',
    array(new Reference('app.mailer'))
));

У YAML, особливий синтаксис @app.mailer “наказує” контейнерові пошукати сервіс з назвою app.mailer і передати цей об’єкт конструкторові NewsletterManager. Проте у цьому випадку, визначений сервіс app.mailer повинен існувати. Якщо ні — виконуватиметься виняток. Ви можете позначити залежності як вибіркові — про це ми розповімо далі.

Посилання — потужний інструмент, який дозволяє створити незалежні класи сервісів з добре-визначеними залежностями. У цьому випадку, для функціонування, сервісу app.newsletter_manager  потрібен сервіс app.mailer. Коли ви зазначите цю залежність у сервісі контейнером, контейнер піклуватиметься про створення екземплярів класів.

Використовуємо скриптову мову виразів

Сервіс контейнером підтримує “вирази”, які допоможуть вам вставити спеціальні значення у сервіс.  


Наприклад, у вас є третій сервіс (тут ми його не показуємо), під назвою   mailer_configuration, який має містить метод  getMailerMethod(), який повертатиме рядок типу sendmail базований на певній крнфігурації. Пам’ятайте, що перший аргумент до сервісу my_mailer — це простий рядок sendmail:

YAML

# app/config/services.yml
services:
    app.mailer:
        class:        AppBundle\Mailer
        arguments:    [sendmail]

XML

<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="app.mailer" class="AppBundle\Mailer">
            <argument>sendmail</argument>
        </service>
    </services>
</container>

PHP

// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;

$container->setDefinition('app.mailer', new Definition(
    'AppBundle\Mailer',
    array('sendmail')
));

А як можна дістати заначення з getMailerMethod() сервісу newmailer_configuration? Це можна зробити лише єдиним способом — використати вираз:

YAML

 # app/config/config.yml
services:
    my_mailer:
        class:        Acme\HelloBundle\Mailer
        arguments:    ["@=service('mailer_configuration').getMailerMethod()"]

XML

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd"
    >

    <services>
        <service id="my_mailer" class="Acme\HelloBundle\Mailer">
            <argument type="expression">service('mailer_configuration').getMailerMethod()</argument>
        </service>
    </services>
</container>

PHP

// app/config/config.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\ExpressionLanguage\Expression;

$container->setDefinition('my_mailer', new Definition(
    'Acme\HelloBundle\Mailer',
    array(new Expression('service("mailer_configuration").getMailerMethod()'))
));

Якщо ви хочете дізнатися більше про синтаксис мови виразів, див.  The Expression Syntax.

У цьому контексті, у вас є доступ до двох функцій:  

service

Повертає даний сервіс (попередній приклад).

parameter

Повертає певне значення параметра (синтакс такий самий як сервіс) .

Також через змінну контейнера, ви матимете доступ до ContainerBuilder. Ось ще один приклад:  

YAML

services:
    my_mailer:
        class:     Acme\HelloBundle\Mailer
        arguments: ["@=container.hasParameter('some_param') ? parameter('some_param') : 'default_value'"]

XML

<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd"
    >

    <services>
        <service id="my_mailer" class="Acme\HelloBundle\Mailer">
            <argument type="expression">container.hasParameter('some_param') ? parameter('some_param') : 'default_value'</argument>
        </service>
    </services>
</container>

PHP

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\ExpressionLanguage\Expression;

$container->setDefinition('my_mailer', new Definition(
    'Acme\HelloBundle\Mailer',
    array(new Expression(
        "container.hasParameter('some_param') ? parameter('some_param') : 'default_value'"
    ))
));

Вирази можна використовувати в аргументах, властивостях у якості агрументів з конфігуратором і у якості аргументів виклику.

Додаткові Залежності: сетер Injection

Впровадження залежностей до конструктора у такий спосіб — дасть вам впевненість, що залежність можна використовувати. Якщо у вас є додаткові залежності для класу, тоді  "setter injection" — найкращий варіант. Це означає впровадження залежності за допомогою методу, а не через конструктор. Клас виглядатиме наступним чином:

namespace AppBundle\Newsletter;

use AppBundle\Mailer;

class NewsletterManager
{
    protected $mailer;

    public function setMailer(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

Впровадження залежності методом setter, вимагає вміни синтаксису.

YAML

# app/config/services.yml
services:
    app.mailer:
        # ...

    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        calls:
            - [setMailer, ['@app.mailer']]

XML

<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="app.mailer">
        <!-- ... -->
        </service>

        <service id="app.newsletter_manager" class="AppBundle\Newsletter\NewsletterManager">
            <call method="setMailer">
                <argument type="service" id="app.mailer" />
            </call>
        </service>
    </services>
</container>

PHP

// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

$container->setDefinition('app.mailer', ...);

$container->setDefinition('app.newsletter_manager', new Definition(
    'AppBundle\Newsletter\NewsletterManager'
))->addMethodCall('setMailer', array(
    new Reference('app.mailer'),
));

Підходи зображені у цьому підрозділі називаються  "впровадження конструктора" і "впровадження сетера". Сервіс контейнером у Symfony  підтримує також "впровадження властивості”.

Accessing the Request in a Service

Якщо вам потрібен доступ до поточного запиту у сервісі, ви можете додати його як аргумент до методів, яким потрібен запит або впровадити сервіс request_stack і отримати доступ до Request за допомогою методу getCurrentRequest():

namespace Acme\HelloBundle\Newsletter;

use Symfony\Component\HttpFoundation\RequestStack;

class NewsletterManager
{
    protected $requestStack;

    public function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    public function anyMethod()
    {
        $request = $this->requestStack->getCurrentRequest();
        // ... do something with the request
    }

    // ...
}

Тепер, просто вставте request_stack, який виглядатиме як звичайний сервіс.

YAML

# src/Acme/HelloBundle/Resources/config/services.yml
services:
    newsletter_manager:
        class:     Acme\HelloBundle\Newsletter\NewsletterManager
        arguments: ["@request_stack"]

XML

<!-- src/Acme/HelloBundle/Resources/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service
            id="newsletter_manager"
            class="Acme\HelloBundle\Newsletter\NewsletterManager"
        >
            <argument type="service" id="request_stack"/>
        </service>
    </services>
</container>

PHP

// src/Acme/HelloBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

// ...
$container->setDefinition('newsletter_manager', new Definition(
    'Acme\HelloBundle\Newsletter\NewsletterManager',
    array(new Reference('request_stack'))
));

Якщо ви визначаєте контролер як сеервіс, тоді вам слід отримати об’єкт Request і не впроваджувати контейнер, бо він буде у якості аргумента вашого методу. Див. The Request object as a Controller Argument.

Робимо посилання необов’язковими

Деколи, сервіс може мати додаткову залежність, а це означає, що що вона не є обов’язковою для вашого сервісу. У попередньому прикладі, сервіс app.mailer обов’язковий, інакше ви отримаєте виняток. Змінюючи визначення сервісу app.newsletter_manager, ви можете зробити це посилання необов’язковим, для цього існує дві стратегії.

Налаштування пропущеної залежності на “null”

Ви можете використовувати стратегію null, щоб налаштувати агрумент на null, якщо сервіс не існуватиме:

XML

<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="app.mailer">
        <!-- ... -->
        </service>

        <service id="app.newsletter_manager" class="AppBundle\Newsletter\NewsletterManager">
            <argument type="service" id="app.mailer" on-invalid="null" />
        </service>
    </services>
</container>

PHP

// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerInterface;

$container->setDefinition('app.mailer', ...);

$container->setDefinition('app.newsletter_manager', new Definition(
    'AppBundle\Newsletter\NewsletterManager',
    array(
        new Reference(
            'app.mailer',
            ContainerInterface::NULL_ON_INVALID_REFERENCE
        )
    )
));

Стратегія "null" на даний момент не підтримується драйвером YAML.

Ігнорування пропущених залежностей

Ігнорування пропущених залежностей — це те саме що стратегія “null". Виняток, коли процес проходить у методі виклику, тоді метод виклику буде видалений.

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

YAML

# app/config/services.yml
services:
    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        arguments: ['@?app.mailer']

XML

<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="app.mailer">
        <!-- ... -->
        </service>

        <service id="app.newsletter_manager" class="AppBundle\Newsletter\NewsletterManager">
            <call method="setMailer">
                <argument type="service" id="my_mailer" on-invalid="ignore"/>
            </call>
        </service>
    </services>
</container>

PHP

// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerInterface;

$container->setDefinition('app.mailer', ...);

$container->setDefinition('app.newsletter_manager', new Definition(
    'AppBundle\Newsletter\NewsletterManager'
))->addMethodCall('setMailer', array(
    new Reference(
        'my_mailer',
        ContainerInterface::IGNORE_ON_INVALID_REFERENCE
    ),
));

У YAML, спеціальний синтакс @? дає команду сервісу контейнером, що залежність необов’язкова. Звичайно, слід переписати і NewsletterManager, для дозволу роботи необов’язкової залежності:

public function __construct(Mailer $mailer = null)
{
    // ...
}

Ядро Symfony і сторонні сервіси пакетів

Оскільки Symfony і усі сторонні пакети налаштовують і дістають власні сервіси через контейнер, ви легко отримаєте доступ до них у ваших сервісах. Щоб спростити весь процес, за замовчуванням Symfony не вимагає щоб контролери були визначені як сервіси. Більше того, Symfony впроваджує цілий сервіс контейнером у ваш контролер. Наприклад, щоб справитися зі зберіганням інформації у сесії користувача, Symfony надає сервіс sessions, до якого ви маєте доступ через стандартний контролер:

public function indexAction($bar)
{
    $session = $this->get('session');
    $session->set('foo', $bar);

    // ...
}

У Symfony, ви зазвичай будете користуватися сервісами, даними через ядро Symfony, або іншими сторонніми пакетами, щоб згенерувати шаблони, надіслати email, або отримати доступ до інформації на запит через request_stack.

Ви можете зробити крок вперед, скориставшись сервісами у сервісах, що ви вже створили у вашому додатку.  Почавши зміну NewsletterManager використовуйте справжній сервіс mailer (замість app.mailer). Також передайте сервіс двигуна шаблонів до NewsletterManager, щоб він міг згенерувати контент email через шаблон:

// src/AppBundle/Newsletter/NewsletterManager.php
namespace AppBundle\Newsletter;

use Symfony\Component\Templating\EngineInterface;

class NewsletterManager
{
    protected $mailer;

    protected $templating;

    public function __construct(
        \Swift_Mailer $mailer,
        EngineInterface $templating
    ) {
        $this->mailer = $mailer;
        $this->templating = $templating;
    }

    // ...
}

Налаштувати сервіс контейнером — просто:

YAML

# app/config/services.yml
services:
    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        arguments: ['@mailer', '@templating']

XML

<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <service id="app.newsletter_manager" class="AppBundle\Newsletter\NewsletterManager">
        <argument type="service" id="mailer"/>
        <argument type="service" id="templating"/>
    </service>
</container>

PHP

// app/config/services.php
$container->setDefinition('app.newsletter_manager', new Definition(
    'AppBundle\Newsletter\NewsletterManager',
    array(
        new Reference('mailer'),
        new Reference('templating'),
    )
));

Сервіс app.newsletter_manager  має доступ до ядра сервісів mailer і templating . Це звичайний спосіб створення сервісів до вашого додатку, що використовує потужність різних сервісів у фреймворку.  

Переконайтесь, що  swiftmailer з’являється у налаштуваннях вашого додатку. Так як ми згадували раніше, ключ swiftmailer “викликає” розширення з SwiftmailerBundle, який реєструє сервіс mailer.

Теги

In the same way that a blog post on the Web might be tagged with things such as "Symfony" or "PHP", services configured in your container can also be tagged. In the service container, a tag implies that the service is meant to be used for a specific purpose. Take the following example:

YAML
# app/config/services.yml
services:
    foo.twig.extension:
        class: AppBundle\Extension\FooExtension
        public: false
        tags:
            -  { name: twig.extension }

XML

<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service
            id="foo.twig.extension"
            class="AppBundle\Extension\FooExtension"
            public="false">

            <tag name="twig.extension" />
        </service>
    </services>
</container>

PHP

// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;

$definition = new Definition('AppBundle\Extension\FooExtension');
$definition->setPublic(false);
$definition->addTag('twig.extension');
$container->setDefinition('foo.twig.extension', $definition);

Тег twig.extension — це спеціальний тег, пфд час конфігурації його використовує TwigBundle. Коли сервіс містить тег twig.extension, пакет “знає”, що сервіс  foo.twig.extension повинен бути зареєстрований як розширення Twig з Twig. Простіше кажучи, Twig знаходить усі сервіси з тегом twig.extension і автоматично реєструє їх як розширення.

Теги існують для того, щоб сказати Symfony чи іншим стороннім пакетам, що ваш сервіс потрібно зареєструвати і пакет повинен їх по-особливому використовувати.

Увесь список тегів у ядрі Symfony Framework, див. The Dependency Injection Tags. Кожен з них по-різному впливає на ваш сервіс, і для багатьох тегів потрібні додаткові аргументи (не просто параметр name).

Налагодження Сервісів

Ви можете дізнатися, які сервіси реєструються з контейнером за допомогою консолі. Для того, щоб показати всі сервіси та клас для кожної служби, виконайте наступну команду:

$ php bin/console debug:container

За замовчуванням, тільки публічні сервіси будуть показані, але ви можете також переглянути і приватні сервіси:

$ php bin/console debug:container --show-private

Якщо приватний сервіс використовується лише як аргумент, до іншого сервісу, команда debug:container не відображатиме його, навіть якщо використати опцію --show-private. Див. Inline Private Services.

Ви можете отримати більш детальну інформацію про конкретний сервіс, вказавши його id:

$ php bin/console debug:container app.mailer

Поділитися