Laravel 第三方 API 串接
Laravel PHP

Laravel 官方網站關於第三方 API 串接的教程

商業,創業,美食,葡萄酒,閱讀,網路科技。

這是我的 FB粉專 以及 IG,我比較常使用 Threads,歡迎大家追蹤互動~

前陣子看到 Laravel 官方部落格有一篇關於 API 串接,很好的文章
https://laravel-news.com/working-with-data-in-api-integrations

larry 覺得這篇文章很好是在於,除了 API 串接的架構我們可以參考,另外關於 PHP 的寫法 (包含 PHP 8 的新語法),還有一些軟體工程的概念,都很值得我們參考。

這篇教程是以範例的方式來進行。首先,新增一個 service
app/Services/MedicalTrust/MedicalTrustService.php

Laravel service container 的觀念,要做 service container binding

// app/Providers/AppServiceProvider.php

public function boot()
{
    $this->app->singleton(MedicalTrustService::class,
        fn () => new MedicalTrustService(
	    baseUrl: ‘......’,
            apiToken: ‘.....’,
	),
    );
}

注意教程範例中,MedicalTrustService 本身就是 concrete class,也沒有 implement 任何介面,其實不用特別寫 binding (系統會自動 resolve)。他這樣寫應該是為了餵參數進去,如果參數寫在 service 裡面,binding 這一段其實可以省掉。

新增一個 trait
app/Services/Concerns/BuildsBaseRequest.php

這邊用的是 Laravel HTTP Client 的用法
https://laravel.com/docs/9.x/http-client

trait BuildsBaseRequest
{
    public function buildRequestWithToken(): PendingRequest
    {
        //…
    }

    public function buildRequestWithDigestAuth(): PendingRequest
    {
        //…
    }
}

新增一個 trait
app/Services/Concerns/CanSendGetRequest.php

trait CanSendGetRequest
{
    public function get(PendingRequest $request, string $url): Response
    {
        return $request->get(
    	    url: $url,
    	);
    }
}

新增一個 trait
app/Services/Concerns/CanSendPostRequest.php

trait CanSendPostRequest
{
    public function post(PendingRequest $request, string $url, array $payload = []): Response
    {
        return $request->post(
    	    url: $url,
    	    data: $payload,
    	);
    }
}

注意這份教程所有輸入參數到 function,都使用 PHP 8 Named Arguments 的語法。

還記得本文最上方新增的
app/Services/MedicalTrust/MedicalTrustService.php
此時就可以把這些 trait 都用上

class MedicalTrustService
{
    use BuildBaseRequest;
    use CanSendGetRequests;
    use CanSendPostRequests;
 
    // PHP 8 的 Constructor property promotion
    public function __construct(
        private readonly string $baseUrl,
        private readonly string $apiToken,
    ) {}

    // ...
}

注意這份教程所有 class constructor,都使用 PHP 8 Constructor property promotion 的語法。

新增一個 service
app/Services/MedicalTrust/Resources/DentalResource.php

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

MedicalTrustService 注入了 DentalResource,也可以說是 DentalResource 再包裝 MedicalTrustService

注意這種寫法,MedicalTrustService 把牙醫的 API 實作完全分離出去了。也就是如果我們要新增,例如復健科,要另新增一個 RehabResource.php

教程的作者提到,這樣寫可以,但要輸入 payload 時,很有可能是長長一段 json 或是 array,所以教程的作者希望把 API 輸入做成 object。下面重寫了 function addRecord,把 payload 用 NewDentalTreatment object 去實作 (larry 本篇重點是了解 whole picture,資料結構做成物件的細節我們先不看)。

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

在調用端 (caller side),我們新增一個 
app/Http/Controllers/Dental/Crowns/StoreController.php

class StoreController
{
    public function __construct(
        private readonly DentalResource $api,
	private readonly DentalTreatmentFactory $factory,
    ) {}
 
    public function __invoke(NewCrownRequest $request): RedirectResponse
    {
        // do some request validation here
	// if fails, throw Exception

    	$treatment = $this->api->addRecord(
    	    identifier: $request->get('patient'),
    	    request: $this->factory->make(
  	        attributes: $request->validated(),
  	    ),
  	);

  	// ...
    }
}

所以整個架構是 MedicalTrustService 注入 DentalResourceDentalResource 注入 controller。注意 controller function 的參數是 NewCrownRequest

NewCrownRequest 是 custom form request,$request->validated() 將 post 資料餵給 factory,產生 NewDentalTreatment object,餵給 DentalResource

Again,我們這篇主要是理解 whole picture,所以 factory method 如何產生 object 這一段我們暫時不看。

結論

基本上 get / post 等基礎工程寫在 MedicalTrustService,它是一個非常通用的 service。DentalResource 做的是針對這組 API,做 post 參數的格式,並將 payload 物件化,開有語意的 function 給 caller 端使用。將 DentalResource 注入 controller 就可以使用了。

本篇除了參考原教程作者串 API 的架構,另外就是參考他 PHP 的寫法。例如 trait 的使用,為什麼要用 MedicalTrustService 注入 DentalResource 這樣的分層。他也大量使用 PHP 8 的新寫法 Named Arguments 以及 Constructor property promotion。輸入的 array 資料物件化的部分也可以參考一下。

larry 最後想提的是,我們要參考的是原教程為什麼要這樣寫的精神,而不是拘泥於它的架構。實際上我們架構如何設計與撰寫,應該還是以實際的狀況和需求來判斷。

商業,創業,美食,葡萄酒,閱讀,網路科技。

這是我的 FB粉專 以及 IG,我比較常使用 Threads,歡迎大家追蹤互動~