在 API 集成中使用数据
发布于 作者: 史蒂夫·麦克杜格尔
与第三方 API 交互可能很令人沮丧;我们得到 JSON 响应,在 PHP 中将表示为普通数组 - 我们也以数组形式发送数据。我们失去了大量的上下文和构建具有出色开发者体验的东西的能力。如果我告诉你事情不必如此呢?如果我告诉你构建一些可以添加更多上下文并改善您与第三方 API 交互的东西并不需要多少努力呢?不相信我?让我们来看一看。
本教程将介绍如何将一个虚构的第三方 API 与嵌套数据集成在一起 - 最混乱的 API。我们将希望能够从 API 获取数据,但也能够将数据发送到该 API,而无需构建我们习惯使用的这些讨厌的数组。
在这里改善您体验的最佳方法是使用最新版本的 PHP 和第三方软件包,如 Laravel Saloon 或 Laravel Transporter - 但有时您不想仅仅为了发出几个 API 请求就引入整个软件包,对吧?如果我们这样做,我们的整个应用程序将变得脆弱,并且依赖于如此多的第三方代码,我们可能不如使用网站构建器。
我们将要集成的 API 是虚构的,它告诉我们用户的/患者的病史。想象一个您正在使用的 API,您希望能够向其中添加新数据 - 例如,全科医生的 Web 应用程序或移动应用程序,您去预约,他们需要将任何进一步的问题或注释注册到您的档案中。他们可能希望检查您的病史并查看您的档案中目前有哪些内容。
开始使用此方法的最佳方法是构建一个服务类,并且根据您需要集成的 API 的数量,通常会指引您进行集成的正确方向。我将构建它,就好像我将来需要与多个 API 集成一样 - 例如,心理健康数据位于完全独立的 API 上。因此,我们将来需要与它集成。我们要做的第一件事是在我们的 app
目录中创建一个用于 Services
的新命名空间,以便我们可以为我们的服务连接提供一个位置。在内部,我们将为需要集成的每个服务创建一个新的命名空间,这些服务是外部或内部服务。将它们按这种方式分组很好,因为它为您提供了一个标准 - 如果您需要扩展,那么毫无疑问它属于哪里;在 App\Services\
中创建一个新的服务集成,就可以开始了。
因此,我们的虚构 API 被称为 medicaltrust
,这是我在编写本教程时想出的一个随机名称 - 如果这确实是已经存在的 API,那么我表示歉意。本教程与该 API 无关,无论从任何方面来看都并非基于该 API。现在创建一个新的目录/命名空间 app/Services/MedicalTrust
;在里面,我们将创建一个类来处理我们的集成 - 如果您阅读了我关于 Laravel Saloon 的教程,那么请将此视为一个连接器。一个处理与 API 的主要连接的类。我将我的类命名为 MedicalTrustService
,因为我喜欢在命名方面尽可能明确,并确保它看起来像下面这样
declare(strict_types=1); namespace App\Services\MedicalTrust; class MedicalTrustService{ public function __construct( private readonly string $baseUrl, private readonly string $apiToken, ) {}}
因此,对于这个 API,我们需要两样东西,一个基本 URL 和一个 API 令牌 - 没什么特别的。在 config/services.php
中添加以下代码块
return [ 'medical-trust' => [ 'url' => env('MEDICAL_TRUST_URL'), 'token' => env('MEDICAL_TRUST_TOKEN'), ]];
在添加第三方服务的配置选项时,我发现最好将它们保存在同一个地方,即使您需要很多配置选项。在使用 API 时,维护处理此问题的统一标准至关重要,因为标准是大多数现代 API 的基础。为了引导我们的服务,我们需要在我们的 app/Providers/AppServiceProvider.php
中添加一条新记录,告诉它在容器中将我们的服务类注册为依赖项,以构建该类。因此,在启动方法中添加以下内容
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')), ), );}
我们在这里所做的只是在容器中添加一个新的单例,当我们请求一个 MedicalTrustService
时,如果我们之前没有构建过一个 - 那么就使用这些配置值构建它。我们使用 strval
来确保它是一个字符串,因为 config()
默认情况下会返回混合类型。这部分相对简单,因此让我们继续提供一种构建一致请求的方法来发送。
发送请求是与 API 集成的唯一目的,因此您希望确保您明智地进行处理。正如我在教程开头所说,我们将以我们将与多个 API 集成的长期目标来处理这个问题。因此,我们需要做的是从服务中抽象出共享的功能。最好的方法是使用 PHP 中的 Traits - 如果你遵循三条规则,那么这将有意义。如果您重复相同的代码或大致相同的代码超过两次,则应该进行抽象。我们可能希望抽象或在一定程度上控制哪些方面?构建我们的基本请求模板是其中之一,确保我们正确地配置了它。发送请求是另一个 - 我们需要确保能够在实际情况下按每个 API 的基础控制我们可以发送的请求。因此,让我们创建一些 Traits 来使这更容易。
首先,我们将创建一个用于控制构建基本请求的 Trait,它在 Trait 中可以有一些针对该方法的选项。这些将位于 Services 命名空间内,但在 Concerns
命名空间下。在 Laravel 中,尤其是 Eloquent 中的 Traits 被称为 Concerns - 因此我们将在这里匹配 Laravel 的命名约定。创建一个名为 app/Services/Concerns/BuildsBaseRequest.php
的新 Trait,并将以下代码添加到其中
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, ); }}
我们在这里所做的是创建一个标准方法,该方法将使用已设置基本 URL 的 Pending Request 创建 - 这依赖于遵循将基本 URL 注入服务类构造函数的标准 - 这就是为什么遵循标准或模式至关重要的原因。然后,我们有一些可选方法,可以使用令牌或摘要身份验证扩展请求。以这种方式处理它允许我们非常灵活,并且我们没有做任何极端的事情或更适合在其他地方做的事情。将这些方法添加到每个服务中是可以的,但是当您开始与越来越多的 API 集成时,拥有一个集中化的方式来做到这一点至关重要。
我们下一组 Concerns/Traits 将有助于控制我们如何将请求发送到第三方 API - 我们希望有多个 Concerns/Traits 来限制我们可以发送的请求类型。第一个将是 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, ); }}
接下来,让我们创建一个 app/Services/Concerns/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, ); }}
如您所见,我们正在将 HTTP 动词构建到 Traits 中,以确保我们的控制力具体,并确保请求始终正确发送。对于一些项目来说,这绝对是过度杀伤,但是想象一下,您正在与 10 多个 API 集成;突然之间,这种方法就不那么愚蠢了。
让我们再花点时间思考一下服务类本身。我们是否要构建这个服务类,使其包含 10-20 多个方法以确保覆盖所有 API 端点?可能性不大;这听起来很乱,对吧?相反,我们将创建特定的资源类,这些类可以使用服务类构建,或直接注入到方法中。由于这是一个虚构的医疗 API,我们将从牙科记录开始。让我们回到我们的 `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, ); }}
我们有一个名为 `dental` 的新方法,它将返回一个特定于牙科资源端点的资源类。我们将服务注入到构造函数中,以调用服务方法,例如 `get` 或 `post` 或 `buildRequestWithToken`。现在让我们看一下这个类,看看我们应该如何构建它 `app/Services/MedicalTrust/Resources/DentalResource.php`
declare(strict_types=1); namespace App\Services\MedicalTrust\Resources; class DentalResource{ public function __construct( private readonly MedicalTrustService $service, ) {}}
非常简单,真的。我们可以从容器中解析它,因为它不需要任何特殊的东西。所以对于牙科记录,假设我们想要列出所有记录并添加一个新记录 - 使用数组这将如何呈现?
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, ); }}
正如你所看到的,这相对直接。我们传递一个标识符来识别用户,然后使用服务,我们使用 `buildRequestWithToken` 作为基础请求,发送一个 `get` 或 `post` 请求。但是,对我来说,这种方法存在缺陷。首先,我们只是按原样返回响应。没有上下文,没有信息 - 只是一个数组。现在,这很好,尤其是在构建 SDK 时 - 但很有可能我们希望围绕响应提供更多信息。请求呢?是的,我们可能使用 HTTP 验证对传入请求进行了一些验证 - 但是如何控制我们发送到 API 的数据?让我们看看如何处理这个问题,以使数组成为过去,上下文对象成为未来。
在完全移除数组之前,我们需要了解数据是如何显示的以及是什么创建了这些数据。让我们看一个牙科记录的示例负载
{ "id": "1234-1234-1234-1234", "treatments": { "crowns": [ { "material": "porcelain", "location": "L12", "implemented": "2022-07-10" } ], "fillings": [ { "material": "white", "location": "R8", "implemented": "2022-07-10" } ] }}
所以我们有一个患者的标识符,然后是一个治疗对象。治疗对象包含患者接受的牙冠和填充物。实际上,这会更大,包含更多信息。牙冠和填充物是已应用的牙科修复的数组 - 使用的材料、使用牙科术语的牙齿以及实施治疗的日期。现在,让我们首先以数组格式看一下
['id' => '1234-1234-1234-1234','treatment' => [ 'crowns' => [ [ 'material' => 'porcelain', 'location' => 'L12', 'implemented' => '2022-07-10', ], ], 'fillings' => [ [ 'material' => 'white', 'location' => 'R8', 'implemented' => '2022-07-10', ], ]]];
不太好,对吧?是的,它相对较好地表示了数据,但想象一下,如果要在 UI 或其他任何地方使用这些数据。有什么替代方案?我们能做些什么来解决这个问题?首先,让我们设计一个表示牙科治疗的对象:`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(), ]; }}
相反,我们现在有一个可以构建的类 - 当查看时,我们知道它的含义。我们明白,这个对象或这组数据与牙科治疗有关。让我们向上走一级,看看治疗本身:`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(), ]; }}
同样,与之前一样,我们有一个特定的类来表示用户可能进行的所有治疗 - 并且可以扩展它以包含其他治疗。假设我们现在想要提供贴面 - 我们可以添加一个新属性并为此创建数据对象。让我们看看像 Crowns 这样的对象可能是什么样子:`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(); }}
这次,我们的构造函数只包含一个可以添加的治疗集合。我们可以使用 docblock 对其进行类型提示,以确保我们只在需要时向其中添加 DentalTreatments。然后,当我们将它转换为数组时,我们遍历治疗(为每个项目进行类型提示)并将治疗转换为数组 - 最后将整个内容转换为数组。我们在类上拥有 `toArray` 方法的原因是,我们可以使用 eloquent 轻松地将其保存到数据库中:`Treatment::query()->create($treatment->toArray());`,也可以用于 CLI 显示和表格。我注意到在这些数据对象上非常有效的一个方便的事情。
那么我们如何利用这些?手动在服务中构建它们是否会让它感觉臃肿?我喜欢使用数据对象工厂构建这些对象,该工厂接受数据作为数组并将其作为对象返回。让我们为牙科治疗(最低级别)创建一个:`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')); ); }}
因此,我们有一个带有 make 方法的工厂,该方法接受属性数组。然后,我们使用 Laravel 的 `data_get` 帮助程序创建一个新的 Dental Treatment 对象,并确保将其转换为正确的类型。对于 `implemented` 属性,我们使用 Carbon 解析传入的日期。现在更进一步,让我们看看如何创建 crows:`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, ), ), ); }}
所以这个比上一个复杂一点。这次,我们传递了一个治疗数组并新建一个集合。然后,一旦我们有了集合,我们想要遍历每个治疗并使用牙科治疗工厂将其制作成一个 Dental Treatment 对象。为了让它更容易使用,我们可以在 Data Factories 中添加一个名为 `new` 的静态方法,该方法接受一个数组并只调用 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')); ); }}
这将使我们的 Crows Factory 更干净
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, ), ), ); }}
或者,我们甚至可以更方便地使用它,通过告诉 DentalTreatment Factory 为我们创建一个集合
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')); ); }}
这将使我们能够进一步简化 Crowns 工厂
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, ), ); }}
这里的道理是,限制在于什么能让你的生活更轻松。也许你不需要走这么远,或者也许你的 API 更扁平,所以很容易实现这种方法。但是,如果我们采用这种方法并应用对我们有效的因素,我们就能获得更具上下文性的 API 响应,并且可以更容易地理解和处理响应。
退一步说,我们还想能够通过 API 创建新的治疗方案。我们想要能够填写表格 - 或类似的东西,并将数据发布到 API 以注册我们已实施了新的治疗方案。为此,我们需要通过我们的 `DentalResource` 使用 `addRecord` 方法发送一个 post 请求。这并不糟糕,但让我们看一下我们可能用来发送 PHP 数组的示例负载
[ 'type' => 'crown', 'material' => 'porcelain', 'location' => 'L12', 'implemented' => now()->toDateString(),];
这不是最糟糕的有效负载,但如果我们想要进行一些验证或扩展它怎么办?重点是,请求数据也缺乏上下文,对开发人员不友好,而且我们没有为应用程序添加任何价值。因此,我们可以做一些不同的事情 - 就像我们对响应所做的那样,我们也可以对请求做同样的事情;构建一个我们使用的对象,并可以将其转换为数组。首先,让我们为请求创建数据对象:`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(), ]; }}
所以这次,我们使用的是对象。就像之前一样,我们将为此创建一个工厂:`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')), ); }}
现在让我们重构服务上的 `addRecord` 方法
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(), ); }}
在这一点上,我们有一个更干净的方法。我们可以点击进入请求类并查看它包含的内容。但为了欣赏这一点,我们可以退一步看看我们如何实现它。想象一下,我们现在有一个处理它的控制器,它是一个来自 Web 表单的 post 请求,它是一个我们用来添加新 Crown 的特定表单:`app/Http/Controllers/Dental/Crowns/StoreController.php`,第一次,我们将使用数组
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... }}
这并不糟糕,对吧?这是相当合理的。我们可以使用表单请求验证来自表单的有效负载,并将验证后的数据传递给资源以添加新记录。但是,我们无法对业务逻辑做任何事情;我们在这里只依赖于 HTTP 验证。让我们看看使用对象可以做些什么
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... }}
所以我们现在使用的是对象而不是数组,但是业务逻辑呢?是的,我们正在做一些可以捕获一些东西的 HTTP 验证 - 但我们还能做些什么?让我们看看如何验证数组
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... }}
所以我们有很多可用的验证选项 - 但从逻辑上讲,我们还想检查 HTTP 验证之外的内容。我们是否支持这种类型 - 因为我们只进行了一个牙冠,我们是否传递了一个有效的牙科术语位置?我们是否可以使用这种材料制作牙冠?所有这些,我们想要确保我们知道并可以编程。是的,我们可以在表单请求中添加所有这些内容,但这会导致请求变得更大。我们想要使用 Laravel 表单请求从基本级别验证输入,并在拥有业务逻辑的地方验证业务逻辑,以便我们在 Web、API 和 CLI 中获得类似的体验。那么使用对象这将是什么样子
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... }}
这次,我们使用工厂从验证后的数据(HTTP 有效数据)创建对象。在这一点上,我们已经通过了 Web 验证。现在我们可以继续进行业务验证。因此,我们创建对象,然后在其上调用验证,这是一个我们需要添加的新方法
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; }}
所以正如你所看到的,请求对象可以为我们保存自己的业务规则 - 这意味着对象可以进行自我验证,而不会给你的 Web 应用程序和 CLI 实现增加额外的复杂性。我认为这种方法的强大之处在于此。在处理 API 的方法标准化方面。这没有什么新奇或突破性的,但采用这种方法意味着你可以以最适合你的方式以一致的标准精确控制你的 API 集成。
你是如何处理 API 数据和请求的?我很想知道有多少其他人找到了类似的有帮助的方法。你认为有没有办法可以改进它?请在 Twitter 上告诉我们,因为我们都喜欢学习和成长!
《Laravel News》的技术作家,Treblle 的开发者倡导者。API 专家,经验丰富的 PHP/Laravel 工程师。《Just Steve King》的 YouTube 直播员。