• Czas czytania ~6 min
  • 21.02.2023

Wielokrotnie mówiłem o integracji API i za każdym razem znajduję poprawę od poprzedniej, po raz kolejny do walki.

Niedawno zostałem oznaczony na Twitterze w związku z artykułem o nazwie SDK, The Laravel Way. To się wyróżniało, ponieważ w pewnym stopniu odzwierciedlało sposób, w jaki robię integrację API i zainspirowało mnie do dodania własnych dwóch groszy.

Dla przypomnienia, artykuł, o którym wspomniałem, jest doskonały i nie ma nic złego w przyjętym podejściu. Czuję, że mogę go trochę rozwinąć dzięki moim doświadczeniom zdobytym w rozwoju i integracji API. Nie będę bezpośrednio kopiował tematu, tak jak w przypadku interfejsu API, którego nie znam. Wezmę jednak koncepcje w interfejsie API, który znam, GitHub API, i przejdę do szczegółów.

Podczas tworzenia nowej integracji API najpierw organizuję zmienne konfiguracyjne i środowiskowe. Dla mnie należą one do pliku konfiguracyjnego usług, ponieważ są to usługi innych firm, które konfigurujemy. Oczywiście nie musisz tego robić i tak naprawdę nie ma znaczenia, gdzie je trzymasz, o ile pamiętasz, gdzie są!

// config/services.php
return [
    // other services

    'github' => [
        'url' => env('GITHUB_URL'),
        'token' => env('GITHUB_TOKEN'),
        'timeout' => env('GITHUB_TIMEOUT', 15),
    ],
];

Najważniejsze rzeczy, które robię, to przechowywanie adresu URL, tokenu API i limitu czasu. Przechowuję adres URL, ponieważ nienawidzę mieć ciągów pływających w klasach lub kodzie aplikacji. Czuję się źle, oczywiście nie jest źle, ale wszyscy wiemy, że mam zdecydowane opinie ...

Następnie tworzę kontrakt/interfejs - w zależności od tego, ile integracji powie mi, jakiego rodzaju interfejsu mogę potrzebować. Lubię używać interfejsu, ponieważ wymusza on kontrakt w moim kodzie i zmusza mnie do myślenia przed zmianą interfejsu API. Chronię się przed przełomowymi zmianami! Mój kontrakt jest jednak bardzo minimalny. Zazwyczaj mam jedną metodę i blok doc dla właściwości, którą zamierzam dodać za pomocą konstruktora.

/**
 * @property-read PendingRequest $request
 */
interface ClientContract
{
    public function send();
}

Pozostawiam odpowiedzialność za wysłanie zapytania do klienta. Ta metoda zostanie zrefaktoryzowana później, ale później przejdę do szczegółów. Ten interfejs może żyć tam, gdzie czujesz się najwygodniej, trzymając go - zazwyczaj trzymam go w App \ Services \ Contracts, ale możesz użyć wyobraźni.

Gdy mam już wstępną umowę, zaczynam budować samo wdrożenie. Lubię tworzyć nową przestrzeń nazw dla każdej tworzonej integracji. Utrzymuje rzeczy pogrupowane i logiczne.

final class Client implements ClientContract
{
    public function __construct(
        private readonly PendingRequest $request,
    ) {}
}

Zacząłem przekazywać skonfigurowane PendingRequest do klienta API, ponieważ utrzymuje to czystość i pozwala uniknąć ręcznej konfiguracji. Podoba mi się takie podejście i zastanawiam się, dlaczego nie zrobiłem tego wcześniej!

Zauważysz, że nadal muszę przestrzegać umowy, ponieważ jest krok, który muszę wcześniej podjąć. Jedna z moich ulubionych funkcji PHP 8.1 - Enums.

Tworzę metodę Enum dla aplikacji, powinienem zrobić pakiet, ponieważ utrzymuje płynność - i znowu, nie ma pływających ciągów w mojej aplikacji!

enum Method: string
{
    case GET = 'GET';
    case POST = 'POST';
    case PUT = 'PUT';
    case PATCH = 'PATCH';
    case DELETE = 'DELETE';
}

Utrzymuję prostotę, aby zacząć i rozszerzać w razie potrzeby - i tylko wtedy, gdy jest to potrzebne. Omawiam główne czasowniki HTTP, których będę używać i dodaję więcej w razie potrzeby.

Mogę refaktoryzować moją umowę, aby uwzględnić sposób, w jaki chcę, aby to działało.

/**
 * @property-read PendingRequest $request
 */
interface ClientContract
{
    public function send(Method $method, string $url, array $options = []): Response;
}

Metoda wysyłania moich klientów powinna znać zastosowaną metodę, adres URL, na który jest wysyłana i wszelkie potrzebne opcje. Jest to prawie takie samo jak metoda wysyłania PendingRequest - poza użyciem Enum dla metody, jest to celowe.

Jednak nie dodaję tego do mojego klienta, ponieważ mogę mieć wielu klientów, którzy chcą wysyłać żądania. Tworzę więc troskę / cechę, którą mogę dodać do każdego klienta, umożliwiając mu wysyłanie żądań.

/**
 * @mixin ClientContract
 */
trait SendsRequests
{
    public function send(Method $method, string $url, array $options = []): Response
    {
        return $this->request->throw()->send(
            method: $method->value,
            url: $url,
            options: $options,
        );
    }
}

