Может войдёшь?
Черновики Написать статью Профиль

Сервис-контейнер

перевод документация 5.х

  1. 1. Введение
  2. 2. Связывание
    1. 2.1. Основы связывания
    2. 2.2. Связывание интерфейса с реализацией
    3. 2.3. Программа для интерфейса
    4. 2.4. Контекстное связывание
    5. 2.5. Тегирование
  3. 3. Использование на практике
  4. 4. Получение из контейнера
  5. 5. События контейнера
Этот перевод актуален для англоязычной документации на (ветка 5.3) , (ветка 5.2) , (ветка 5.1) и (ветка 5.0). Опечатка? Выдели и нажми Ctrl+Enter.

Введение

Сервис-контейнер в Laravel — это мощное средство для управления зависимостями классов и внедрения зависимостей. Внедрение зависимостей — это модный термин, который означает «внедрение» зависимостей класса в этот класс через конструктор или метод-сеттер.

Давайте посмотрим на простой пример:

+ 5.3

добавлено в 5.3 ()

PHP
<?php

namespace App\Http\Controllers;

use 
App\User;
use 
App\Repositories\UserRepository;
use 
App\Http\Controllers\Controller;

class 
UserController extends Controller
{
  
/**
   * Реализация репозитория пользователей.
   *
   * @var UserRepository
   */
  
protected $users;

  
/**
   * Создать новый экземпляр контроллера.
   *
   * @param  UserRepository  $users
   * @return void
   */
  
public function __construct(UserRepository $users)
  {
    
$this->users $users;
  }

  
/**
   * Показать профиль данного пользователя.
   *
   * @param  int  $id
   * @return Response
   */
  
public function show($id)
  {
    
$user $this->users->find($id);

    return 
view('user.profile', ['user' => $user]);
  }
}

В этом примере UserController должен получить пользователей из источника данных. Поэтому мы внедрим сервис, умеющий получать пользователей. В данном случае наш UserRepository скорее всего использует Eloquent для получения информации о пользователе из БД. Но поскольку репозиторий внедрён, мы можем легко подменить его другой реализацией. Мы также можем легко создать «mock» или фиктивную реализацию UserRepository при тестировании нашего приложения.

+ 5.2 5.1 5.0

добавлено в 5.2 () 5.1 () 5.0 ()

PHP
<?php

namespace App\Jobs;

use 
App\User;
use 
Illuminate\Contracts\Mail\Mailer;
use 
Illuminate\Contracts\Bus\SelfHandling;

class 
PurchasePodcast implements SelfHandling
{
  
/**
   * Реализация почтового сервиса.
   */
  
protected $mailer;

  
/**
   * Создание нового экземпляра.
   *
   * @param  Mailer  $mailer
   * @return void
   */
  
public function __construct(Mailer $mailer)
  {
    
$this->mailer $mailer;
  }

  
/**
   * Покупка подкаста.
   *
   * @return void
   */
  
public function handle()
  {
    
//
  
}
}

В этом примере командный обработчик PurchasePodcast должен отправить письмо пользователю для подтверждения покупки. Поэтому мы внедрим сервис, отправляющий электронные письма. Когда сервис внедрён, мы можем легко подменить его с другой реализацией. Мы также можем легко создать «mock» или фиктивную реализацию отправителя почтовых сообщений при тестировании нашего приложения.

Глубокое понимание сервис-контейнера Laravel важно для создания мощного, высокопроизводительного приложения, а также для работы с ядром Laravel.

Связывание

Основы связывания

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

Если классы не зависят от каких-либо интерфейсов, то нет необходимости связывать их в контейнере. Не нужно объяснять контейнеру, как создавать эти объекты, поскольку он автоматически извлекает такие объекты при помощи отражения (reflection).

В сервис-провайдере всегда есть доступ к контейнеру через свойство PHP$this->app. Зарегистрировать привязку можно методом PHPbind(), передав имя того класса или интерфейса, который мы хотим зарегистрировать, вместе с замыканием, которое возвращает экземпляр класса:

PHP
$this->app->bind('HelpSpot\API', function ($app) {
  return new 
HelpSpot\API($app->make('HttpClient'));
});

Обратите внимание, что мы получаем сам контейнер в виде аргумента «резолвера». Затем мы можем использовать контейнер, чтобы получать под-зависимости создаваемого объекта.

Связывание синглтона

Метод PHPsingleton() привязывает класс или интерфейс к контейнеру, который должен быть создан только один раз. После создания связанного синглтона все последующие обращения к нему будут возвращать этот созданный экземпляр:

PHP
$this->app->singleton('HelpSpot\API', function ($app) {
  return new 
HelpSpot\API($app->make('HttpClient'));
});

Связывание существующего экземпляра класса с контейнером

Вы можете также привязать существующий экземпляр объекта к контейнеру, используя метод PHPinstance(). Данный экземпляр будет всегда возвращаться при последующих обращениях к контейнеру:

PHP
$api = new HelpSpot\API(new HttpClient);

$this->app->instance('HelpSpot\Api'$api);
+ 5.3 5.2

добавлено в 5.3 () 5.2 ()

Связывание примитивов

