• Czas czytania ~22 min
  • 10.08.2022

Praca z interfejsami API innych firm może być frustrująca; otrzymujemy odpowiedzi JSON, które w PHP będą reprezentowane jako zwykła stara tablica - a także wysyłamy dane jako tablice. Tracimy dużo kontekstu i możliwość zbudowania czegoś z doskonałym doświadczeniem programisty. A gdybym ci powiedział, że nie musi tak być?Co jeśli powiem Ci, że zbudowanie czegoś, co doda więcej kontekstu i usprawni Twoją pracę z zewnętrznymi interfejsami API, nie wymaga wiele wysiłku? Nie wierzysz mi? Rzućmy okiem.

W tym samouczku omówimy integrację fikcyjnego interfejsu API innej firmy z zagnieżdżonymi danymi wewnątrz — najbardziej nieuporządkowanego z interfejsów API.Będziemy chcieli móc pobierać dane z interfejsu API, ale także móc wysyłać dane do tego interfejsu API bez konieczności budowania tych paskudnych tablic, do których jesteśmy przyzwyczajeni.

Najlepszym sposobem na poprawę doświadczenia jest użycie najnowszej wersji PHP i pakietu innej firmy, takiego jak Laravel Saloon lub Laravel Transporter - ale czasami nie chcesz ściągać całego pakietu tylko po to, aby kilka żądań API, prawda? Gdybyśmy to zrobili, cała nasza aplikacja byłaby krucha i polegałaby na tak dużej ilości kodu firm trzecich, że równie dobrze moglibyśmy używać narzędzia do tworzenia witryn.

Interfejs API, z którym zamierzamy się zintegrować, jest fikcyjny, co informuje nas o historii medycznej naszych użytkowników/pacjentów. Wyobraź sobie interfejs API, z którym pracujesz i chcesz mieć możliwość dodawania do niego nowych danych - powiedzmy, że aplikacja internetowa lub aplikacja mobilna lekarza rodzinnego, i idziesz na spotkanie, a oni muszą zarejestrować wszelkie dalsze problemy lub uwagi do twojego plik.Mogą chcieć sprawdzić Twoją historię i zobaczyć, co jest aktualnie w Twoim pliku.

Najlepszym sposobem na rozpoczęcie tego jest zbudowanie klasy usług i w zależności od ile interfejsów API musisz zintegrować, zwykle wskazuje właściwy kierunek integracji.Zamierzam to zbudować tak, jakbym musiał zintegrować się z wieloma interfejsami API – powiedzmy, że dane dotyczące zdrowia psychicznego znajdują się w całkowicie oddzielnym interfejsie API. Więc będziemy musieli się z tym zintegrować w pewnym momencie w przyszłości. Pierwszą rzeczą, którą chcemy zrobić w naszym katalogu app, jest utworzenie nowej przestrzeni nazw dla Usługabyśmy mogli mieć gdzie mieszkać nasze połączenia serwisowe. Wewnątrz utworzymy nową przestrzeń nazw dla każdej usługi, którą musimy zintegrować, czyli usług zewnętrznych lub wewnętrznych. Fajnie jest je pogrupować w ten sposób, ponieważ daje to standard - jeśli musisz rozszerzyć, nie ma mowy o tym, gdzie należy; utwórz nową integrację usług w App\Services\i dobrze jest iść.

Tak więc nasz fikcyjny interfejs API nosi nazwę medicaltrust, losowo wymyśloną nazwę, którą wymyśliłem podczas pisania tego samouczka — jeśli rzeczywiście jest to już API, przepraszam. Ten samouczek nie jest odzwierciedleniem ani w żaden sposób nie jest oparty na tym interfejsie API, kształcie ani formie. Teraz utwórz nowy katalog/przestrzeń nazw app/Services/MedicalTrust;w środku, będziemy chcieli stworzyć klasę, która obsłuży naszą integrację - jeśli przeczytasz mój samouczek na Laravel Saloon uznaj to za złącze. Klasa, która obsłuży podstawowe połączenie z API. Zadzwoniłem do mojego MedicalTrustServiceponieważ lubię jasno określić, gdzie mogę się znaleźć, i upewnić się, że wygląda to mniej więcej tak:

declare(strict_types=1);
 
namespace App\Services\MedicalTrust;
 
class MedicalTrustService
{
	public function __construct(
		private readonly string $baseUrl,
		private readonly string $apiToken,
    ) {}
}

