В данной статье будут рассмотрены такие элементы архитектуры фреймворка Laravel 5 как:
  • контракты;
  • сервис-провайдеры;
  • сервис-контейнер;
  • фасады.
А так же даны примеры их создания и использования.


Контракты.

Контракты в Laravel – это интерфейсы. На их основе сгруппированы классы, которые реализуют эти интерфейсы и в которых определены методы указанные в интерфейсах (плюс другие нужные методы). Свои контракты (интерфейсы) можно хранить в папке app\Helpers\Contracts.

Пример создания своего контракта и классов реализующих данный интерфейс.

Создаем контракт SaveStr (файл app\Helpers\Contracts\SaveStr.php)
<?php
namespace App\Helpers\Contracts;

use Illuminate\Http\Request;
use App\User;

Interface SaveStr {
    
    public static function save(Request $request, User $user);
    
    public function checkData($array);
}

Классы реализующие интерфейс создаем в папке app\Helpers.

Файл app\Helpers\SaveEloquent.php
<?php
namespace app\Helpers;

use App\Helpers\Contracts\SaveStr;
use Illuminate\Http\Request;
use App\User;

class SaveEloquent implements SaveStr{
    
public static function save(Request $request, User $user){
    $obj = new self;
    $date = $obj->checkData($request->only('description', 'text'));
    $user->posts()->create($date);
}
    
public function checkData($array){
    //тут проверка данных
    return $array;
    }
}

Файл app\Helpers\SaveFile.php
<?php
namespace app\Helpers;

use App\Helpers\Contracts\SaveStr;
use Illuminate\Http\Request;
use App\User;
use Illuminate\Support\Facades\Storage;

class SaveFile implements SaveStr{
    
public static function save(Request $request, User $user){
    $obj = new self;
    $date = $obj->checkData($request->only('description', 'text'));
    $str = $date['description'] . ' | ' . $date['text'];

    /*
     * фасад для работы с файлами
     * метод prepend() вставляет данные в конец файла
     */
    Storage::prepend('str.txt', $str);
}
    
public function checkData($array){
    //тут проверка данных
    return $array;
    }
}
Итак, у нас готов контракт SaveStr. Практическое применение будет описано ниже.

Создание сервис-провайдера.
Сервис-провайдеры предназначены для регистрации сервисов (классов) в сервис-контейнере (глобальном объекте App).
Сервис-провайдеры позволяют внедрять зависимости (объекты нужных классов) в нужные методы:
public function form(Request $request, SaveStr $save){}
при этом, автоматически, создаются объекты указанных зависимостей и дальше в коде мы работаем с одним и тем же объектом при повторном внедрении зависимости в других методах (т.к. обычно используется шаблон проектирования синглтон).

В файле config\app.php в массиве providers перечислены классы сервис-провайдеров, которые регистрируют в сервис-контейнере (глобальном объекте App) определенный сервис сразу или при первом вызове метода register() - зависит от того, является ли провайдер «отложенным».

Все сервис-провайдеры должны наследовать специальный класс ServiceProvider.

В большинстве сервис-провайдеров есть методы register() и boot(). В методе register() служит исключительно для привязки нужных классов в сервис-контейнер.

Для создания файла сервис-провайдера с названием SaveStrServiceProvider выполнить в консоли:
php artisan make:provider SaveStrServiceProvider
создастся соответствующий файл в папке app\Providers который будет иметь пустые методы boot() и register().


Регистрация сервиса в сервис-контейнере (App).
Для этого используетс метод register(), который выполняется при начальной загрузке приложения для каждого сервис-провайдера.
<?php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Helpers\SaveEloquent;
use App\Helpers\SaveFile;

class SaveStrServiceProvider extends ServiceProvider
{

    public function boot()
    {
        //
    }

    public function register()
    {
        $this->app->singleton('App\Helpers\Contracts\SaveStr', function(){
        return new SaveEloquent();
        });
    }
}
или аналогичный вариант привязки в методе register():
App::singleton(SaveStr::class, function(){
    return new SaveEloquent();
});

Так же можно использовать метод bind() вместо singleton(), если не нужно использовать одноименный паттерн.
Данные методы позволяют выполнить в анонимной функции дополнительный код и преобразования. Если это не требуется то привязку можно осуществить так:
public function register()
{
    App::singleton(SaveStr::class, SaveEloquent::class);
}

В методы singleton() и bind() первым параметром передается название контракта (интерфейса) который регистрируем, а в качестве второго параметра – анонимная функция, возвращающая один из классов реализующих данный интерфейс.
Лучше использовать метод singleton() т.к. он соответствует шаблону проектирования «синглтон» - то есть создает объект указанного класса только 1 раз, а при последующих обращениях возвращает один и тот же объект.

Можно получить доступ к сервис-контейнеру из сервис-провайдера:
$this->app



Регистраця сервис-провайдера.

Все сервис-провайдеры нужно зарегистрировать в файле config\app.php в массиве providers. В данном случае добавляем в конец массива:
/*
 * My Service Providers
 */
App\Providers\SaveStrServiceProvider::class,

Пример использования.
Допустим нужно использовать данный сервис-провайдер при сохранении данных введенных пользователем в форме. Например метод form() соответствующего контроллера получает, проверяет и сохраняет данные.
В таком случае, нужно в данный метод передать зависимости:
  • $request - стандартная зависимость для таких случаев – объект запроса;
  • $save – объект класса SaveEloquent, реализующего интерфейс SaveStr.

