К основному контенту

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




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

Я сама не так давно осознала необходимость этих паттернов для разработки и поспособствовал тому проект на Symfony.
Можно много лет успешно кодить приложения на Laravel среднего уровня сложности и спокойно обходиться без сервис провайдеров, кастомных фасадов и понимания как это работает. Проверено на своем опыте :)
Но в один момент мне стало стыдно перед собой за подобную лень и безалаберность. И я решила сесть и разобраться. Этот прием реально работает! Во всем в программировании достаточно сесть и разобраться. И все оказывается доступно для понимания. 
Все эти термины – это лишь красивые названия в доску простых механизмов, и когда вы поймете насколько эти механизмы простые – вы себе не простите такого долгого неознакомления с ними.
Не повторяйте мою ошибку. Сядьте и разберитесь. И сейчас я попытаюсь вам в этом помочь. Возможно, мой стиль статей не отличается точностью подачи а больше имеет публицистичный характер, но тем, кому нужен учебник – велкам в доку, суше некуда ;)

Оговорюсь сразу – понять как работают все эти механизмы можно с любым уровнем знаний и опыта в программировании. Но вот понять на кой это использовать, вот действительно почувствовать, проникнуться необходимостью и воскликнуть «Как я раньше жил без этого?!» можно спустя только опыт разработки сложных приложений.

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

Контракты


Это просто интерфейс. Все. Это все что нужно знать. Если вы знаете как работают интерфейсы и наследование в PHP – вы все знаете о контрактах.
Если говорить правильно – то контракты – мощный инструмент, позволяющий разграничить высокоуровневую логику от деталей реализации и исполнителей.

Если на пальцах без замашки на гениальность – у вас есть сохранение картинок в приложении через облачное хранилище и непосредственно на диск. И так вышло, что вам нужно использовать оба режима сохранения одновременно. Чтобы не путаться - вы создаете интерфейс с перечнем методов сохранения и в дальнейшем наследуете его под разные реализации. А вдруг добавится сохранение еще на одно облако? Не беда, всегда можно создать еще один реализатор контракта.

Размещение контрактов произвольное, но я бы советовала их складывать в отдельную папку в App/Contracts.
Пример контракта:
SaveImage.php

interface SaveImage{

      public static function save();

      public static function delete();

}

Реализаторы: размещение произвольное, но я бы советовала их складывать в отдельную папку в App/Helpers:

Реализатор под сохранение на диск
SaveImageDisk.php

class SaveImageDisk implements SaveImage{

    public static function save (){
           //process saving on disk
    }

    public static function delete(){
           //process deleting from disk
    }
}

Реализатор под сохранение на облачный сервер:
SaveImageAws.php

class SaveImageAws implements SaveImage{

      public static function save (){
           //process saving on aws
      }

      public static function delete(){
           //process deleting from aws
      }
}

Не забывайте namespace и все use относительно вашего приложения!
При необходимости – просто используем тот или иной реализатор, например:
SaveImageDisk::save();
SaveImageAws::delete();

Service Providers aka Поставщики услуг


Окей, гугл, а если мне не надо параллельно использовать обе реализации сохранения картинок, а надо просто переключать легко и быстро режим с «сохранение на диск» на «сохранение на облако» и наоборот и без вот этого унылого по всему проекту ctrl+shift+F с автозаменой?

Ты по адресу J Страшные слова Service Provider сейчас станут для тебя манной небесной. А за использование подобных паттернов сразу +100 к ЧСВ :)
Сервис провайдер как раз и предоставляет эту самую возможность легкого переключения между реализациями. Не верите? Ну смотрите.

Давайте сразу создадим сервис провайдер и посмотрим что в нем:
php artisan make:provider SaveImageServiceProvider

В папке Providers найдем новый класс, нас интересует функция register, добавим в нее код:
SaveImageServiceProvider.php

public function register (){
     $this->app->bind('App\Contracts\SaveImage', function(){
            return new saveImageAws();
            //return new saveImageDisk();
     });
}

Мы говорим приложению, что когда происходит обращение к 'App\Contracts\SaveImage’ – пусть возвращает новый объект класса-реализатора saveImageAws.

То есть, в коде мы используем такую форму:
App\Contracts\SaveImage::save();

А метод save будет от класса saveImageAws. В провайдере легко изменить реализатор на saveImageDisk, просто раскомментировав его и закомментировав прежний.
Здесь происходит своего рода подмена понятий. Мы будто обращаемся сразу к интерфейсу и здесь есть что-то от полиморфизма, но нет, все намного банальнее, мы всего лишь обращаемся к ключу-строке в сервис контейнере, а не к интерфейсу в папке Contracts.

Завершим процесс регистрации нового провайдера внеся его в массив ‘providers’ в app.php, как раз в тот самый сервис контейнер о котором ниже.
Кстати, если вы хотите, чтобы создавался только один объект класса реализатора, вы хотите быть уверены в этом – используйте вместо bind – singleton.

Service Container


Это глобальный массив-контейнер всех доступных сервис – провайдеров. Таких же, как мы создали выше. Да-да, вот он, dependency-injection, легендарный. Есть множество способов внедрения зависимостей в фреймворке и это один из них.

Facades