Więc będziemy potrzebować 2 rzeczy dla tego interfejsu API, bazowy adres URL i token API - nic nadzwyczajnego. Wewnątrz config/services.php dodaj następujący blok:

return [
	'medical-trust' => [
		'url' => env('MEDICAL_TRUST_URL'),
		'token' => env('MEDICAL_TRUST_TOKEN'),
	]
];

Dodając opcje konfiguracji dla usług innych firm, uważam, że zawsze najlepiej jest trzymać je w tym samym miejscu, nawet jeśli potrzebujesz wielu opcji konfiguracyjnych. Utrzymanie spójnego standardu do obsługi tego ma kluczowe znaczenie podczas pracy z interfejsami API, ponieważ standardy są podstawą większości nowoczesnych interfejsów API. Aby uruchomić naszą usługę, musimy dodać nowy rekord do naszego app/Providers/AppServiceProvider.php, aby zarejestrował naszą klasę usług jako zależność w kontenerze, aby zbudować klasę. Dodaj więc następujące elementy do metody rozruchu:

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

Wszystko, co tutaj robimy, to dodawanie nowego singletona do kontenera, a kiedy prosimy o MedicalTrustService< /code> jeśli nie zbudowaliśmy go wcześniej - zbuduj go, przekazując te wartości konfiguracyjne. Używamy strval, aby upewnić się, że jest to ciąg znaków, ponieważ config() domyślnie zwróci wartość mieszaną. Ta część była stosunkowo prosta, więc przejdźmy do zapewnienia sposobu, w jaki możemy tworzyć spójne żądania do wysłania.

Wysyłanie żądań jest jedynym celem integracji z API, więc chcesz mieć pewność, że podchodzisz do tego rozsądnie.Jak powiedziałem na początku samouczka, podejdziemy do tego tak, jakbyśmy integrowali się z więcej niż jednym API na dłuższą metę. Więc to, co musimy zrobić, to abstrakcyjna funkcjonalność z dala od naszej usługi, w której jest udostępniana. Najlepszym sposobem, aby to zrobić, jest użycie cech w PHP - a jeśli zastosujesz się do zasady 3, będzie to miało sens.Powinieneś abstrahować, jeśli powtórzysz ten sam kod lub mniej więcej ten sam kod więcej niż dwa razy. Jakie rzeczy moglibyśmy chcieć abstrahować lub mieć nad nimi pewien poziom kontroli? Zbudowanie naszego podstawowego szablonu żądania to jedno, co zapewnia, że ​​mamy go poprawnie skonfigurowany. Wysyłanie żądań to kolejna - musimy upewnić się, że możemy kontrolować żądania, które możemy wysyłać na podstawie API w rzeczywistości.Stwórzmy więc kilka cech, aby trochę to ułatwić.

Najpierw stworzymy cechę, która kontroluje tworzenie żądania podstawowego i może mieć kilka opcji w cecha podejścia. Będą one znajdować się w przestrzeni nazw usług, ale w przestrzeni nazw Concerns.W Laravel cechy szczególnie w Eloquent nazywają się Concerns - więc dopasujemy tutaj konwencję nazewnictwa Laravela. Utwórz nową cechę o nazwie app/Services/Concerns/BuildsBaseRequest.php i dodaj do niej następujący kod:

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

Tworzymy tutaj standardową metodę, która utworzy oczekujące żądanie z ustawionym podstawowym adresem URL — polega to na przestrzeganiu standardu wstrzykiwania podstawowego adresu URL do konstruktora klasy usługi — dlatego konieczne jest przestrzeganie standard lub wzór. Następnie mamy opcjonalne metody rozszerzenia żądania za pomocą tokena lub uwierzytelniania za pomocą skrótu.Podejście do tego w ten sposób pozwala nam być bardzo elastycznymi i nie robimy niczego ekstremalnego ani czegoś, co działałoby lepiej gdzie indziej. Dodanie tych metod do każdej usługi jest w porządku, ale gdy zaczniesz integrować się z coraz większą liczbą interfejsów API, kluczowe jest posiadanie scentralizowanego sposobu, aby to zrobić.

Nasz następny zestaw obaw/cech będzie pomagał kontrolować sposób wysyłania żądań do zewnętrznych interfejsów API — chcemy mieć wiele obaw/cech, aby ograniczyć typy żądań, które możemy wysyłać. Pierwszym z nich będzie 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,
    );
  }
}

Następnie utwórzmy app/Services/ Obawy/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,
    );
  }
}

