Работа со сторонними API может вызывать затруднения; мы получаем ответы JSON, которые в PHP будут представлены в виде простого старого массива, и мы также отправляем данные в виде массивов. Мы теряем большую часть контекста и возможность создавать что-то с большим опытом разработчиков. Что, если я скажу тебе, что так быть не должно?Что, если я скажу вам, что не требуется много усилий, чтобы создать что-то, что добавит больше контекста и улучшит вашу работу со сторонними API? Не верите мне? Давайте посмотрим.
В этом руководстве мы рассмотрим интеграцию вымышленного стороннего API с вложенными данными внутри — самого запутанного из API.Мы хотим иметь возможность получать данные из API, а также иметь возможность отправлять данные в этот API без необходимости создавать эти неприятные массивы, к которым мы привыкли.
Лучший способ улучшить свой опыт здесь — использовать самую последнюю версию PHP и сторонний пакет, такой как Laravel Saloon или Laravel Transporter, но иногда вы не хотите использовать весь пакет только для того, чтобы сделать пару запросов к API, верно? Если бы мы это сделали, все наше приложение было бы хрупким и зависело бы от такого большого количества стороннего кода, что мы могли бы также использовать конструктор веб-сайтов.
API, с которым мы собираемся интегрироваться, — вымышленный, который сообщает нам историю болезни наших пользователей/пациентов. Представьте себе API, с которым вы работаете, и хотите иметь возможность добавлять в него новые данные — скажем, веб-приложение или мобильное приложение врача общей практики, и вы идете на прием, и им нужно зарегистрировать любые дополнительные проблемы или примечания к вашему файл.Они могут захотеть проверить вашу историю и посмотреть, что в данный момент находится в вашем файле.
Лучший способ начать с этого — создать класс обслуживания, и в зависимости от количество API, с которыми вам нужно интегрироваться, обычно укажет вам правильное направление для интеграции.Я собираюсь построить это, как если бы мне нужно было интегрироваться с несколькими API-интерфейсами — скажем, данные о психическом здоровье находятся в совершенно отдельном API. Так что нам нужно будет интегрироваться с этим в какой-то момент в будущем. Первое, что мы хотим сделать в нашем каталоге app
, — это создать новое пространство имен для Services
.чтобы у нас было место для наших служебных подключений. Внутри мы создадим новое пространство имен для каждой службы, которую нам нужно интегрировать, будь то внешние или внутренние службы. Хорошо сгруппировать их таким образом, поскольку это дает вам стандарт — если вам нужно расширить, нет вопроса, где это должно быть; создать новую интеграцию службы в App\Services\
и все готово.
Итак, наш вымышленный API называется medicaltrust
, это случайное имя, которое я придумал при написании этого руководства - если это уже действительно API, то прошу прощения. Это руководство никоим образом не отражает и не основано на этом API. Теперь создайте новый каталог/пространство имен app/Services/MedicalTrust
;внутри здесь мы хотим создать класс, который будет обрабатывать нашу интеграцию - если вы читали мой учебник на Laravel Saloon, тогда считайте это соединителем. Класс, который будет обрабатывать основное подключение к API. Я позвонил в свой MedicalTrustService
потому что мне нравится быть явным в моем именовании, где я могу быть, и убедиться, что оно выглядит примерно так:
declare(strict_types=1);
namespace App\Services\MedicalTrust;
class MedicalTrustService
{
public function __construct(
private readonly string $baseUrl,
private readonly string $apiToken,
) {}
}
Итак, нам нужны 2 вещи для этого API, базовый URL и токен API — ничего необычного. Внутри config/services.php
добавьте следующий блок:
return [
'medical-trust' => [
'url' => env('MEDICAL_TRUST_URL'),
'token' => env('MEDICAL_TRUST_TOKEN'),
]
];
Я считаю, что при добавлении параметров конфигурации для сторонних сервисов всегда лучше хранить их в одном месте, даже если вам нужно много параметров конфигурации. Поддержание согласованного стандарта для решения этой проблемы жизненно важно при работе с API, поскольку стандарты являются основой большинства современных API. Чтобы запустить наш сервис, нам нужно добавить новую запись в наш app/Providers/AppServiceProvider.php
, чтобы указать ему зарегистрировать наш класс службы в качестве зависимости в контейнере для создания класса. Итак, добавьте в метод загрузки следующее:
public function boot(): void
{
$this->app->singleton(
abstract: MedicalTrustService::class,
concrete: fn () => new MedicalTrustService(
baseUrl: strval(config('services.medical-trust.url')),
apiToken: strval(config('services.medical-trust.token')),
),
);
}
Все, что мы здесь делаем, это добавляем новый синглтон в контейнер, и когда мы запрашиваем MedicalTrustService< /code> если мы еще не построили его раньше - тогда создайте его, передав эти значения конфигурации. Мы используем
strval
, чтобы убедиться, что это строка, так как config()
по умолчанию возвращает смешанный код. Эта часть была относительно простой, поэтому давайте перейдем к тому, как мы можем создавать согласованные запросы для отправки.
Отправка запросов — единственная цель интеграции с API, так что вы хотите убедиться, что вы подходите к нему разумно.Как я сказал в начале руководства, мы подойдем к этому так, как если бы в долгосрочной перспективе мы интегрировались с более чем одним API. Итак, что нам нужно сделать, это абстрагировать функциональность от нашего сервиса, где она используется совместно. Лучший способ сделать это — использовать трейты в PHP — и если вы будете следовать правилу 3, то это будет иметь смысл.Вы должны абстрагироваться, если вы повторяете один и тот же код или примерно один и тот же код более двух раз. Какие вещи мы хотели бы абстрагировать или иметь некоторый уровень контроля? Одним из них является создание нашего базового шаблона запроса, обеспечивающего его правильную настройку. Отправка запросов — это другое — нам нужно убедиться, что мы можем контролировать запросы, которые мы можем отправлять для каждого API в реальности.Итак, давайте создадим несколько трейтов, чтобы сделать это немного проще.
Сначала мы создадим трейт, который управляет построением базового запроса, и у него может быть несколько опций в черта подхода. Они будут жить в пространстве имен Services, но в пространстве имен Concerns
.В Laravel трейты в Eloquent называются Concerns, поэтому здесь мы будем соответствовать соглашению об именах Laravel. Создайте новый трейт с именем app/Services/Concerns/BuildsBaseRequest.php
и добавьте в него следующий код:
declare(strict_types=1);
namespace App\Services\Concerns;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
trait BuildBaseRequest
{
public function buildRequestWithToken(): PendingRequest
{
return $this->withBaseUrl()->timeout(
seconds: 15,
)->withToken(
token: $this->apiToken,
);
}
public function buildRequestWithDigestAuth(): PendingRequest
{
return $this->withBaseUrl()->timeout(
seconds: 15,
)->withDigestAuth(
username: $this->username,
password: $this->password,
);
}
public function withBaseUrl(): PendingRequest
{
return Http::baseUrl(
url: $this->baseUrl,
);
}
}
Здесь мы создаем стандартный метод, который создаст ожидающий запрос с набором базового URL-адреса — это основано на соблюдении стандарта внедрения базового URL-адреса в конструктор класса службы — поэтому важно следовать стандарт или образец. Затем у нас есть необязательные методы для расширения запроса с помощью токена или дайджест-аутентификации.Такой подход позволяет нам быть очень гибкими, и мы не делаем ничего экстремального или того, что могло бы работать лучше в другом месте. Добавление этих методов в каждую службу — это хорошо, но по мере того, как вы начинаете интегрироваться со все большим количеством API, крайне важно иметь централизованный способ сделать это.
Наш следующий набор проблем/особенностей будет заключаться в том, чтобы помочь контролировать то, как мы отправляем запросы к сторонним API — мы хотим иметь несколько проблем/особенностей, чтобы ограничить типы запросов, которые мы можем отправлять. Первым будет app/Services/Concerns/CanSendGetRequest.php
declare(strict_types=1);
namespace App\Services\Concerns;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
trait CanSendGetRequest
{
public function get(PendingRequest $request, string $url): Response
{
return $request->get(
url: $url,
);
}
}
Далее создадим app/Services/ Проблемы/CanSendPostRequest.php
:
declare(strict_types=1);
namespace App\Services\Concerns;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
trait CanSendPostRequest
{
public function post(PendingRequest $request, string $url, array $payload = []): Response
{
return $request->post(
url: $url,
data: $payload,
);
}
}
Как видите, мы встраиваем глаголы HTTP в трейты, чтобы быть конкретными в нашем элементе управления, чтобы гарантировать, что запросы всегда отправляются правильно. Для некоторых проектов это будет абсолютным излишеством, но представьте, что вы интегрируетесь с более чем 10 API; вдруг этот подход не так глуп.
Давайте еще раз подумаем о самом классе обслуживания.Хотим ли мы построить этот сервисный класс так, чтобы у него было 10-20+ методов, чтобы обеспечить доступ ко всем конечным точкам API? Скорее всего нет; это звучит довольно грязно, верно? Вместо этого мы создадим определенные классы ресурсов, которые мы можем построить с помощью класса службы или внедрить непосредственно в методы. Поскольку это вымышленный медицинский API, для начала мы рассмотрим стоматологические записи. Вернемся к нашему MedicalTrustService
:
declare(strict_types=1);
namespace App\Services\MedicalTrust;
use App\Services\Concerns\BuildBaseRequest;
use App\Services\Concerns\CanSendGetRequests;
use App\Services\Concerns\CanSendPostRequests;
class MedicalTrustService
{
use BuildBaseRequest;
use CanSendGetRequests;
use CanSendPostRequests;
public function __construct(
private readonly string $baseUrl,
private readonly string $apiToken,
) {}
public function dental(): DentalResource
{
return new DentalResource(
service: $this,
);
}
}
У нас есть новый метод под названием dental
, который возвращает класс ресурсов, специфичный для конечных точек стоматологических ресурсов. Мы внедряем службу в конструктор для вызова методов службы, таких как get
или post
или buildRequestWithToken
. Давайте теперь посмотрим на этот класс и посмотрим, как мы должны его построить app/Services/MedicalTrust/Resources/DentalResource.php
declare(strict_types=1);
namespace App\Services\MedicalTrust\Resources;
class DentalResource
{
public function __construct(
private readonly MedicalTrustService $service,
) {}
}
Хорошо и просто, правда. Мы можем решить это из контейнера, так как он не требует ничего особенного. Итак, для стоматологических записей, скажем, мы хотим перечислить все записи и добавить новую запись — как это будет выглядеть с использованием массивов?
declare(strict_types=1);
namespace App\Services\MedicalTrust\Resources;
use Illuminate\Http\Client\Response;
class DentalResource
{
public function __construct(
private readonly MedicalTrustService $service,
) {}
public function list(string $identifier): Response
{
return $this->service->get(
request: $this->service->buildRequestWithToken(),
url: "/dental/{$identifier}/records",
);
}
public function addRecord(string $identifier, array $data = []): Response
{
return $this->service->post(
request: $this->service->buildRequestWithToken(),
url: "/dental/{$identifier}/records",
payload: $data,
);
}
}
Как видите, это относительно простой.Мы передаем идентификатор для идентификации пользователя, затем с помощью сервиса отправляем запрос get
или post
, используя buildRequestWithToken
в качестве базового запроса к использовать. Однако такой подход ко мне ошибочен. Во-первых, мы возвращаем только ответ как есть. Здесь нет контекста, нет информации — просто массив.Теперь это нормально, особенно при создании SDK, но есть вероятность, что нам нужно немного больше информации об ответах. А как же запрос? Да, мы, вероятно, сделали некоторую проверку входящего запроса с помощью проверки HTTP, но как насчет контроля данных, которые мы отправляем в API? Давайте посмотрим, как мы можем справиться с этим, чтобы массивы остались в прошлом, а контекстные объекты — в будущем.
Прежде чем полностью удалить массивы, нам нужно понять, как отображаются данные и что создает эти данные. Давайте посмотрим на пример полезной нагрузки для стоматологических карт:
{
"id": "1234-1234-1234-1234",
"treatments": {
"crowns": [
{
"material": "porcelain",
"location": "L12",
"implemented": "2022-07-10"
}
],
"fillings": [
{
"material": "white",
"location": "R8",
"implemented": "2022-07-10"
}
]
}
}
Итак, у нас есть идентификатор пациента, а затем объект лечения. Объект лечения имеет коронки и пломбы, полученные пациентом. На самом деле, это было бы намного больше и содержало бы гораздо больше информации.Коронка и пломба представляют собой набор примененных стоматологических исправлений - используемый материал, зуб с использованием стоматологического жаргона и дата проведения лечения. Теперь давайте сначала посмотрим на это в формате массива:
[
'id' => '1234-1234-1234-1234',
'treatment' => [
'crowns' => [
[
'material' => 'porcelain',
'location' => 'L12',
'implemented' => '2022-07-10',
],
],
'fillings' => [
[
'material' => 'white',
'location' => 'R8',
'implemented' => '2022-07-10',
],
]
]
];
Не так хорошо, верно? Да, он относительно хорошо представляет данные, но представьте, что вы пытаетесь использовать эти данные в пользовательском интерфейсе или где-то еще. Какова альтернатива?Что мы можем сделать, чтобы исправить это? Во-первых, давайте создадим объект, представляющий лечение зубов: app/Services/MedicalTrust/DataObjects/DentalTreatment.php
declare(strict_types=1);
namespace App\ServicesMedicalTrust\DataObjects;
use Illuminate\Support\Carbon;
class DentalTreatment
{
public function __construct(
public readonly string $material,
public readonly string $location,
public readonly Carbon $implemented,
) {}
public function toArray(): array
{
return [
'material' => $this->material,
'location' => $this->location,
'implemented' => $this->implemented->toDateString(),
];
}
}
Вместо этого мы сейчас иметь класс, который можно построить - что, глядя на него, мы знаем, что это значит. Мы понимаем, что этот объект или этот набор данных имеет отношение к стоматологическому лечению.Давайте поднимемся на уровень выше и посмотрим на сами методы лечения: app/Services/MedicalTrust/DataObjects/Treatments.php
declare(strict_types=1);
namespace App\Services\MedicalTrust\DataObjects;
class Treatments
{
public function __construct(
public readonly Crowns $crowns,
public readonly Fillings $fillings,
) {}
public function toArray(): array
{
return [
'crowns' => $this->crowns->toArray(),
'fillings' => $this->fillings->toArray(),
];
}
}
Снова, как и раньше, мы иметь определенный класс, который представляет все процедуры, которые может предпринять пользователь, и его можно расширить, включив в него другие. Допустим, теперь мы хотим предложить шпон — мы можем добавить новое свойство и создать для этого объект данных.Давайте посмотрим, как может выглядеть такой объект, как короны: app/Services/MedicalTrust/DataObjects/Crowns.php
declare(strict_types=1);
namespace App\Services\MedicalTrust\DataObjects;
use Illuminate\Support\Collection;
class Crowns
{
public function __construct(
public Collection $treatments,
) {}
public function toArray(): array
{
return $this->treatments->map(fn (DentalTreatment $treatment) =>
$treatment->toArray(),
)->toArray();
}
}
На этот раз наш конструктор просто содержит коллекцию процедур, которые можно добавить. Мы могли бы указать это с помощью docblocks, чтобы гарантировать, что мы добавляем к нему только DentalTreatments, если захотим.Затем, когда мы приводим это к массиву, мы сопоставляем обработки (тип, подсказывающий каждый элемент) и приводим обработку к массиву, а затем, наконец, приводим все это к массиву. Причина, по которой у нас есть метод toArray
в наших классах, заключается в том, что мы можем легко сохранить его в базе данных с помощью Eloquent: Treatment::query()->create($treatment-> ;к массиву());
но также для отображения и таблиц CLI. Удобная вещь, которую я заметил, хорошо работает с этими объектами данных.
Итак, как мы можем использовать их? Конечно, создание их вручную в сервисе заставит его чувствовать себя раздутым? Мне нравится создавать эти объекты с помощью фабрики объектов данных, которая принимает данные в виде массива и возвращает их в виде объекта. Давайте создадим один для лечения зубов (самый нижний): app/Services/MedicalTrust/DataFactories/DentalTreatmentFactory.php
declare(strict_types=1);
namespace App\Services\MedicalTrust\DataFactories;
use App\Services\MedicalTrust\DataObjects\DentalTreatment;
use Illuminate\Support\Carbon;
class DentalTreatmentFactory
{
public function make(array $attributes): DentalTreatment
{
return new DentalTreatment(
material: strval(data_get($attributes, 'material')),
location: strval(data_get($attributes, 'location')),
implemented: Carbon::parse(strval($attributes, 'implemented'));
);
}
}
Итак, у нас есть фабрика с методом make, который принимает массив атрибутов. Затем мы создаем новый объект Dental Treatment с помощью помощника Laravel data_get
и убеждаемся, что мы привели его к правильному типу. Что касается реализованного
свойство, мы используем Carbon для анализа переданной даты. Теперь, сделав еще один шаг, давайте посмотрим, как мы можем создавать ворон: app/Services/MedicalTrust/DataFactories/CrownsFactory.php
:
declare(strict_types=1);
namespace App\Services\MedicalTrust\DataFactories;
use App\Services\MedicalTrust\DataObjects\Crowns;
use Illuminate\Support\Carbon;
class CrownsFactory
{
public function make(array $treatments): Crowns
{
return new Crowns(
treatments: new Collection(
items: $treatments,
)->map(fn ($treatment): DentalTreatment =>
(new DentalTreatmentFactory)->make(
attributes: $treatment,
),
),
);
}
}
Итак, это немного сложнее, чем предыдущее. На этот раз мы передаем множество процедур и обновляем коллекцию.Затем, когда у нас есть наша коллекция, мы хотим пройтись по каждому лечению и использовать стоматологическую фабрику, чтобы превратить ее в объект стоматологического лечения. Чтобы с этим было проще работать, мы могли бы добавить к нашим фабрикам данных статический метод с именем new
, который принимает массив и просто вызывает метод make:
declare(strict_types=1);
namespace App\Services\MedicalTrust\DataFactories;
use App\Services\MedicalTrust\DataObjects\DentalTreatment;
use Illuminate\Support\Carbon;
class DentalTreatmentFactory
{
public static new(array $attributes): DentalTreatment
{
return (new static)->make(
attributes: $attributes,
);
}
public function make(array $attributes): DentalTreatment
{
return new DentalTreatment(
material: strval(data_get($attributes, 'material')),
location: strval(data_get($attributes, 'location')),
implemented: Carbon::parse(strval($attributes, 'implemented'));
);
}
}
Это сделает нашу фабрику Crows Factory намного чище:
declare(strict_types=1);
namespace App\Services\MedicalTrust\DataFactories;
use App\Services\MedicalTrust\DataObjects\Crowns;
use Illuminate\Support\Carbon;
class CrownsFactory
{
public function make(array $treatments): Crowns
{
return new Crowns(
treatments: new Collection(
items: $treatments,
)->map(fn ($treatment): DentalTreatment =>
DentalTreatmentFactory::new(
attributes: $treatment,
),
),
);
}
}
Или мы могли бы даже сделать его еще проще для нас, сказав DentalTreatment Factory создать коллекцию для нас:
declare(strict_types=1);
namespace App\Services\MedicalTrust\DataFactories;
use App\Services\MedicalTrust\DataObjects\DentalTreatment;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
class DentalTreatmentFactory
{
public static collection(array $treatments): Collection
{
return (new Collection(
items: $treatments,
))->map(fn ($treatment): DentalTreatment =>
static::new(attributes: $treatment),
);
}
public static new(array $attributes): DentalTreatment
{
return (new static)->make(
attributes: $attributes,
);
}
public function make(array $attributes): DentalTreatment
{
return new DentalTreatment(
material: strval(data_get($attributes, 'material')),
location: strval(data_get($attributes, 'location')),
implemented: Carbon::parse(strval($attributes, 'implemented'));
);
}
}
Это позволило бы нам упростить завод Crowns, который гораздо дальше:
declare(strict_types=1);
namespace App\Services\MedicalTrust\DataFactories;
use App\Services\MedicalTrust\DataObjects\Crowns;
use Illuminate\Support\Carbon;
class CrownsFactory
{
public function make(array $treatments): Crowns
{
return new Crowns(
treatments: DentalTreatmentFactory::collection(
treatments: $treatments,
),
);
}
}
Суть здесь в том, что ограничение на то, что делает вашу жизнь проще. Возможно, вам не нужно заходить так далеко, а может быть, ваш API немного более плоский, поэтому реализовать такой подход несложно.Но если мы возьмем подход и применим то, что работает для нас, мы получим более контекстуальный ответ API и сможем намного легче понять ответ и работать с ним намного проще.
Отступая назад, мы также хотим иметь возможность создавать новые методы лечения с помощью API.Мы хотим иметь возможность заполнить форму или что-то подобное и отправить данные в API, чтобы зарегистрировать, что мы внедрили новую обработку. Для этого нам нужно отправить запрос на публикацию через наш DentalResource
с помощью метода addRecord
. Это не страшно, но давайте посмотрим на пример полезной нагрузки, которую мы могли бы использовать для отправки массива PHP:
[
'type' => 'crown',
'material' => 'porcelain',
'location' => 'L12',
'implemented' => now()->toDateString(),
];
Это не самая плохая полезная нагрузка, но что, если мы захотим проверить или расширить ее? Дело в том, что данные запроса также имеют минимальный контекст, неудобны для разработчиков, и мы не добавляем никакой ценности нашему приложению. Поэтому вместо этого мы можем сделать что-то другое — так же, как мы делали с ответами, мы можем делать то же самое с запросами; построить объект, который мы используем и можем преобразовать в массив.Во-первых, давайте создадим объект данных для запроса:app/Services/MedicalTrust/Requests/NewDentalTreatment.php
declare(strict_types=1);
namespace App\Services\MedicalTrust\Requests;
class NewDentalTreatment
{
public function __construct(
public readonly string $type,
public readonly string $material,
public readonly string $location,
public readonly Carbon $implemented,
) {}
public function toArray(): array
{
return [
'type' => $this->type,
'material' => $this->material,
'location' => $this->location,
'implemented' => Carbon::now()->toDateString(),
];
}
}
Итак, на этот раз мы используя объект. Как и раньше, мы создадим для этого фабрику: app/Services/MedicalTrust/RequestFactories/DentalTreatmentFactory.php
declare(strict_types=1);
namespace App\Services\MedicalTrust\RequestFactories;
use Illuminate\Support\Carbon;
class DentalTreatmentFactory
{
public function make(array $attributes): NewDentalTreatment
{
return new NewDentalTreatment(
type: strval(data_get($attributes, 'type')),
material: strval(data_get($attributes, 'material')),
location: strval(data_get($attributes, 'location')),
implemented: Carbon::parse(data_get($attributes, 'implemented')),
);
}
}
Давайте теперь рефакторим < code>addRecord в нашем сервисе:
declare(strict_types=1);
namespace App\Services\MedicalTrust\Resources;
use App\Services\MedicalTrust\Requests\NewDentalTreatment;
use Illuminate\Http\Client\Response;
class DentalResource
{
public function addRecord(string $identifier, NewDentalTreatment $request): Response
{
return $this->service->post(
request: $this->service->buildRequestWithToken(),
url: "/dental/{$identifier}/records",
payload: $request->toArray(),
);
}
}
На данный момент у нас есть гораздо более чистый метод. Мы можем перейти к классу запроса и посмотреть, что он содержит. Но чтобы оценить это, мы можем сделать шаг назад, чтобы увидеть, как это выглядит для нас, чтобы реализовать это. Представьте, что теперь у нас есть контроллер, который обрабатывает это, это почтовый запрос входящей веб-формы, и это особая форма, которую мы используем для добавления новой короны: app/Http/Controllers/Dental/Crowns/StoreController.php
в первый раз мы будем использовать массив:
declare(strict_types=1);
namespace App\Http\Controllers\Dental\Crowns;
use App\Http\Requests\Dental\NewCrownRequest;
use App\Services\MedicalTrust\Resources\DentalResource;
class StoreController
{
public function __construct(
private readonly DentalResource $api,
) {}
public function __invoke(NewCrownRequest $request): RedirectResponse
{
$treatment = $this->api->addRecord(
identifier: $request->get('patient'),
data: $request->validated(),
);
// Whatever else we need to do...
}
}
Это не страшно, правда ? Это довольно разумно. Мы можем проверить полезную нагрузку, поступающую из формы, с помощью запроса формы и передать проверенные данные в ресурс, чтобы добавить новую запись. Но мы ничего не можем поделать с бизнес-логикой; здесь мы полагаемся только на проверку HTTP.Давайте посмотрим, что мы можем делать с объектами:
declare(strict_types=1);
namespace App\Http\Controllers\Dental\Crowns;
use App\Http\Requests\Dental\NewCrownRequest;
use App\Services\MedicalTrust\RequestFactories\DentalTreatmentFactory;
use App\Services\MedicalTrust\Resources\DentalResource;
class StoreController
{
public function __construct(
private readonly DentalResource $api,
private readonly DentalTreatmentFactory $factory,
) {}
public function __invoke(NewCrownRequest $request): RedirectResponse
{
$treatment = $this->api->addRecord(
identifier: $request->get('patient'),
request: $this->factory->make(
attributes: $request->validated(),
),
);
// Whatever else we need to do...
}
}
Итак, теперь мы используем объекты вместо массивов, но как насчет бизнес-логики? Да, мы проводим проверку HTTP, которая может кое-что уловить, но что еще мы можем сделать? Давайте посмотрим, как мы проверяем массив:
declare(strict_types=1);
namespace App\Http\Controllers\Dental\Crowns;
use App\Http\Requests\Dental\NewCrownRequest;
use App\Services\MedicalTrust\Resources\DentalResource;
class StoreController
{
public function __construct(
private readonly DentalResource $api,
) {}
public function __invoke(NewCrownRequest $request): RedirectResponse
{
if ($request->get('type') !== DentalTreatmentOption::crown()) {
throw new InvalidArgumentException(
message: 'Cannot create a new treatment, the only option available right now is crowns.',
);
}
if (! in_array($request->get('location'), DentalLocationOptions::teeth())) {
throw new InvalidArgumentException(
message: 'Passed through location is not a recognised dental location.',
);
}
if (! in_array($request->get('material'), DentalCrownMaterials::all())) {
throw new InvalidArgumentException(
message: 'Cannot use this material for a crown.',
);
}
$treatment = $this->api->addRecord(
identifier: $request->get('patient'),
data: $request->validated(),
);
// Whatever else we need to do...
}
}
Итак, у нас есть много доступных вариантов проверки, но с точки зрения логики мы также хотим проверять помимо проверки HTTP.Поддерживаем ли мы этот тип - поскольку мы делаем только коронку, прошли ли мы в правильном месте с точки зрения стоматологического жаргона? Можно ли использовать этот материал для короны? Все это мы хотим убедиться, что знаем и умеем программировать. Да, мы могли бы добавить все это в форму запроса, но тогда запрос стал бы больше.Мы хотим проверять ввод с базового уровня с помощью запросов формы Laravel и проверять бизнес-логику в месте, которому принадлежит бизнес-логика, чтобы у нас был аналогичный опыт в Интернете, API и CLI. Итак, как это будет выглядеть с использованием объекта:
declare(strict_types=1);
namespace App\Http\Controllers\Dental\Crowns;
use App\Http\Requests\Dental\NewCrownRequest;
use App\Services\MedicalTrust\RequestFactories\DentalTreatmentFactory;
use App\Services\MedicalTrust\Resources\DentalResource;
class StoreController
{
public function __construct(
private readonly DentalResource $api,
private readonly DentalTreatmentFactory $factory,
) {}
public function __invoke(NewCrownRequest $request): RedirectResponse
{
$treatment = $this->api->addRecord(
identifier: $request->get('patient'),
request: $this->factory->make(
attributes: $request->validated(),
)->validate(),
);
// Whatever else we need to do...
}
}
На этот раз мы используем фабрику для создания объекта из проверенных данных — допустимых данных HTTP.На данный момент мы прошли веб-проверку. Теперь мы можем перейти к проверке бизнеса. Итак, мы создаем объект, а затем вызываем его проверку, что является новым методом, который нам нужно добавить:
declare(strict_types=1);
namespace App\Services\MedicalTrust\Requests;
class NewDentalTreatment
{
public function __construct(
public readonly string $type,
public readonly string $material,
public readonly string $location,
public readonly Carbon $implemented,
) {}
public function toArray(): array
{
return [
'type' => $this->type,
'material' => $this->material,
'location' => $this->location,
'implemented' => Carbon::now()->toDateString(),
];
}
public function validate(): static
{
if ($this->type !== DentalTreatmentOption::crown()) {
throw new InvalidArgumentException(
message: "Cannot create a new treatment, the only option available right now is crowns, you asked for {$this->type}"
);
}
if (! in_array($this->location, DentalLocationOptions::teeth())) {
throw new InvalidArgumentException(
message: "Passed through location [{$this->location}] is not a recognised dental location.",
);
}
if (! in_array($this->material, DentalCrownMaterials::all())) {
throw new InvalidArgumentException(
message: "Cannot use material [{$this->material}] for a crown.",
);
}
return $this;
}
}
Итак, как вы можете видеть, объект запроса может содержать для нас свои собственные бизнес-правила — это означает, что объект может пройти проверку самостоятельно, не усложняя ваше веб-приложение и реализации CLI. Вот где я считаю, что сила этого подхода проявляется в стандартизации того, как вы подходите к API.В этом нет ничего нового или новаторского, но принятие такого подхода означает, что вы можете точно контролировать свои API-интеграции в соответствии с единым стандартом наилучшим из возможных способов, которые вам подходят.