Продвинутый 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. И я призываю вас продолжать изучать и экспериментировать с этим увлекательным фреймворком!