• Час читання ~9 хв
  • 10.08.2022

Робота зі сторонніми 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 у довгостроковій перспективі. Отже, що нам потрібно зробити, це абстрагувати функціональність від нашого сервісу, де вона є спільною. Найкращий спосіб зробити це — використовувати Traits у 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/ Concerns/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(),
    ];
  }
}

Знову, як і раніше, ми мати певний клас, який представляє всі процедури, які може виконати користувач, і його можна розширити, щоб включити інші. Скажімо, тепер ми хочемо запропонувати вініри — ми можемо додати нову властивість і створити для цього об’єкт даних.Давайте подивимося, як може виглядати такий об’єкт, як Crowns: 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-> ;toArray());але також для відображення 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/DataFactory/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'));
  	);
  }
}

Це зробило б нашу фабрику ворон набагато чистішою:

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'));
    );
  }
}

Це дозволить нам спростити фабрику коронок, яка набагато далі:

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 у узгодженому стандарті найкращим можливим способом, який вам підходить.

< p>Як ви обробляєте дані та запити API? Мені цікаво побачити, скільки інших знайшли подібний спосіб корисним. Як ви думаєте, чи можна це покращити?Повідомте нас у Twitter, оскільки всі ми любимо вчитися та розвиватися!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Про автора CrazyBoy49z
WORK EXPERIENCE
Контакти
Ukraine, Lutsk
+380979856297