Jak widać, budujemy czasowniki HTTP w cechy, które mają być określone w naszej kontroli, aby zapewnić, że żądania są zawsze wysyłane poprawnie. W przypadku niektórych projektów będzie to absolutna przesada, ale wyobraź sobie, że integrujesz się z ponad 10 interfejsami API; nagle to podejście nie jest takie głupie.

Poświęćmy chwilę, aby ponownie pomyśleć o samej klasie usług.Czy chcemy zbudować tę klasę usług, aby zawierała 10-20+ metod, aby zapewnić, że trafimy na wszystkie punkty końcowe interfejsu API? Najprawdopodobniej nie; to brzmi dość niechlujnie, prawda? Zamiast tego stworzymy określone klasy zasobów, które możemy zbudować za pomocą klasy usługi lub wstrzyknąć bezpośrednio do metod. Ponieważ jest to fikcyjny medyczny interfejs API, na początek przyjrzymy się dokumentacji dentystycznej. Wróćmy do naszego 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,
    );
  }
}

Mamy nową metodę o nazwie dental, która zwróci klasę zasobów specyficzną dla punktów końcowych zasobów stomatologicznych. Wstrzykujemy usługę do konstruktora, aby wywołać metody usługi, takie jak get lub post lub buildRequestWithToken. Przyjrzyjmy się teraz tej klasie i zobaczmy, jak powinniśmy ją zbudować app/Services/MedicalTrust/Resources/DentalResource.php

declare(strict_types=1);
 
namespace App\Services\MedicalTrust\Resources;
 
class DentalResource
{
  public function __construct(
  	private readonly MedicalTrustService $service,
  ) {}
}

Ładne i proste, naprawdę. Możemy rozwiązać ten problem z kontenera, ponieważ nie potrzebuje niczego specjalnego. Tak więc w przypadku danych dentystycznych załóżmy, że chcemy wyświetlić listę wszystkich rekordów i dodać nowy rekord – jak to wygląda przy użyciu tablic?

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

Jak widać, jest to względnie bezpośredni.Przekazujemy identyfikator w celu identyfikacji użytkownika, a następnie korzystając z usługi wysyłamy żądanie get lub post używając buildRequestWithToken jako żądania bazowego do posługiwać się. Jednak takie podejście jest dla mnie wadliwe. Po pierwsze, zwracamy odpowiedź taką, jaka jest. Nie ma kontekstu ani informacji - tylko tablica.Teraz jest to w porządku, zwłaszcza jeśli tworzysz SDK – ale są szanse, że potrzebujemy trochę więcej informacji wokół odpowiedzi. A co z prośbą? Tak, prawdopodobnie wykonaliśmy pewną walidację z przychodzącym żądaniem za pomocą walidacji HTTP - ale co z kontrolowaniem danych, które wysyłamy do API? Przyjrzyjmy się, jak możemy sobie z tym poradzić, aby tablice stały się przeszłością, a obiekty kontekstowe przyszłością.

Przed całkowitym usunięciem tablic musimy zrozumieć, w jaki sposób wyświetlane są dane i co je tworzy. Spójrzmy na przykładowy ładunek danych dentystycznych:

{
  "id": "1234-1234-1234-1234",
  "treatments": {
    "crowns": [
      {
      	"material": "porcelain",
      	"location": "L12",
      	"implemented": "2022-07-10"
      }
    ],
    "fillings": [
      {
      	"material": "white",
      	"location": "R8",
      	"implemented": "2022-07-10"
      }
    ]
  }
}

Mamy więc identyfikator pacjenta, a następnie obiekt leczenia. Obiekt zabiegowy posiada korony i wypełnienia, które otrzymał pacjent. W rzeczywistości byłby znacznie większy i zawierałby znacznie więcej informacji.Korona i wypełnienie to szereg zastosowanych mocowań dentystycznych – użyty materiał, który ząb w żargonie dentystycznym oraz data wykonania zabiegu. Teraz spójrzmy najpierw na to w formacie tablicy:

[
'id' => '1234-1234-1234-1234',
'treatment' => [
  'crowns' => [
    [
      'material' => 'porcelain',
      'location' => 'L12',
      'implemented' => '2022-07-10',
    ],
  ],
  'fillings' => [
    [
      'material' => 'white',
      'location' => 'R8',
      'implemented' => '2022-07-10',
    ],
  ]
]
];

Nie tak dobrze, prawda? Tak, reprezentuje dane stosunkowo dobrze, ale wyobraź sobie, że próbujesz użyć tych danych w interfejsie użytkownika lub w czymkolwiek innym. Jaka jest alternatywa?Co możemy zrobić, aby to naprawić? Najpierw zaprojektujmy obiekt, który reprezentuje leczenie stomatologiczne: 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(),
    ];
  }
}