Standaryzuje to sposób wysyłania żądań interfejsu API i wymusza automatyczne zgłaszanie wyjątków. Mogę teraz dodać to zachowanie do samego klienta, czyniąc go czystszym i bardziej minimalistycznym.

final class Client implements ClientContract
{
    use SendsRequests;

    public function __construct(
        private readonly PendingRequest $request,
    ) {}
}

Stąd zaczynam określać zasoby i to, za co chcę, aby zasób był odpowiedzialny. W tym momencie chodzi o projekt obiektu. Dla mnie zasób jest punktem końcowym, zasobem zewnętrznym dostępnym za pośrednictwem warstwy transportowej HTTP. Nie potrzebuje wiele więcej.

Jak zwykle tworzę kontrakt/interfejs, za którym mają podążać wszystkie moje zasoby, co oznacza, że mam przewidywalny kod.

/**
 * @property-read ClientContract $client
 */
interface ResourceContract
{
    public function client(): ClientContract;
}

Chcemy, aby nasze zasoby miały dostęp do samego klienta metodą getter. Dodajemy również klienta jako właściwość docblock.

Teraz możemy stworzyć nasz pierwszy zasób. Skupimy się na problemach, ponieważ jest to dość ekscytujący punkt końcowy. Zacznijmy od stworzenia klasy i rozwinięcia jej.

final class IssuesResource implements ResourceContract
{
    use CanAccessClient;
}

Stworzyłem tutaj nową cechę / problem o nazwie CanAccessClient, ponieważ wszystkie nasze zasoby, bez względu na API, będą chciały uzyskać dostęp do klienta nadrzędnego. Przeniosłem też konstruktora do tej cechy/koncernu - przypadkowo odkryłem tę pracę i bardzo mi się podobała. Nadal zastanawiam się, czy zawsze będę to robił, czy też będę to tam trzymał, ale dzięki temu moje zasoby są czyste i skupione - więc na razie to zachowam. Chciałbym jednak usłyszeć twoje zdanie na ten temat!

/**
 * @mixin ResourceContract
 */
trait CanAccessClient
{
    public function __construct(
        private readonly ClientContract $client,
    ) {}
    public function client(): ClientContract
    {
        return $this->client;
    }
}

Teraz, gdy mamy już zasoby, możemy poinformować o tym naszego klienta - i zacząć patrzeć na ekscytującą część integracji: żądania.

final class Client implements ClientContract
{
    use SendsRequests;

    public function __construct(
        private readonly PendingRequest $request,
    ) {}
    public function issues(): IssuesResource
    {
        return new IssuesResource(
            client: $this,
        );
    }
}

Dzięki temu możemy mieć ładne i czyste API $client->issues()-> więc nie polegamy na magicznych metodach ani proxy niczego - jest czysty i wykrywalny dla naszego IDE.

Pierwszym żądaniem, które będziemy chcieli wysłać, jest lista wszystkich problemów dla uwierzytelnionego użytkownika. Punktem końcowym API jest https://api.github.com/issues, co jest dość proste. Przyjrzyjmy się teraz naszym prośbom i sposobowi, w jaki chcemy je wysłać. Tak, zgadłeś, będziemy potrzebować do tego ponownie umowy / interfejsu.

/**
 * @property-read ResourceContract $resource
 */
interface RequestContract
{
    public function resource(): ResourceContract;
}

Nasze żądanie zaimplementuje problem/cechę, która umożliwi wywołanie zasobu i przekazanie żądanego żądania z powrotem do klienta.

/**
 * @mixin RequestContract
 */
trait HasResource
{
    public function __construct(
        private ResourceContract $resource,
    ) {}
    public function resource(): ResourceContract
    {
        return $this->resource;
    }
}

W końcu możemy zacząć myśleć o prośbie, którą chcemy wysłać! Jest wiele schematów, aby dostać się do tego etapu. Jednak na dłuższą metę będzie warto. Możemy dostroić i wprowadzić zmiany w dowolnym punkcie tego łańcucha bez wprowadzania przełomowych zmian.

final class ListIssuesRequest implements RequestContract
{
    use HasResource;

    public function __invoke(): Response
    {
        return $this->resource()->client()->send(
            method: Method::GET,
            url: 'issues',
        );
    }
}

W tym momencie możemy zacząć zastanawiać się nad przekształceniem żądania, jeśli chcemy. Ustawiliśmy to tylko tak, aby bezpośrednio zwrócić odpowiedź, ale w razie potrzeby możemy ją dalej refaktoryzować. Sprawiamy, że nasza prośba jest inwokable, abyśmy mogli zadzwonić do niej bezpośrednio. Nasz interfejs API wygląda teraz następująco:

$client->issues()->list();

Pracujemy z Illuminate Response, aby uzyskać dostęp do danych od tego momentu, więc ma wszystkie wygodne metody, których możemy potrzebować. Jest to jednak tylko czasami idealne. Czasami chcemy użyć czegoś bardziej użytecznego jako obiektu w naszej aplikacji. Aby to zrobić, musimy przyjrzeć się przekształceniu reakcji.

Nie zamierzam tutaj zbytnio zagłębiać się w przekształcanie odpowiedzi i tego, co możesz zrobić, ponieważ myślę, że sam w sobie byłby świetnym samouczkiem. Daj nam znać, jeśli uważasz, że ten samouczek jest pomocny lub jeśli masz jakieś sugestie dotyczące ulepszenia tego procesu.

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