Продвинутый Laravel: Контракты и Реализации

Источник: «Advanced Laravel: Contracts and Implementations»
Контракты и реализации — это мощные инструменты в Laravel, позволяющие определять стандартный интерфейс и писать код, который можно адаптировать к различным реализациям.

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

Контракт похож на деловое соглашение в том смысле, что это соглашение между сторонами. Контракты обычно имеют условия, которые являются условиями соглашения, которым обе стороны соглашаются следовать. Когда мы берём это и переводим в код, у нас получается что-то вроде этого:

namespace App\Contracts;

interface Dvr
{
public function play();

public function pause();
}

В этом контракте мы соглашаемся вести дела с DVR и условиями (методами) контракта, которые мы должны соблюдать, чтобы play() и pause() DVR. Как вы можете себе представить, есть несколько компаний предоставляющих услуги DVR. В Соединённых Штатах двумя крупнейшими DVR поставщиками являются Honeywell и Haydon.

Теперь давайте создадим службу API для Honeywell и Haydon.

app/Services/HoneywellApi.php:

namespace App\Services;

class HoneywellApi
{
public function pressPlay()
{
return 'Play Honeywell DVR';
}

public function pressPause()
{
return 'Pause Honeywell DVR';
}
}

app/Services/HaydonApi.php:

namespace App\Services;

class HaydonApi
{
public function play()
{
return 'Play Haydon DVR';
}

public function pause()
{
return 'Pause Haydon DVR';
}
}

Вы. вероятно, заметили, что имена методов, используемые HoneywellApi, отличаются от имён методов HaydonApi, но это совершенно нормально. Почему? Потому что мы не можем сделать всех поставщиков API и SDK одинаковыми. Теперь мы создадим реализацию для предоставления каждого API, соблюдая при этом контракт.

app/Implementations/Honeywell.php:

namespace App\Implementations;

use App\Contracts\Dvr;
use App\Services\HoneywellApi;

class Honeywell implements Dvr
{
public function __construct(protected HoneywellApi $api) {}

public function play()
{
return $this->api->pressPlay();
}

public function pause()
{
return $this->api->pressPause();
}
}

app/Implementations/Haydon.php:

namespace App\Implementations;

use App\Contracts\Dvr;
use App\Services\HaydonApi;

class Haydon implements Dvr
{
public function __construct(protected HaydonApi $api){}

public function play()
{
return $this->api->play();
}

public function pause()
{
return $this->api->pause();
}
}

Для использования одного из этих провайдеров, нужно создать контроллер вместе с маршрутом, указывающим на контроллер. Как вы скоро увидите, мы внедрим interface в конструктор. Это важная часть, поскольку она позволяет нам переключаться между поставщиками API, если что-нибудь понадобится. (Позже мы воспользуемся сервис-контейнером Laravel, чтобы собрать всё вместе)

app/Http/Controllers/DvrController.php:

namespace App\Http\Controllers;

use App\Contracts\Dvr;

class DvrController extends Controller
{
public function __construct(protected Dvr $dvr){}

public function play()
{
return $this->dvr->play();
}

public function pause()
{
return $this->dvr->pause();
}
}

Контроллер заботиться только о воспроизведении или приостановке DVR, его не волнует (и не должно заботить) то, кто будет предоставлять базовую услугу.

Двигаясь дальше, мы создадим маршрут для каждого метода контроллера в файле маршрутов api.php.

routes/api.php:

use Illuminate\Support\Facades\Route;

Route::get(‘dvr/play’, [\App\Http\Controllers\DvrController::class, ‘play’]);

Route::get(‘dvr/pause’,[\App\Http\Controllers\DvrController::class, ‘pause’]);

Когда мы запускаем браузер и переходим к нашему маршруту воспроизведения (http://example.dev/api/dvr/play), мы сталкиваемся со следующим:

Illuminate\Contracts\Container\BindingResolutionException
Target [App\Contracts\Dvr] is not instantiable while building [App\Http\Controllers\DvrController].

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

Давайте немного повеселимся. Мы войдём в наш AppServiceprovider и скажем Laravel, что когда приложение ищет реализацию Dvr, мы вернём ему реализацию Honeywell.

app/Providers/AppServiceProvider.php:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/

public function register(): void
{
$this->app->bind(
\App\Contracts\Dvr::class,
\App\Implementations\Honeywell::class
);
}

/**
* Bootstrap any application services.
*/

public function boot(): void
{
//
}
}