Иногда у вас есть такой класс, который получает другие внедрённые классы, но при этом ему нужны ещё и внедрённые примитивные значения, например, числа. Вы можете использовать контекстное связывание для внедрения любых необходимых вашему классу значений:

PHP
$this->app->when('App\Http\Controllers\UserController')
          ->
needs('$variableName')
          ->
give($value);

Связывание интерфейса с реализацией

Довольно мощная функция сервис-контейнера — возможность связать интерфейс с реализацией.

+ 5.0

добавлено в 5.0 ()

Например, если наше приложение интегрировано с веб-сервисом для отправки и получения событий в реальном времени Pusher. И если мы используем Pusher PHP SDK, то можем внедрить экземпляр клиента Pusher в класс:

PHP
<?php namespace App\Handlers\Commands;

use 
App\Commands\CreateOrder;
use 
Pusher\Client as PusherClient;

class 
CreateOrderHandler {

  
/**
   * Экземпляр клиента Pusher SDK.
   */
  
protected $pusher;

  
/**
   * Создание нового экземпляра обработчика заказов.
   *
   * @param  PusherClient  $pusher
   * @return void
   */
  
public function __construct(PusherClient $pusher)
  {
    
$this->pusher $pusher;
  }

  
/**
   * Выполнение данной команды.
   *
   * @param  CreateOrder  $command
   * @return void
   */
  
public function execute(CreateOrder $command)
  {
    
//
  
}

}

В этом примере хорошо то, что мы внедрили зависимости класса, но теперь мы жёстко привязаны к Pusher SDK. Если методы Pusher SDK изменятся, или мы решим полностью перейти на новый сервис событий, то нам надо будет переписывать CreateOrderHandler.

Программа для интерфейса

Чтобы «изолировать» CreateOrderHandler от изменений в сервисе событий, мы можем определить интерфейс EventPusher и реализацию PusherEventPusher:

PHP
<?php namespace App\Contracts;

interface 
EventPusher {

  
/**
   * Push a new event to all clients.
   *
   * @param  string  $event
   * @param  array  $data
   * @return void
   */
  
public function push($event, array $data);

}

Например, допустим, у нас есть интерфейс EventPusher и реализация RedisEventPusher. Когда мы написали нашу реализацию RedisEventPusher для этого интерфейса, мы можем зарегистрировать её в сервис-контейнере так:

PHP
$this->app->bind(
  
'App\Contracts\EventPusher',
  
'App\Services\RedisEventPusher'
);

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

PHP
use App\Contracts\EventPusher;

/**
 * Создание нового экземпляра класса.
 *
 * @param  EventPusher  $pusher
 * @return void
 */
public function __construct(EventPusher $pusher)
{
    
$this->pusher $pusher;
}

Контекстное связывание

Иногда у вас может быть два класса, которые используют один интерфейс. Но вы хотите внедрить различные реализации в каждый класс.

+ 5.3

добавлено в 5.3 ()

Например, два контроллера могут работать на основе разных реализаций контракта Illuminate\Contracts\Filesystem\Filesystem. Laravel предоставляет простой и гибкий интерфейс для описания такого поведения:

PHP
use Illuminate\Support\Facades\Storage;
use 
App\Http\Controllers\PhotoController;
use 
App\Http\Controllers\VideoController;
use 
Illuminate\Contracts\Filesystem\Filesystem;

$this->app->when(PhotoController::class)
          ->
needs(Filesystem::class)
          ->
give(function () {
            return 
Storage::disk('local');
          });

$this->app->when(VideoController::class)
          ->
needs(Filesystem::class)
          ->
give(function () {
            return 
Storage::disk('s3');
          });
+ 5.2 5.1 5.0

добавлено в 5.2 () 5.1 () 5.0 ()

Например, когда наша система получает новый заказ, нам может понадобиться отправка сообщения через PubNub, а не через Pusher. Laravel предоставляет простой и гибкий интерфейс для описания такого поведения:

PHP
$this->app->when('App\Handlers\Commands\CreateOrderHandler')
          ->
needs('App\Contracts\EventPusher')
          ->
give('App\Services\PubNubEventPusher');

Вы даже можете передать замыкание в метод PHPgive():

PHP
$this->app->when('App\Handlers\Commands\CreateOrderHandler')
          ->
needs('App\Contracts\EventPusher')
          ->
give(function () {
              
// Извлечение зависимости...
            
});

Тегирование

Иногда вам может потребоваться получить все реализации в определенной категории. Например, вы пишете сборщик отчётов, который принимает массив различных реализаций интерфейса Report. После регистрации реализаций Report вы можете присвоить им тег, используя метод PHPtag():

PHP
$this->app->bind('SpeedReport', function () {
  
//
});

$this->app->bind('MemoryReport', function () {
  
//
});

$this->app->tag(['SpeedReport''MemoryReport'], 'reports');

Теперь вы можете получить их по тегу методом PHPtagged():

PHP
$this->app->bind('ReportAggregator', function ($app) {
  return new 
ReportAggregator($app->tagged('reports'));
});
+ 5.0

добавлено в 5.0 ()

Использование на практике

