Laravel. Контракт, сервис-провайдер и фасад

15.10.2020

Теги: LaravelPHPWeb-разработкаКлассООПТеорияФреймворк

Контракты или интерфейсы

Контракты в 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

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.