Nieco ponad dwa lata temu napisałem samouczek na temat tego, jak powinieneś pracować z usługami innych firm w Laravel. Do dziś jest to najczęściej odwiedzana strona na mojej stronie. Jednak w ciągu ostatnich dwóch lat wiele się zmieniło i postanowiłem ponownie podejść do tego tematu.
Pracuję więc z usługami stron trzecich od tak dawna, że nie pamiętam, kiedy nie byłem. Jako młodszy programista zintegrowałem API z innymi platformami, takimi jak Joomla, Magento i WordPress. Teraz integruje się głównie z moimi aplikacjami Laravel, aby rozszerzyć logikę biznesową, opierając się na innych usługach.
Ten samouczek opisuje, jak zazwyczaj podchodzę do integracji z interfejsem API dzisiaj. Jeśli przeczytałeś mój poprzedni samouczek, czytaj dalej, ponieważ kilka rzeczy się zmieniło - z tego, co uważam za dobre powody.
Zacznijmy od API. Potrzebujemy API do integracji. Mój oryginalny samouczek integrował się z PingPing, doskonałym rozwiązaniem do monitorowania czasu pracy społeczności Laravel. Jednak tym razem chcę wypróbować inne API.
W tym samouczku użyjemy interfejsu API Planetscale.For this tutorial, we will use the Planetscale API. Planetscale to niesamowita usługa bazy danych, której używam, aby moje operacje odczytu i zapisu były bliższe moim użytkownikom w codziennej pracy.
Do czego przyczyni się nasza integracja? Wyobraźmy sobie, że mamy aplikację, która pozwala nam zarządzać naszą infrastrukturą. Nasze serwery działają przez Laravel Forge, a nasza baza danych jest skończona w Planetscale. Nie ma czystego sposobu zarządzania tym przepływem pracy, więc stworzyliśmy własny. W tym celu potrzebujemy integracji lub dwóch.
Początkowo trzymałem moje integracje pod ; app/Services
jednak, gdy moje aplikacje stały się bardziej rozbudowane i skomplikowane, musiałem używać Services
przestrzeni nazw dla usług wewnętrznych, co doprowadziło do zanieczyszczonej przestrzeni nazw. Przeniosłem moje integracje do app/Http/Integrations
. Ma to sens i jest sztuczką, którą zaczerpnąłem z Saloon Sama Carrè.
Teraz mógłbym użyć Saloon do integracji API, ale chciałem wyjaśnić, jak to zrobić bez pakietu. Jeśli potrzebujesz integracji API w 2023 roku, gorąco polecam korzystanie z Saloon. To jest niesamowite!
Zacznijmy więc od stworzenia katalogu dla naszej integracji. Możesz użyć następującego polecenia bash:
mkdir app/Http/Integrations/Planetscale
Gdy mamy już katalog Planetscale, musimy stworzyć sposób na połączenie się z nim. Inną konwencją nazewnictwa, którą przejąłem z biblioteki Saloon, jest spojrzenie na te klasy bazowe jako łączniki - ponieważ ich celem jest umożliwienie połączenia się z określonym interfejsem API lub stroną trzecią.
Stwórz nową klasę o nazwie PlanetscaleConnector
w app/Http/Integrations/Planetscale
katalogu, a my możemy rozwinąć, czego ta klasa potrzebuje, co będzie świetną zabawą.
Musimy więc zarejestrować tę klasę w naszym kontenerze, aby ją rozwiązać lub zbudować wokół niej fasadę. Moglibyśmy zarejestrować to "długo" w dostawcy usług - ale moim najnowszym podejściem jest to, aby te łączniki same się rejestrowały - coś w rodzaju ...
declare(strict_types=1);
namespace App\Http\Integrations\Planetscale;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
final readonly class PlanetscaleConnector
{
public function __construct(
private PendingRequest $request,
) {}
public static function register(Application $app): void
{
$app->bind(
abstract: PlanetscaleConnector::class,
concrete: fn () => new PlanetscaleConnector(
request: Http::baseUrl(
url: '',
)->timeout(
seconds: 15,
)->withHeaders(
headers: [],
)->asJson()->acceptJson(),
),
);
}
}
Chodzi o to, że wszystkie informacje o tym, jak ta klasa jest rejestrowana w kontenerze, znajdują się w samej klasie. Wszystko, co usługodawca musi zrobić, to wywołać metodę rejestru statycznego na zajęciach! Zaoszczędziło mi to wiele czasu podczas integracji z wieloma interfejsami API, ponieważ nie muszę polować na dostawcę i znajdować właściwego wiązania, wśród wielu innych. Idę do klasy, o której mowa, która jest przede mną.
Zauważysz, że obecnie nic nie jest przekazywane do metod tokenu lub podstawowego adresu URL w żądaniu. Naprawmy to dalej. Możesz je zdobyć na swoim koncie Planetscale.
Utwórz następujące rekordy w .env
pliku.
PLANETSCALE_SERVICE_ID="your-service-id-goes-here"
PLANETSCALE_SERVICE_TOKEN="your-token-goes-here"
PLANETSCALE_URL="https://api.planetscale.com/v1"
Następnie należy je pobrać do konfiguracji aplikacji. Wszystkie one należąconfig/services.php
, ponieważ jest to miejsce, w którym zazwyczaj konfigurowane są usługi innych firm.
return [
// the rest of your services config
'planetscale' => [
'id' => env('PLANETSCALE_SERVICE_ID'),
'token' => env('PLANETSCALE_SERVICE_TOKEN'),
'url' => env('PLANETSCALE_URL'),
],
];
Teraz możemy z nich korzystać w naszej PlanetscaleConnector
metodzie pod rejestrem.
declare(strict_types=1);
namespace App\Http\Integrations\Planetscale;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
final readonly class PlanetscaleConnector
{
public function __construct(
private PendingRequest $request,
) {}
public static function register(Application $app): void
{
$app->bind(
abstract: PlanetscaleConnector::class,
concrete: fn () => new PlanetscaleConnector(
request: Http::baseUrl(
url: config('services.planetscale.url'),
)->timeout(
seconds: 15,
)->withHeaders(
headers: [
'Authorization' => config('services.planetscale.id') . ':' . config('services.planetscale.token'),
],
)->asJson()->acceptJson(),
),
);
}
}
Musisz wysłać tokeny do Planetscale w następującym formacie: , więc nie możemy użyć domyślnej withToken
metody, service-id:service-token
ponieważ nie pozwala nam ona dostosować jej tak, jak potrzebujemy.
Teraz, gdy mamy już stworzoną podstawową klasę, możemy zacząć myśleć o stopniu naszej integracji. Musimy to zrobić podczas tworzenia naszego tokenu usługi, aby dodać poprawne uprawnienia. W naszej aplikacji chcemy mieć możliwość wykonania następujących czynności:
Lista baz danych.
Wyświetl listę regionów bazy danych.
Lista kopii zapasowych baz danych.
Utwórz kopię zapasową bazy danych.
Usuń kopię zapasową bazy danych.
Możemy więc pogrupować je w dwie kategorie:
Baz danych.
Kopie zapasowe.
Dodajmy dwie nowe metody do naszego łącznika, aby utworzyć to, databases
czego potrzebujemy:
declare(strict_types=1);
namespace App\Http\Integrations\Planetscale;
use App\Http\Integrations\Planetscale\Resources\BackupResource;
use App\Http\Integrations\Planetscale\Resources\DatabaseResource;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
final readonly class PlanetscaleConnector
{
public function __construct(
private PendingRequest $request,
) {}
public function databases(): DatabaseResource
{
return new DatabaseResource(
connector: $this,
);
}
public function backups(): BackupResource
{
return new BackupResource(
connector: $this,
);
}
public static function register(Application $app): void
{
$app->bind(
abstract: PlanetscaleConnector::class,
concrete: fn () => new PlanetscaleConnector(
request: Http::baseUrl(
url: config('services.planetscale.url'),
)->timeout(
seconds: 15,
)->withHeaders(
headers: [
'Authorization' => config('services.planetscale.id') . ':' . config('services.planetscale.token'),
],
)->asJson()->acceptJson(),
),
);
}
}
Jak widać, stworzyliśmy dwie nowe metody i backups
. Zwrócą one nowe klasy zasobów, przechodząc przez łącznik. Logikę można teraz zaimplementować w klasach zasobów, ale później musimy dodać inną metodę do naszego łącznika.
<?php
declare(strict_types=1);
namespace App\Http\Integrations\Planetscale\Resources;
use App\Http\Integrations\Planetscale\PlanetscaleConnector;
final readonly class DatabaseResource
{
public function __construct(
private PlanetscaleConnector $connector,
) {}
public function list()
{
//
}
public function regions()
{
//
}
}
To jest nasze DatabaseResource
; teraz wymyśliliśmy metody, które chcemy wdrożyć. Możesz zrobić to samo dla BackupResource
pliku . Będzie to wyglądać nieco podobnie.
Tak więc wyniki mogą być podzielone na strony na liście baz danych. Nie będę się jednak tym tutaj zajmował - skłaniałbym się do tego na Saloon, ponieważ jego wdrożenie dla wyników paginowanych jest fantastyczne. W tym przykładzie nie będziemy się martwić o paginację. Zanim wypełnimy DatabaseResource
, musimy dodać jeszcze jedną metodę do PlanetscaleConnector
ładnego wysyłania żądań. W tym celu używam mojego pakietu o nazwie juststeveking/http-helpers
, który ma wyliczenie dla wszystkich typowych metod HTTP, których używam.
public function send(Method $method, string $uri, array $options = []): Response
{
return $this->request->send(
method: $method->value,
url: $uri,
options: $options,
)->throw();
}
Teraz możemy wrócić do naszego DatabaseResource
i zacząć wypełniać logikę metody list.
declare(strict_types=1);
namespace App\Http\Integrations\Planetscale\Resources;
use App\Http\Integrations\Planetscale\PlanetscaleConnector;
use Illuminate\Support\Collection;
use JustSteveKing\HttpHelpers\Enums\Method;
use Throwable;
final readonly class DatabaseResource
{
public function __construct(
private PlanetscaleConnector $connector,
) {}
public function list(string $organization): Collection
{
try {
$response = $this->connector->send(
method: Method::GET,
uri: "/organizations/{$organization}/databases"
);
} catch (Throwable $exception) {
throw $exception;
}
return $response->collect('data');
}
public function regions()
{
//
}
}
Nasza metoda list akceptuje parametr organization
do przejścia przez organizację do listy baz danych. Następnie używamy tego do wysyłania żądania do określonego adresu URL za pośrednictwem łącznika. Opakowanie tego w instrukcję try-catch pozwala nam wychwycić potencjalne wyjątki od metody wysyłania łączników. Na koniec możemy zwrócić kolekcję z metody do pracy z nią w naszej aplikacji.
Możemy przejść do bardziej szczegółowych informacji w tym żądaniu, ponieważ możemy zacząć mapować dane z tablic na coś bardziej użytecznego kontekstowo za pomocą DTO. Pisałem o tym tutaj, więc nie będę powtarzał tego samego tutaj.
Spójrzmy szybko na BackupResource
coś więcej niż tylko prośbę o uzyskanie.
declare(strict_types=1);
namespace App\Http\Integrations\Planetscale\Resources;
use App\Http\Integrations\Planetscale\Entities\CreateBackup;
use App\Http\Integrations\Planetscale\PlanetscaleConnector;
use JustSteveKing\HttpHelpers\Enums\Method;
use Throwable;
final readonly class BackupResource
{
public function __construct(
private PlanetscaleConnector $connector,
) {}
public function create(CreateBackup $entity): array
{
try {
$response = $this->connector->send(
method: Method::POST,
uri: "/organizations/{$entity->organization}/databases/{$entity->database}/branches/{$entity->branch}",
options: $entity->toRequestBody(),
);
} catch (Throwable $exception) {
throw $exception;
}
return $response->json('data');
}
}
Nasza metoda tworzenia akceptuje klasę encji, której używam do przekazywania danych przez aplikację w razie potrzeby. Jest to przydatne, gdy adres URL wymaga zestawu parametrów i musimy wysłać treść żądania.
Nie omówiłem tutaj testowania, ale napisałem samouczek na temat testowania punktów końcowych JSON: API przy użyciu PestPHP tutaj, który będzie miał podobne koncepcje do testowania takiej integracji.
Mogę tworzyć niezawodne i rozszerzalne integracje z osobami trzecimi przy użyciu tego podejścia. Jest podzielony na logiczne części, więc mogę poradzić sobie z ilością logiki. Zazwyczaj miałbym więcej integracji, więc część tej logiki można udostępnić i wyodrębnić w cechy, aby odziedziczyć zachowanie między integracjami.