Zamiast tego teraz mieć klasę, którą można zbudować - że patrząc na nią wiemy, co to znaczy. Rozumiemy, że ten obiekt lub ten zestaw danych jest związany z leczeniem stomatologicznym.Wejdźmy na wyższy poziom i spójrzmy na same zabiegi: 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(),
    ];
  }
}

Ponownie jak poprzednio, my mieć określoną klasę, która reprezentuje wszystkie zabiegi, które użytkownik może podjąć - i może zostać rozszerzona o inne. Załóżmy, że teraz chcemy oferować okleiny — możemy dodać nową właściwość i utworzyć dla niej obiekt danych.Przyjrzyjmy się, jak może wyglądać obiekt taki jak Korony: 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();
  }
}

Tym razem, nasz konstruktor po prostu posiada Zbiór zabiegów, które można dodać. Moglibyśmy podpowiedzieć to za pomocą docblocków, aby upewnić się, że dodamy do niego DentalTreatments tylko wtedy, gdybyśmy chcieli.Następnie, kiedy rzutujemy to na tablicę, mapujemy zabiegi (wpisz wskazując każdy element) i rzutujemy zabieg na tablicę - a następnie w końcu rzutujemy całość na tablicę. Powodem, dla którego mamy metodę toArray w naszych klasach, jest możliwość łatwego zapisania jej w bazie danych za pomocą elokwentnego: Treatment::query()->create($treatment-> ;toArray());ale także do wyświetlania i tabel CLI. Przydatna rzecz, którą zauważyłem, działa dobrze na tych obiektach danych.

Jak więc możemy je wykorzystać? Z pewnością ręczne budowanie ich w serwisie sprawi, że poczujesz się nadęty? Lubię budować te obiekty za pomocą fabryki obiektów danych, która akceptuje dane jako tablicę i zwraca je jako obiekt. Stwórzmy jeden do Leczenia Stomatologicznego (najniższy): 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'));
    );
  }
}

Mamy więc fabrykę z metodą make, która akceptuje tablicę atrybutów. Następnie tworzymy nowy obiekt Dental Treatment za pomocą helpera Laravel data_get i upewniamy się, że rzutujemy go na właściwy typ. Jeśli chodzi o wdrożonewłaściwość, używamy Carbon do analizy przekazanej daty. Teraz idąc o krok dalej, spójrzmy, jak możemy tworzyć wrony: 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,
    		),
    	),
    );
  }
}

Więc ten jest trochę bardziej złożony niż poprzedni. Tym razem przekazujemy szereg zabiegów w i unowocześniamy Kolekcję.Następnie, gdy już mamy naszą kolekcję, chcemy przejrzeć każdy zabieg i użyć fabryki leczenia dentystycznego, aby przekształcić go w obiekt leczenia stomatologicznego. Aby ułatwić pracę, moglibyśmy dodać do naszych fabryk danych metodę statyczną o nazwie new, która akceptuje tablicę i po prostu wywołuje metodę 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'));
  	);
  }
}

Dzięki temu nasza fabryka wron byłaby dużo czystsza:

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

Moglibyśmy nawet jeszcze bardziej ułatwić nam korzystanie, prosząc fabrykę DentalTreatment o utworzenie kolekcji dla nas:

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

Pozwoliłoby nam to uprościć fabrykę koron, która znacznie dalej:

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

Historia tutaj polega na tym, że limitem jest to, co czyni Twoje życie łatwiejszym. Być może nie musisz iść tak daleko, a może Twoje API jest trochę bardziej płaskie, więc łatwo jest wdrożyć takie podejście.Ale jeśli przyjmiemy podejście i zastosujemy to, co dla nas działa, otrzymamy bardziej kontekstową odpowiedź API i będziemy mogli znacznie łatwiej zrozumieć odpowiedź i znacznie łatwiej z nią pracować.

Wycofując się, chcemy również mieć możliwość tworzenia nowych zabiegów za pośrednictwem interfejsu API.Chcemy móc wypełnić formularz - lub coś podobnego i przesłać dane do API, aby zarejestrować, że wdrożyliśmy nowy zabieg. Aby to zrobić, musimy wysłać żądanie postu za pośrednictwem naszego DentalResource przy użyciu metody addRecord. Nie jest to straszne, ale spójrzmy na przykładowy ładunek, którego możemy użyć do wysłania w tablicy PHP:

