商業,創業,美食,葡萄酒,閱讀,網路科技。
這是我的 FB粉專 以及 IG,我比較常使用 Threads,歡迎大家追蹤互動~
前陣子看到 Laravel 官方部落格有一篇關於 API 串接,很好的文章
https://laravel-news.com/working-with-data-in-api-integrations
larry 覺得這篇文章很好是在於,除了 API 串接的架構我們可以參考,另外關於 PHP 的寫法 (包含 PHP 8 的新語法),還有一些軟體工程的概念,都很值得我們參考。
這篇教程是以範例的方式來進行。首先,新增一個 serviceapp/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 這一段其實可以省掉。
新增一個 traitapp/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
{
//…
}
}
新增一個 traitapp/Services/Concerns/CanSendGetRequest.php
trait CanSendGetRequest
{
public function get(PendingRequest $request, string $url): Response
{
return $request->get(
url: $url,
);
}
}
新增一個 traitapp/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 的語法。
新增一個 serviceapp/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
注入 DentalResource
,DentalResource
注入 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 最後想提的是,我們要參考的是原教程為什麼要這樣寫的精神,而不是拘泥於它的架構。實際上我們架構如何設計與撰寫,應該還是以實際的狀況和需求來判斷。
商業,創業,美食,葡萄酒,閱讀,網路科技。