Теперь мы вернёмся на страницу, которую только что посещали, и обновим.

Play Honeywell DVR

Довольно круто, да? Теперь давайте посетим маршрут паузы (http://example.dev/api/dvr/pause), который возвращает:

Pause Honeywell DVR

Теперь предположим, что ваш менеджер приходит и заявляет, что мы собираемся перейти с Honeywell на Haydon, вам нужно просто обновить AppServiceProvider до:

app/Providers/AppServiceProvider.php:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/

public function register(): void
{
$this->app->bind(
\App\Contracts\Dvr::class,
\App\Implementations\Haydon::class
);
}

/**
* Bootstrap any application services.
*/

public function boot(): void
{
//
}
}

Посетим страницу маршрута воспроизведения (http://example.dev/api/dvr/play), который возвращает:

Play Haydon DVR

Теперь маршрут паузы (http://example.dev/api/dvr/pause) возвращает:

Pause Haydon DVR

Теперь давайте продвинемся ещё дальше. Представьте себе, что менеджер возвращается и говорит: Итак, мы говорили об этом, на самом деле мы хотим использовать как Honeywell, так и Haydon. Вы можете это сделать? А вы говорите: Я вас прикрою, босс!

Учитывая, что теперь мы будем предоставлять несколько поставщиков, давайте применим DRY к нашим контроллерам.

app/Http/Controllers/HaydonController.php:

namespace App\Http\Controllers;

class HaydonController extends DvrController {}

app/Http/Controllers/HoneywellController.php:

namespace App\Http\Controllers;

class HoneywellController extends DvrController {}

Как видите, они оба расширяют изначально созданный нами DvrController.

Давайте обновим файл маршрутов api.php, чтобы они соответствовали им:

routes/api.php:

use Illuminate\Support\Facades\Route;

Route::get('dvr/play', [\App\Http\Controllers\DvrController::class, 'play']);
Route::get('dvr/pause', [\App\Http\Controllers\DvrController::class, 'pause']);

Route::get('dvr/play/honeywell', [\App\Http\Controllers\HoneywellController::class, 'play']);
Route::get('dvr/pause/honewell', [\App\Http\Controllers\HoneywellController::class, 'pause']);

Route::get('dvr/play/haydon', [\App\Http\Controllers\HaydonController::class, 'play']);
Route::get('dvr/pause/haydon', [\App\Http\Controllers\HaydonController::class, 'pause']);

Когда мы посещаем конечную точку воспроизведения Honeywell (http://laravelcontractsandimplementations.test/api/dvr/play/honeywell), сталкиваемся с проблемой:

Play Haydon DVR

Сейчас, Laravel знает, что когда запрашиваем Dvr, мы возвращаем реализацию Haydon.

Итак, как нам это решить? Нужно предоставить AppServiceProvider дополнительный контекст:

app/Providers/AppServiceProvider.php:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/

public function register(): void
{
$this->app->bind(
\App\Contracts\Dvr::class,
\App\Implementations\Haydon::class
);

$this->app
->when(\App\Http\Controllers\HoneywellController::class)
->needs(\App\Contracts\Dvr::class)
->give(\App\Implementations\Honeywell::class);

$this->app
->when(\App\Http\Controllers\HaydonController::class)
->needs(\App\Contracts\Dvr::class)
->give(\App\Implementations\Haydon::class);
}

/**
* Bootstrap any application services.
*/

public function boot(): void
{
//
}
}

Теперь, когда мы посещаем конечный точки для Honeywell или Haydon, получаем правильные данные.

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

Дополнительные материалы

Предыдущая Статья

Новое в Symfony 6.3 — Атрибуты исключений HTTP

Следующая Статья

Новое в Symfony 6.3 — Сопоставление данных Request с типизирова­н­ны­ми объектами