Laravel. Контракт, сервис-провайдер и фасад
Контракты или интерфейсы
Контракты в Laravel — это интерфейсы. Это название связано с тем, что почти все интерфейсы находятся в пространстве имен Contracts
. Свои контракты (интерфейсы) можно хранить где угодно, например в app/Helpers/Contracts
. Давайте создадим контракт и два класса, которые его реализуют.
Допустим, нам надо сохранять картинки в облако и на диск. И так вышло, что нужно использовать оба режима сохранения одновременно. Мы создаем контракт с перечнем методов и два класса, реализующих этот интерфейс.
namespace App\Helpers\Contracts; interface ImageSaverContract { public function save($src, $dst); public function delete($dst); }
namespace App\Helpers; use App\Helpers\Contracts\ImageSaverContract; /* * Это первая реализация интерфейса ImageSaverContract */ class LocalImageSaver implements ImageSaverContract { public function save($src, $dst) { return 'Файл был сохранен на локальном сервере'; } public function delete($dst) { return 'Файл был удален на локальном сервере'; } }
namespace App\Helpers; use App\Helpers\Contracts\ImageSaverContract; /* * Это вторая реализация интерфейса ImageSaverContract */ class CloudImageSaver implements ImageSaverContract { public function save($src, $dst) { return 'Файл был сохранен на удаленном сервере'; } public function delete($dst) { return 'Файл был удален на удаленном сервере'; } }
Теперь можем где-нибудь воспользоваться нашими классами:
namespace App\Http\Controllers; use App\Helpers\LocalImageSaver; use App\Helpers\CloudImageSaver; use App\Http\Controllers\Controller; use App\Models\Item; use Illuminate\Http\Request; class ItemController extends Controller { /* ... */ public function store(Request $request) { // сохраняем файл на локальный сервер $src = '/some/tmp/dir/image.jpg'; $dst = '/some/storage/image.jpg'; $localSaver = new LocalImageSaver(); $localSaver->save($src, $dst); // сохраняем файл на удаленный сервер $src = '/some/tmp/dir/other.jpg'; $dst = '/some/storage/other.jpg'; $cloudSaver = new CloudImageSaver(); $cloudSaver->save($src, $dst); /* ... */ } /* ... */ }
Поставщик услуг (Service Provider)
Допустим, нам не нужно параллельно использовать обе реализации сохранения картинок, а надо просто легко и быстро переключать режим с «сохранение на диск» на «сохранение в облако» и наоборот. Сервис провайдер как раз и предоставляет возможность легкого переключения между нескольким реализациями.
Давайте создадим сервис провайдер и реализуем метод register()
, который изначально пустой.
> php artisan make:provider ImageSaverServiceProvider
namespace App\Providers; use App\Helpers\Contracts\ImageSaverContract; use App\Helpers\LocalImageSaver; use Illuminate\Support\ServiceProvider; class ImageSaverServiceProvider extends ServiceProvider { /** * Register services. * * @return void */ public function register() { /* $this->app->bind(ImageSaverContract::class, function ($app) { return new LocalImageSaver(); // return new CloudImageSaver(); }); */ $this->app->singleton(ImageSaverContract::class, function ($app) { /* * здесь мы можем заменить реализацию интерфейса ImageSaverContract */ return new LocalImageSaver(); // return new CloudImageSaver(); }); } /** * Bootstrap services. * * @return void */ public function boot() { // ... } }
В методы singleton()
и bind()
первым параметром передается название контракта (интерфейса), а вторым параметром — анонимная функция, возвращающая один из классов, реализующих данный интерфейс. Лучше использовать метод singleton()
т.к. он создает объект указанного класса только один раз, а при последующих обращениях возвращает тот же объект.
singleton()
и bind()
есть смысл использовать, когда в анонимной функции нужно выполнить дополнительный код. В противном случае можно поступить гораздо проще.
class ImageSaverServiceProvider extends ServiceProvider { /* ... */ public function register() { App::singleton(ImageSaverContract::class, LocalImageSaver::class); } /* ... */ }
class ImageSaverServiceProvider extends ServiceProvider { /* ... */ public function register() { App::bind(ImageSaverContract::class, LocalImageSaver::class); } /* ... */ }
class ImageSaverServiceProvider extends ServiceProvider { public $singletons = [ ImageSaverContract::class => LocalImageSaver::class, ]; /* ... */ }
class ImageSaverServiceProvider extends ServiceProvider { public $bindings = [ ImageSaverContract::class => LocalImageSaver::class, ]; /* ... */ }
Тем самым мы говорим приложению, что когда происходит обращение к App\Helpers\Contracts\ImageSaverContract
— нужно вернуть новый объект класса LocalImageSaver
. Это первый шаг регистрации нового поставщика услуг (Service Provider) в контейнере сервисов (Service Container). Второй шаг — добавить нового провайдера в массив providers
в файле конфигурации config\app.php
return [ /* ... */ 'providers' => [ /* ... */ App\Providers\ImageSaverServiceProvider::class ] /* ... */ ];
Внедрение зависимости (Dependency Injection)
Допустим, нам нужно использовать данный сервис-провайдер при сохранении данных, введенных пользователем в форме.
namespace App\Http\Controllers; use App\Helpers\Contracts\ImageSaverContract; use App\Http\Controllers\Controller; use App\Models\Item; use Illuminate\Http\Request; class ItemController extends Controller { /* ... */ public function store(Request $request, ImageSaverContract $saver) { // сохраняем файл на локальный сервер $src = '/some/tmp/dir/image.jpg'; $dst = '/some/storage/image.jpg'; $res = $saver->save($src, $dst); /* ... */ } /* ... */ }
Мы передаем в метод store()
две зависимости (Dependency Injection):
$request
— объект классаRequest
, стандартная зависимость для таких случаев$saver
— объект классаLocalImageSaver
, реализующего интерфейсImageSaverContract
Можно внедрить зависимость не для отдельного метода, а для всего класса:
namespace App\Http\Controllers; use App\Helpers\Contracts\ImageSaverContract; use App\Http\Controllers\Controller; use App\Models\Item; use Illuminate\Http\Request; class ItemController extends Controller { private $saver; public function __construct(ImageSaverContract $saver) { $this->saver = $saver; } /* ... */ public function store(Request $request) { // сохраняем файл на локальный сервер $src = '/some/tmp/dir/image.jpg'; $dst = '/some/storage/image.jpg'; $res = $this->saver->save($src, $dst); /* ... */ } /* ... */ public function update(Request $request) { // сохраняем файл на локальный сервер $src = '/some/tmp/dir/image.jpg'; $dst = '/some/storage/image.jpg'; $res = $this->saver->save($src, $dst); /* ... */ } /* ... */ }
Если у провайдера есть зависимость
Допустим, что перед сохранением изображения мы хотим изменить его размер — нам нужен класс, который это умеет делать. Давайте его создадим, а потом передадим как зависимость классу, который сохраняет изображения.
namespace App\Helpers; class ImageResizeMaster { public function resize($src, $dst) { return 'Размер изображения был изменен'; } }
namespace App\Providers; use App\Helpers\Contracts\ImageSaverContract; use App\Helpers\ImageResizeMaster; use App\Helpers\LocalImageSaver; use Illuminate\Support\ServiceProvider; class ImageSaverServiceProvider extends ServiceProvider { public function register() { $this->app->singleton(ImageSaverContract::class, function ($app) { return new LocalImageSaver(new ImageResizeMaster()); // return new CloudImageSaver(new ImageResizeMaster()); }); } public function boot() { // ... } }
Немного усложним задачу — класс ImageResizeMaster
реализует интерфейс ImageResizerContract
. И мы хотим иметь возможность в любой момент заменить одну реализацию на другую — ImageResizeMaster
на ImageResizeWizard
и обратно.
namespace App\Helpers\Contracts; interface ImageResizerContract { public function resize($src, $dst); }
namespace App\Helpers; use App\Helpers\Contracts\ImageResizerContract; /* * Это первая реализация интерфейса ImageResizerContract */ class ImageResizeMaster implements ImageResizerContract { public function resize($src, $dst) { return 'Размер изображения был изменен ImageResizeMaster'; } }
namespace App\Helpers; use App\Helpers\Contracts\ImageResizerContract; /* * Это вторая реализация интерфейса ImageResizerContract */ class ImageResizeWizard implements ImageResizerContract { public function resize($src, $dst) { return 'Размер изображения был изменен ImageResizeWizard'; } }
> php artisan make:provider ImageResizerServiceProvider
namespace App\Providers; use App\Helpers\Contracts\ImageResizerContract; // use App\Helpers\ImageResizeMaster; use App\Helpers\ImageResizeWizard; use Illuminate\Support\ServiceProvider; class ImageResizerServiceProvider extends ServiceProvider { public $singletons = [ /* * здесь можем заменить реализацию интерфейса ImageResizerContract */ ImageResizerContract::class => ImageResizeWizard::class, // ImageResizerContract::class => ImageResizeMaster::class, ]; public function register() { // ... } public function boot() { // ... } }
И вносим изменения в сервис-провайдер ImageSaverServiceProvider
:
namespace App\Providers; use App\Helpers\Contracts\ImageResizerContract; use App\Helpers\Contracts\ImageSaverContract; use App\Helpers\LocalImageSaver; // use App\Helpers\CloudImageSaver; use Illuminate\Support\ServiceProvider; class SaveImageServiceProvider extends ServiceProvider { public function register() { $this->app->singleton(ImageSaverContract::class, function ($app) { /* * здесь можем заменить реализацию интерфейса ImageSaverContract */ return new LocalImageSaver($app->make(ImageResizerContract::class)); // return new CloudImageSaver($app->make(ImageResizerContract::class)); }); } public function boot() { // ... } }
Не нужно усложнять без необходимости
Очень важное замечание из документации Laravel:
Нет необходимости связывать классы в контейнер, если они не зависят от каких-либо интерфейсов. Контейнеру не нужно указывать, как создавать эти объекты, поскольку он может автоматически разрешать эти объекты с помощью отражения.
Практически это означает, что если у нас есть класс SimpleImageSaver
, не реализующий какой-либо интерфейс, нам не нужно создавать сервис-провайдер и помещать его в сервис-контейнер. С созданием экземпляра класса и его внедрением Laravel справится сам, без нашего участия.
namespace App\Helpers; class SimpleImageSaver { public function save($src, $dst) { return 'Отработал метод save() класса SimpleImageSaver'; } }
namespace App\Http\Controllers; use App\Helpers\SimpleImageSaver; use App\Http\Controllers\Controller; use App\Models\Item; use Illuminate\Http\Request; class ItemController extends Controller { /* ... */ public function store(Request $request, SimpleImageSaver $saver) { // сохраняем файл куда-то там $src = '/some/tmp/dir/image.jpg'; $dst = '/some/storage/image.jpg'; $res = $saver->save($src, $dst); /* ... */ } /* ... */ }
Мало того, если у класса SimpleImageSaver
есть зависимость — например, класс SimpleImageResizer
— Laravel разрешит и эту зависимость. Опять-таки, без каких-либо дополнительных указаний с нашей стороны.
namespace App\Helpers; class SimpleImageResizer { public function resize($src, $dst) { return 'Отработал метод resize() класса SimpleImageResizer'; } }
namespace App\Helpers; class SimpleImageSaver { private $resizer; public function __construct(SimpleImageResizer $resizer) { $this->resizer = $resizer; } public function save($src, $dst) { $res = $this->resizer->resize($src, $dst); return $res . ' Отработал метод save() класса SimpleImageSaver'; } }
namespace App\Http\Controllers; use App\Helpers\SimpleImageResizer; use App\Helpers\SimpleImageSaver; use App\Http\Controllers\Controller; use App\Models\Item; use Illuminate\Http\Request; class ItemController extends Controller { /* ... */ public function store(Request $request, SimpleImageSaver $saver) { // сохраняем файл куда-то там $src = '/some/tmp/dir/image.jpg'; $dst = '/some/storage/image.jpg'; $res = $saver->save($src, $dst); /* ... */ } /* ... */ }
Фасад (Facade)
Фасады предоставляют легкий доступ к классам, зарегистрированным в сервис-контейнере. В целом, можно обходиться и без них, внедряя зависимость в отдельные методы. Кроме того, чтобы получить объект сервиса, можно использовать хелпер app()
или глобальный объект App
. Фасады есть смысл создавать для часто используемых сервисов, чтобы упростить доступ к их методам.
Раз уж вспомнили о хелпере app()
и глобальным объекте App
— посмотрим, как их можно использовать:
namespace App\Http\Controllers; use App; use App\Helpers\Contracts\ImageSaverContract; use App\Http\Controllers\Controller; use App\Models\Item; use Illuminate\Http\Request; class ItemController extends Controller { /* ... */ public function store(Request $request) { // сохраняем файл куда-то там $src = '/some/tmp/dir/image.jpg'; $dst = '/some/storage/image.jpg'; /* $saver = App::make(ImageSaverContract::class); $saver = app()->make(ImageSaverContract::class); */ $saver = app(ImageSaverContract::class); $res = $saver->save($src, $dst); } /* ... */ }
Класс фасада должен наследоваться от родительского класса Illuminate\Support\Facades\Facade
и переопределять метод getFacadeAccessor()
. Данный метод должен возвращать ключ (строку), к которому привязывается класс в сервис-провайдере. Ключ может быть абсолютно любой строкой, при работе с фасадом обращаться нужно будет не к нему, а к алиасу.
namespace App\Helpers\Facades; use Illuminate\Support\Facades\Facade; class ImageSaver extends Facade { protected static function getFacadeAccessor() { return 'ImageSaver'; } }
$this->app->singleton('ImageSaver', function ($app) { ... }) \___ ___/ \/ ключ
namespace App\Providers; use App\Helpers\Contracts\ImageResizerContract; use App\Helpers\LocalImageSaver; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; class SaveImageServiceProvider extends ServiceProvider { public function register() { $this->app->singleton('ImageSaver', function (Application $app) { return new LocalImageSaver($app->make(ImageResizerContract::class)); }); } public function boot() { // ... } }
Создаем алиас в файле config\app.php
в массиве aliases
, по которому и будем обращаться к фасаду:
return [ /* ... */ 'aliases' => [ /* ... */ 'Validator' => Illuminate\Support\Facades\Validator::class, 'View' => Illuminate\Support\Facades\View::class, 'ImageSaver' => App\Helpers\Facades\ImageSaver::class, ], ]
При работе с фасадом мы используем алиас ImageSaver
, который существует в глобальной области видимости. Другими словами, доступ к алиасу возможен везде, внедрять зависимость не нужно, ведь фасады это как раз альтернатива внедрению зависимостей.
use ImageSaver;
namespace App\Http\Controllers; use ImageSaver; use App\Http\Controllers\Controller; use App\Models\Item; use Illuminate\Http\Request; class ItemController extends Controller { /* ... */ public function store(Request $request) { // сохраняем файл куда-то там $src = '/some/tmp/dir/image.jpg'; $dst = '/some/storage/image.jpg'; $res = ImageSaver::save($src, $dst); /* ... */ } /* ... */ }
Класс Illuminate\Support\Facades\Facade
имеет приватное свойство $app
, которое содержит ссылку на сервис-контейнер. Свойство $resolvedInstance
хранит извлеченный из контейнера сервис при первом использовании фасада. Это предотвращает затраты на повторное извлечение сервиса из сервис-контейнера при последующих вызовах методов фасада.
Магия работы фасадов находится в магическом методе __callStatic()
. Данный метод обрабатывает вызовы статических методов фасада, которые в действительности не существуют. В __callStatic()
извлекается соответствующий сервис из контейнера с помощью метода getFacadeRoot()
и вызывается уже метод извлеченного сервиса. Аргументы $method
и $args
содержат имя метода извлеченного сервиса и фактические параметры, с которыми будет вызван данный метод.
/** * Handle dynamic, static calls to the object. * * @param string $method * @param array $args * @return mixed * * @throws \RuntimeException */ public static function __callStatic($method, $args) { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); }
Поиск: Laravel • PHP • Web-разработка • Класс • ООП • Теория • Фреймворк • Фасад • Facade • Сервис провайдер • Service • Provider • Interface