use App\Helpers\Contracts\SaveStr;
use Auth;
use App\User;
…

public function form(Request $request, SaveStr $save){

    if($request->isMethod('post')){

        $save->save($request, User::find(4));
        // или для текущего, аутентифицированного пользователя:
        // $save->save($request, Auth::user());
    }
…

В сервис-провайдере у нас прописан класс SaveEloquent для реализации контракта (интерфейса) SaveStr. Поэтому сохранение будет проводиться в БД. Для того, чтобы использовался другой класс, например SaveFile (сохранение в файл) достаточно изменить в методе register() класса SaveStrServiceProvider название привязываемого класса:
public function register()
{
    App::singleton(SaveStr::class, function(){
        return new SaveFile();
    });  
}

Используя сервис провайдер, можно зарегистрировать в сервис-контейнере уже существующий объект:
public function register()
{
    $obj = new SaveEloquent();
    $this->app->instance('App\Helpers\Contracts\SaveStr', $obj); 
}

Обратиться к зарегистрированному сервису из сервис-провайдера:
$this->app['App\Helpers\Contracts\SaveStr'];
или
$this->app->make('App\Helpers\Contracts\SaveStr')
Метод boot() всех классов сервис-провайдеров вызывается автоматически после регистрации всех сервисов. Таким образом в данный метод можно внедрять любую зависимость.

Можно получить нужный сервис из сервис-контейнера без внедрения зависимости:
App::make('App\Helpers\Contracts\SaveStr')
тут указывается строка к которой была привязана анонимная функция из метода register().

Если привязать, например, к строковому ключу 'Save' (при использовании фасадов)
public function register()
{
    App::singleton('Save', function(){
        return new SaveEloquent();
    });  
}
то получить тот же самый объект можно так:
App::make('Save')
а вот внедрение зависимости работать не будет. Чтобы работало нужно создать фасад.


ФАСАДЫ.

Фасады предоставляют легкий доступ к классам зарегистрированным в сервис-контейнере. В целом, можно обходиться и без них, внедряя зависимость в методы или получая объект нужного сервиса прямо в коде из глобального объекта App. Фасады стоит создавать для часто используемых сервисов, для упрощения доступа к их методам.

Пример доступа к методам используется статический интерфейс:
Auth::user()
можно сказать, что тут вызывается метод user() фасада Auth. На самом деле, обращение идет к алиасу, который перенаправляет вызов на класс фасада Auth, а сам фасад не имеет данного метода, да и сам метод вовсе не статический… Вот как все запутано :) Немного проясним это ниже.

При работе с фасадами используются алиасы, которые существуют в глобальной области видимости. Подключение:
use Auth;

Доступ к алиасу получаем из любого метода, внедрять зависимость не нужно, ведь фасады это как раз альтернатива внедрению зависимостей.

Фактически результат одинаковый с тем что мы получили при внедрении зависимости. Вместо того чтобы писать:
public function form(Request $request, SaveStr $save){

    if($request->isMethod('post')){
        $save->save($request, User::find(4));
    }
…
понадобится:
public function form(Request $request){

    if($request->isMethod('post')){
        SaveStr::save($request, User::find(4));
    }
…

где SaveStr – алиас для фасада, который будет ссылаться на ячейку в сервис контейнере, к которой привязан нужный класс.

Создание фасада.
Файл app\Helpers\Facades\SaveStr.php:
<?php
namespace App\Helpers\Facades;
use Illuminate\Support\Facades\Facade;

class SaveStr extends Facade{
    
    protected static function getFacadeAccessor()
    {
        return 'save';
    }
}

Класс фасада должен наследовать от родительского класса Facade и переопределять метод getFacadeAccessor().
Данный метод должен возвращать ключ (строку), к которому привязывается класс в сервис-провайдере. Ключ может быть абсолютно любой строкой, при работе с фасадом обращаться нужно будет не к нему, а к алиасу.

Указываем данный ключ в сервис-провайдере SaveStrServiceProvider:
public function register()
{
    App::singleton('save', SaveFile::class);
}

Создаем алиас в файле config\app.php в массиве aliases:
'Save' => App\Helpers\Facades\SaveStr::class

Тут, в качестве ключа указывается алиас с которым работает пользователь, а в качестве значения класс фасада.

Теперь вызов любого метода класса-сервиса (зарегистрированного в сервис-контейнере) выглядит так:
use Save;
…
Save::save($request, Auth::user());

Основные задачи фасада:

  • возвращает строковый ключ по которому зарегистрирован определенный сервис (класс) – метод getFacadeAccessor(). Строковый ключ, обычно, делается коротким и по-смыслу связанным с действием привязанного класса. Таким образом, фасад упрощают доступ к объекту нужного сервиса.
  • позволяет обращаться ко всем методам сервиса как к статическим. Для этого используется метод __callStatic($method, $args) из родительского класса Facade, который перенаправляет вызов методов на класс зарегистрированный в сервис-контейнере с помощью сервис-провайдера.


Посмотреть зарегистрированные в сервис-контейнере (глобальном объекте App) сервис-провайдеры и в целом свойства глобального объекта Application можно используя функцию-помощник app():
dump(app());
у которого в свойстве bindings находится массив, где ключ массива – это алиас или контракт, а в значении можно посмотреть в т.ч. привязанный сервис-провайдер.