Введение
Сервис-контейнер в Laravel — это мощное средство для управления зависимостями классов и внедрения зависимостей. Внедрение зависимостей — это модный термин, который означает «внедрение» зависимостей класса в этот класс через конструктор или метод-сеттер.
Давайте посмотрим на простой пример:
добавлено в 5.3 ()
<?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 ()
<?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()
, передав имя того класса или интерфейса, который мы хотим зарегистрировать, вместе с замыканием, которое возвращает экземпляр класса:
$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
Обратите внимание, что мы получаем сам контейнер в виде аргумента «резолвера». Затем мы можем использовать контейнер, чтобы получать под-зависимости создаваемого объекта.
Метод PHPsingleton()
привязывает класс или интерфейс к контейнеру, который должен быть создан только один раз. После создания связанного синглтона все последующие обращения к нему будут возвращать этот созданный экземпляр:
$this->app->singleton('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
Связывание существующего экземпляра класса с контейнером
Вы можете также привязать существующий экземпляр объекта к контейнеру, используя метод PHPinstance()
. Данный экземпляр будет всегда возвращаться при последующих обращениях к контейнеру:
$api = new HelpSpot\API(new HttpClient);
$this->app->instance('HelpSpot\Api', $api);
Иногда у вас есть такой класс, который получает другие внедрённые классы, но при этом ему нужны ещё и внедрённые примитивные значения, например, числа. Вы можете использовать контекстное связывание для внедрения любых необходимых вашему классу значений:
$this->app->when('App\Http\Controllers\UserController')
->needs('$variableName')
->give($value);
Связывание интерфейса с реализацией
Довольно мощная функция сервис-контейнера — возможность связать интерфейс с реализацией.
добавлено в 5.0 ()
Например, если наше приложение интегрировано с веб-сервисом для отправки и получения событий в реальном времени Pusher. И если мы используем Pusher PHP SDK, то можем внедрить экземпляр клиента Pusher в класс:
<?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 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 для этого интерфейса, мы можем зарегистрировать её в сервис-контейнере так:
$this->app->bind(
'App\Contracts\EventPusher',
'App\Services\RedisEventPusher'
);
Так контейнер понимает, что должен внедрить RedisEventPusher, когда классу нужна реализация EventPusher. Теперь мы можем использовать указание типа интерфейса EventPusher в конструкторе, или любом другом месте, где сервис-контейнер внедряет зависимости:
use App\Contracts\EventPusher;
/**
* Создание нового экземпляра класса.
*
* @param EventPusher $pusher
* @return void
*/
public function __construct(EventPusher $pusher)
{
$this->pusher = $pusher;
}
Контекстное связывание
Иногда у вас может быть два класса, которые используют один интерфейс. Но вы хотите внедрить различные реализации в каждый класс.
добавлено в 5.3 ()
Например, два контроллера могут работать на основе разных реализаций контракта Illuminate\Contracts\Filesystem\Filesystem. Laravel предоставляет простой и гибкий интерфейс для описания такого поведения:
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 ()
Например, когда наша система получает новый заказ, нам может понадобиться отправка сообщения через PubNub, а не через Pusher. Laravel предоставляет простой и гибкий интерфейс для описания такого поведения:
$this->app->when('App\Handlers\Commands\CreateOrderHandler')
->needs('App\Contracts\EventPusher')
->give('App\Services\PubNubEventPusher');
Вы даже можете передать замыкание в метод PHPgive()
:
$this->app->when('App\Handlers\Commands\CreateOrderHandler')
->needs('App\Contracts\EventPusher')
->give(function () {
// Извлечение зависимости...
});
Тегирование
Иногда вам может потребоваться получить все реализации в определенной категории. Например, вы пишете сборщик отчётов, который принимает массив различных реализаций интерфейса Report. После регистрации реализаций Report вы можете присвоить им тег, используя метод PHPtag()
:
$this->app->bind('SpeedReport', function () {
//
});
$this->app->bind('MemoryReport', function () {
//
});
$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');
Теперь вы можете получить их по тегу методом PHPtagged()
:
$this->app->bind('ReportAggregator', function ($app) {
return new ReportAggregator($app->tagged('reports'));
});
добавлено в 5.0 ()
Использование на практике
Laravel предоставляет несколько способов использования сервис-контейнера для повышения гибкости и тестируемости вашего приложения. Одним из характерных примеров является получение контроллеров. Все контроллеры регистрируются через сервис-контейнер, и поэтому при получении класса контроллера из контейнера автоматически получаются все зависимости, указанные в аргументах конструктора и других методах контроллера.
<?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()
для получения экземпляра класса из контейнера. Метод принимает имя класса или интерфейса, который вы хотите получить:
$api = $this->app->make('HelpSpot\API');
Если вы находитесь в таком месте вашего кода, где нет доступа к переменной PHP$app
, вы можете использовать глобальный вспомогательный метод PHPresolve()
:
$api = resolve('HelpSpot\API');
И самая важная возможность — вы можете просто указать тип зависимости в конструкторе класса, который имеется в контейнере, включая контроллеры, слушатели событий, очереди задач, посредники и т.д. Это тот способ, с помощью которого должно получатся большинство объектов из контейнера на практике.
Например, вы можете указать тип репозитория, определённого вашим приложением в конструкторе контроллера. Репозиторий будет автоматически получен и внедрён в класс:
<?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()
:
$this->app->resolving(function ($object, $app) {
// Вызывается при извлечении объекта любого типа...
});
$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
// Вызывается при извлечении объекта типа "HelpSpot\API"...
});
Как видите, объект, получаемый из контейнера, передаётся в функцию обратного вызова, что позволяет вам задать любые дополнительные свойства для объекта перед тем, как отдать его тому, кто его запросил.
Комментарии (6)
очень непонятный текст.
внедрение зависимостей — это внедрение зависимостей...
очень информативно.
Цитаты : "В этом примере хорошо то, что мы внедрили зависимости класса, но теперь мы жёстко привязаны к Pusher SDK. Если методы Pusher SDK изменятся, или мы решим полностью перейти на новый сервис событий, то нам надо будет переписывать CreateOrderHandler."
Не понятно. Напиши EventPusher, используй везде где можешь. Меняй код только одного файла. - EventPusher'a. Просто лично для меня больше файлов - больше путаниц. Может кто объяснить по подробнее в чем практическое применение интерфейсов?
Почитай про SOLID и все станет ясно.
Есть такой принцип в программировании: программируй интерфейсами, а не классами. Цель — показать какие методы можно юзать у твоего класса для работы с ним, а реализация остается за тобой и никто не будет в неё вникать. А если кому-то понадобится написать новую логику, он просто напишет новый класс, который будет реализовывать твой интерфейс, но работать по другому. Так делают потому, что есть еще один принцип программирования, классы должны быть закрыты для изменения, но открыты для расширения. Как-то так.
Сказать, что всё понятно — это всё равно что соврать. Местами написано так, что складывается впечатление о человеке, который едва понимает о чём речь, но всё равно хочет что-то сказать интересное.
Подскажите пожалуйста где должен быть определен класс HelpSpot\API ( указан в разделе «Связывание», «основы связывания») ?