[
	'type' => 'crown',
	'material' => 'porcelain',
	'location' => 'L12',
	'implemented' => now()->toDateString(),
];

Nie jest to najgorszy możliwy ładunek, ale co, jeśli chcemy przeprowadzić walidację lub rozszerzyć to? Chodzi o to, że dane żądania również mają minimalny kontekst, nie są przyjazne dla programistów i nie dodajemy żadnej wartości do naszej aplikacji. Zamiast tego możemy zrobić coś innego – podobnie jak w przypadku odpowiedzi, możemy zrobić to samo z prośbami; zbudować obiekt, którego używamy i który możemy rzutować jako tablicę.Najpierw utwórzmy obiekt danych dla żądania: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(),
    ];
  }
}

Więc tym razem jesteśmy za pomocą obiektu. Tak jak poprzednio, stworzymy dla tego fabrykę: 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')),
    );
  }
}

Zmieńmy teraz < metoda code>addRecord w naszym serwisie:

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(),
    );
  }
}

W tym momencie mamy znacznie czyściej wyglądającą metodę. Możemy przejść do klasy request i zobaczyć, co ona zawiera. Ale żeby to docenić, możemy cofnąć się o krok, aby zobaczyć, jak to wygląda dla nas, aby to wdrożyć. Wyobraź sobie teraz, że mamy kontroler, który to obsługuje, jest to przychodzące żądanie posta w formularzu internetowym i jest to specyficzny formularz, którego używamy do dodawania nowej korony: app/Http/Controllers/Dental/Crowns/StoreController.php za pierwszym razem użyjemy tablicy:

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...
  }
}

To nie jest straszne, prawda ? To całkiem rozsądne. Możemy zweryfikować ładunek pochodzący z formularza za pomocą żądania formularza i przekazać zweryfikowane dane do zasobu, aby dodać nowy rekord. Ale nic nie możemy zrobić z logiką biznesową; polegamy tutaj tylko na walidacji HTTP.Zobaczmy, co możemy zrobić z obiektami:

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...
  }
}

Więc używamy teraz obiektów zamiast tablic, ale co z logiką biznesową? Tak, przeprowadzamy walidację HTTP, która może przechwycić pewne rzeczy - ale co innego możemy zrobić? Przyjrzyjmy się, jak walidujemy tablicę:

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...
  }
}

Więc mamy dostępnych wiele opcji walidacji – ale logicznie chcemy również sprawdzić poza walidacją HTTP.Czy popieramy ten typ - skoro robimy tylko koronę, czy przeszliśmy w prawidłowym miejscu pod względem dentystycznego żargonu? Czy możemy użyć tego materiału na koronę? Wszystko to chcemy mieć pewność, że wiemy i potrafimy programować. Tak, moglibyśmy dodać to wszystko do wniosku o formularz, ale wtedy prośba byłaby większa.Chcemy zweryfikować dane wejściowe na podstawowym poziomie za pomocą żądań formularzy Laravel i zweryfikować logikę biznesową w miejscu, które jest właścicielem logiki biznesowej, aby mieć podobne doświadczenie w sieci, API i CLI. A więc jak by to wyglądało przy użyciu obiektu:

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...
  }
}

Tym razem używamy fabryki do stworzenia obiektu z walidowanych danych - poprawnych danych HTTP.W tym momencie przeszliśmy weryfikację sieciową. Teraz możemy przejść do walidacji biznesowej. Tworzymy więc obiekt, a następnie wywołujemy na nim validate, co jest nową metodą, którą musimy dodać:

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

Jak widać, obiekt żądania może zawierać dla nas własne reguły biznesowe — co oznacza, że ​​obiekt może przejść przez sam proces weryfikacji, nie zwiększając złożoności aplikacji sieciowej i implementacji CLI. W tym miejscu wierzę, że siła tego podejścia wkracza w standaryzację podejścia do API.Nie jest to nic nowego ani przełomowego, ale przyjęcie tego podejścia oznacza, że ​​możesz precyzyjnie kontrolować swoje integracje API w spójnym standardzie w najlepszy możliwy sposób, który Ci odpowiada.

< p>Jak radzisz sobie z danymi i żądaniami API? Jestem ciekawy, jak wielu innych znalazło podobny sposób bycia pomocnym. Czy sądzisz, że można to poprawić?Daj nam znać na Twitterze, ponieważ wszyscy uwielbiamy się uczyć i rozwijać!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

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...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297