А вот если ты, мой друг, придирчивый педант и в курсе, что писать static методы не по фэншую и ты хочешь чтобы твой код был легок к пониманию и интуитивно понятен - добро пожаловать в мир фасадов! 
Это вот прямо мое любимое. Тоже связано с подменой понятий (ну или с синтаксическим сахаром). Если правильно говорить, то фасады предоставляют статический интерфейс классам, зарегистрированным в сервис-контейнере.
Проще говоря, если у вас есть не static функции в классе - вы сможете обращаться к ним как к static в сокращенном синтаксисе без создания объекта класса с использованием фасадов. Как это происходит?  Класс Facade использует магический метод PHP __callStatic() для перенаправления вызовов методов с вашего фасада на полученный объект.


Переделаем наши классы контракта и реализаторов кошерно:

SaveImage.php

interface SaveImage{

    public function save();

    public function delete();
}

SaveImageDisk.php

class SaveImageDisk implements SaveImage{

    public function save (){
          //process saving on disk
    }

    public function delete(){
          //process deleting from disk
    }
}

SaveImageAws.php

class SaveImageAws implements SaveImage{

     public function save (){
           //process saving on aws
     }

     public function delete(){
           //process deleting from aws
     }
}

И приступим к созданию фасада в папке App/Facades:
SaveImage.php

class SaveImage extends Facade{
    protected static function getFacadeAccessor(){
        return ’App\Contracts\SaveImage’;
    }
}

Мы возвращаем строковый ключ для доступа к определенной ячейке Сервис контейнера,  то, что мы привязали в сервис провайдере, а именно -  'App\Contracts\SaveImage'.

Чтобы обращаться к нашему новому фасаду по сокращенному алиасу, скрыть детали реализации и явное наличие механизма фасада - в app.php в массиве ‘aliases’ добавляем:
‘SaveImage’ => ‘App\Facades\SaveImage::class’

И теперь мы используем данный механизм как:
SaveImage::save();

Этот код эквивалентен следующему:
$app = app();
$app->make('SaveImage')->save();

Функция save будет по прежнему взята из класса-реализатора saveImageAws, хоть функция не static - мы теперь имеем возможность обращаться к ней как к static.
Не забывайте, SaveImage – это только алиас, строка. Необходимо добавить в use SaveImage.
Если вы не добавите в app.php в массив ‘aliases’ запись с ключом SaveImage то вы можете использовать свой "перегруженный" класс через фасад напрямую:
App\Facades\SaveImage::save();

Все вышеперечисленные механизмы актуальны для действительно больших приложений. В подавляющем большинстве случаев достаточно внедрения зависимостей через конструктор. Неумелое использование паттернов фасада и сервисов может вызвать бессонные ночи J В общем – экспериментируйте, учитесь, но используйте знания во благо и уместно. Помните, что скорее всего после вас еще будет кто-то править этот код :)

Комментарии

  1. я понимаю, что примеры выдуманные, но все же, почему у вас интерфейс декларирует статические методы?
    учитывая то, что этот материал в бОльшей степени для начинающих, зачем учить заведомо неверных примерах, которые на этапе интерпретации выдадут ошибки?
    но в целом за статью спасибо.

    ОтветитьУдалить
    Ответы
    1. Автор уже разбирается в фасадах и сервис провайдерах но ещё видимо не разобрался что такое интерфейсы, как говорится постигает науку с другого конца )))))

      Удалить
    2. "А вот если ты, мой друг, придирчивый педант и в курсе, что писать static методы не по фэншую и ты хочешь чтобы твой код был легок к пониманию и интуитивно понятен - добро пожаловать в мир фасадов! "

      Удалить
    3. Так вроде в конце статьи все было переписано без использования статических методов.

      Удалить
  2. Статья понравилась. Но в параграфе про фасады не понятно, почему в итоге реализуется именно saveImageAws, а не SaveImageDisk ?

    ОтветитьУдалить
    Ответы
    1. Потому что при вызове App\Contracts\SaveImage будет возвращаться обьект из сервис провайдера который зарегистрирован в контейнере. А в сервис-провайдере связь с saveImageAws. То есть через сервис-провайдер ты управляешь экземпляр какого класса возвращать по умолчанию при запросе контракта App\Contracts\SaveImage

      Удалить
  3. ахаха. у автора куча ошибок в решениях задач из учебника.

    ОтветитьУдалить
    Ответы
    1. автор писал эти решения задач 10 лет назад, а сейчас уже 5 лет как веслает.

      Удалить
  4. Негодуйте сколько хотите, но я наконец-то нашел статью, где простым языком объясняется архитектура Laravel. Спасибо автору за это!

    ОтветитьУдалить

Отправить комментарий

Популярные сообщения из этого блога

ВОПРОС К ЧИТАТЕЛЯМ

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

7 глава, Лафоре Р

*1. Напишите функцию reversit(), которая переворачивает строку (массив типа char). Используйте цикл for, который меняет местами первый и последний символы, затем следующие и т. д. до предпоследнего. Строка должна пере- даваться в функцию reversit() как аргумент. Напишите программу для выполнения функции reversit(). Программа долж- на принимать строку от пользователя, вызывать функцию reversit(), а за- тем выводить полученный результат. Используйте метод ввода, который позволяет использовать внутренние пробелы. Протестируйте программу на примере фразы «Аргентина манит негра».

6 глава, Лафоре Р

*1. Создайте класс Int, имитирующий стандартный тип int. Единственное поле этого класса должно иметь тип int. Создайте методы, которые будут устанавливать значение поля, равным нулю, инициализировать его целым значением, выводить значение поля на экран и складывать два значения типа Int. Напишите программу, в которой будут созданы три объекта класса Int, два из которых будут инициализированы. Сложите два инициализирован- ных объекта, присвойте результат третьему, а затем отобразите результат на экране.