Laravel предоставляет несколько способов использования сервис-контейнера для повышения гибкости и тестируемости вашего приложения. Одним из характерных примеров является получение контроллеров. Все контроллеры регистрируются через сервис-контейнер, и поэтому при получении класса контроллера из контейнера автоматически получаются все зависимости, указанные в аргументах конструктора и других методах контроллера.

PHP
<?php namespace App\Http\Controllers;

use 
Illuminate\Routing\Controller;
use 
App\Repositories\OrderRepository;

class 
OrdersController extends Controller {

  
/**
   * Экземляр репозитория заказа.
   */
  
protected $orders;

  
/**
   * Создание экземпляра контроллера.
   *
   * @param  OrderRepository  $orders
   * @return void
   */
  
public function __construct(OrderRepository $orders)
  {
    
$this->orders $orders;
  }

  
/**
   * Показать все заказы.
   *
   * @return Response
   */
  
public function index()
  {
    
$orders $this->orders->all();

    return 
view('orders', ['orders' => $orders]);
  }

}

В этом примере класс OrderRepository будет автоматически внедрён в контроллер. Это означает, что «mock» OrderRepository может быть привязан к контейнеру во время unit-тестирования, позволяя сделать безболезненную заглушку для взаимодействия на уровне базы данных.

Другие примеры использования контейнера

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

Получение из контейнера

Метод PHPmake()

Вы можете использовать метод PHPmake() для получения экземпляра класса из контейнера. Метод принимает имя класса или интерфейса, который вы хотите получить:

PHP
$api $this->app->make('HelpSpot\API');

Если вы находитесь в таком месте вашего кода, где нет доступа к переменной PHP$app, вы можете использовать глобальный вспомогательный метод PHPresolve():

PHP
$api resolve('HelpSpot\API');

Автоматическое внедрение

И самая важная возможность — вы можете просто указать тип зависимости в конструкторе класса, который имеется в контейнере, включая контроллеры, слушатели событий, очереди задач, посредники и т.д. Это тот способ, с помощью которого должно получатся большинство объектов из контейнера на практике.

Например, вы можете указать тип репозитория, определённого вашим приложением в конструкторе контроллера. Репозиторий будет автоматически получен и внедрён в класс:

PHP
<?php

namespace App\Http\Controllers;

use 
App\Users\Repository as UserRepository;

class 
UserController extends Controller
{
  
/**
   * Экземпляр репозитория пользователя.
   */
  
protected $users;

  
/**
   * Создание нового экземпляра контроллера.
   *
   * @param  UserRepository  $users
   * @return void
   */
  
public function __construct(UserRepository $users)
  {
    
$this->users $users;
  }

  
/**
   * Показать пользователя с данным ID.
   *
   * @param  int  $id
   * @return Response
   */
  
public function show($id)
  {
    
//
  
}
}

В этом примере для версии 5.1 и ранее использовался ещё один контроллер PHPuse Illuminate\Routing\Controller;прим. пер.

События контейнера

Контейнер создаёт событие каждый раз, когда из него извлекается объект. Вы можете слушать эти события, используя метод PHPresolving():

PHP
$this->app->resolving(function ($object$app) {
  
// Вызывается при извлечении объекта любого типа...
});

$this->app->resolving(HelpSpot\API::class, function ($api$app) {
  
// Вызывается при извлечении объекта типа "HelpSpot\API"...
});

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

Комментарии (6)

Grigory

очень непонятный текст.

  1. Внедрение зависимостей — это модный термин, который означает «внедрение» зависимостей класса в этот класс через конструктор или метод-сеттер.

внедрение зависимостей — это внедрение зависимостей...
очень информативно.

Ramirez

Цитаты : "В этом примере хорошо то, что мы внедрили зависимости класса, но теперь мы жёстко привязаны к Pusher SDK. Если методы Pusher SDK изменятся, или мы решим полностью перейти на новый сервис событий, то нам надо будет переписывать CreateOrderHandler."

Не понятно. Напиши EventPusher, используй везде где можешь. Меняй код только одного файла. - EventPusher'a. Просто лично для меня больше файлов - больше путаниц. Может кто объяснить по подробнее в чем практическое применение интерфейсов?

leos2000

Почитай про SOLID и все станет ясно.

gram

Есть такой принцип в программировании: программируй интерфейсами, а не классами. Цель — показать какие методы можно юзать у твоего класса для работы с ним, а реализация остается за тобой и никто не будет в неё вникать. А если кому-то понадобится написать новую логику, он просто напишет новый класс, который будет реализовывать твой интерфейс, но работать по другому. Так делают потому, что есть еще один принцип программирования, классы должны быть закрыты для изменения, но открыты для расширения. Как-то так.

sprntl

Сказать, что всё понятно — это всё равно что соврать. Местами написано так, что складывается впечатление о человеке, который едва понимает о чём речь, но всё равно хочет что-то сказать интересное.

_Mik

Подскажите пожалуйста где должен быть определен класс HelpSpot\API ( указан в разделе «Связывание», «основы связывания») ?

Написать комментарий

Разметка: ? ?

Авторизуйся, чтобы прокомментировать.