Làm việc với third party services trong laravel
Hơn hai năm trước, tôi đã viết một bài hướng dẫn về cách làm việc với các dịch vụ bên thứ ba trong Laravel. Cho đến ngày hôm nay, đó vẫn là trang được truy cập nhiều nhất trên trang web của tôi. Tuy nhiên, trong hai năm qua, có nhiều thay đổi xảy ra và tôi đã quyết định tiếp cận lại chủ đề này.
Tôi đã làm việc với các dịch vụ bên thứ ba trong một thời gian dài đến nỗi không thể nhớ khi nào tôi bắt đầu. Với tư cách là một Nhà phát triển Junior, tôi đã tích hợp các API vào các nền tảng khác nhau như Joomla, Magento và WordPress. Bây giờ, tôi chủ yếu tích hợp chúng vào các ứng dụng Laravel của mình để mở rộng logic kinh doanh bằng cách dựa vào các dịch vụ khác.
Hướng dẫn này sẽ mô tả cách tiếp cận thông thường của tôi khi tích hợp với một API ngày nay. Nếu bạn đã đọc bài hướng dẫn trước đây của tôi, hãy tiếp tục đọc vì một số điều đã thay đổi – với những lý do tôi cho là tốt.
Hãy bắt đầu với một API. Chúng ta cần một API để tích hợp. Bài hướng dẫn ban đầu của tôi đã tích hợp với PingPing, một giải pháp giám sát thời gian hoạt động tuyệt vời từ cộng đồng Laravel. Tuy nhiên, lần này tôi muốn thử một API khác.
Trong hướng dẫn này, chúng ta sẽ sử dụng Planetscale API. Planetscale là một dịch vụ cơ sở dữ liệu tuyệt vời mà tôi sử dụng để đưa các hoạt động đọc và ghi gần hơn với người dùng trong công việc hàng ngày của mình.
Integration của chúng ta sẽ làm gì? Hãy tưởng tượng rằng chúng ta có một ứng dụng cho phép quản lý cơ sở hạ tầng. Các máy chủ của chúng ta chạy thông qua Laravel Forge và cơ sở dữ liệu của chúng ta nằm trên Planetscale. Không có cách sạch sẽ nào để quản lý quy trình làm việc này, vì vậy chúng tôi đã tạo ra công cụ riêng của mình. Để làm điều này, chúng ta cần một hoặc hai tích hợp.
Ban đầu, tôi thường giữ tích hợp của mình dưới thư mục app/Services; tuy nhiên, khi ứng dụng của tôi trở nên lớn hơn và phức tạp hơn, tôi đã cần sử dụng namespace Services cho các dịch vụ nội bộ, dẫn đến tình trạng namespace bị ô nhiễm. Tôi đã chuyển tích hợp của mình vào app/Http/Integrations. Điều này hợp lý và là một mẹo tôi nhặt được từ Saloon của Sam Carrè.
Bây giờ tôi có thể sử dụng Saloon cho tích hợp API của mình, nhưng tôi muốn giải thích cách làm nó mà không cần một gói mở rộng. Nếu bạn cần tích hợp API vào năm 2023, tôi rất khuyến nghị sử dụng Saloon. Nó thực sự tuyệt vời!
Vậy, hãy bắt đầu bằng cách tạo một thư mục cho tích hợp của chúng ta. Bạn có thể sử dụng lệnh bash sau đây để thực hiện điều đó:
mkdir app/Http/Integrations/Planetscale
Sau khi có thư mục Planetscale, chúng ta cần tạo một cách để kết nối với nó. Một quy ước đặt tên khác mà tôi nhặt được từ thư viện Saloon là coi các lớp cơ sở này như các “connectors” – vì mục đích của chúng là cho phép bạn kết nối với một API hoặc bên thứ ba cụ thể.
Hãy tạo một lớp mới có tên PlanetscaleConnector trong thư mục app/Http/Integrations/Planetscale, và chúng ta có thể bắt đầu triển khai những gì lớp này cần, điều đó sẽ rất thú vị.
Vì vậy, chúng ta phải đăng ký lớp này với container để có thể giải quyết nó hoặc xây dựng một facade xung quanh nó. Chúng ta có thể đăng ký theo cách “dài” trong một Service Provider – nhưng phương pháp mới nhất của tôi là để các Connectors đăng ký chính chúng – một cách nào đó…
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(),
),
);
}
}
Ý tưởng ở đây là tất cả thông tin về cách lớp này được đăng ký vào container đều nằm trong chính lớp đó. Tất cả những gì service provider cần làm là gọi phương thức tĩnh register trên lớp đó! Điều này đã tiết kiệm rất nhiều thời gian cho tôi khi tích hợp với nhiều API vì tôi không cần tìm kiếm provider và tìm liên kết chính xác, giữa nhiều thứ khác. Tôi chỉ cần đi đến lớp đang xem xét, mọi thứ đều nằm trước mắt tôi.
Bạn sẽ nhận thấy hiện tại, chúng ta không có gì được truyền vào các phương thức token hoặc base url trong yêu cầu. Hãy sửa lỗi đó tiếp theo. Bạn có thể lấy được thông tin này trong tài khoản Planetscale của mình.
Tạo các bản ghi sau trong tệp .env của bạn.
PLANETSCALE_SERVICE_ID="your-service-id-goes-here"
PLANETSCALE_SERVICE_TOKEN="your-token-goes-here"
PLANETSCALE_URL="https://api.planetscale.com/v1"
Tiếp theo, chúng cần được đưa vào cấu hình của ứng dụng. Tất cả những thứ này thuộc về config/services.php vì đây là nơi các dịch vụ của bên thứ ba thường được định cấu hình.
return [
// the rest of your services config
'planetscale' => [
'id' => env('PLANETSCALE_SERVICE_ID'),
'token' => env('PLANETSCALE_SERVICE_TOKEN'),
'url' => env('PLANETSCALE_URL'),
],
];
Bây giờ chúng ta có thể sử dụng chúng trong PlanetscaleConnector của chúng ta dưới phương thức register.
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(),
),
);
}
}
Bạn cần gửi mã thông báo đến Planetscale theo định dạng sau: service-id:service-token, vì vậy chúng ta không thể sử dụng phương thức mặc định withToken vì nó không cho phép chúng ta tùy chỉnh theo cách chúng ta cần.
Bây giờ chúng ta đã tạo ra một lớp cơ bản, chúng ta có thể bắt đầu nghĩ về phạm vi của tích hợp của chúng ta. Chúng ta phải làm điều này khi tạo mã thông báo dịch vụ để thêm các quyền đúng. Trong ứng dụng của chúng ta, chúng ta muốn có khả năng thực hiện các công việc sau:
- Liệt kê cơ sở dữ liệu.
- Liệt kê vùng cơ sở dữ liệu.
- Liệt kê sao lưu cơ sở dữ liệu.
- Tạo sao lưu cơ sở dữ liệu.
- Xóa sao lưu cơ sở dữ liệu.
Vì vậy, chúng ta có thể nhìn vào việc nhóm chúng thành hai danh mục:
- Cơ sở dữ liệu.
- Sao lưu.
Hãy thêm hai phương thức mới vào connector của chúng ta để tạo ra những gì chúng ta cần:
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(),
),
);
}
}
Như bạn đã thấy, chúng ta đã tạo hai phương thức mới là databases
và backups
. Hai phương thức này sẽ trả về các lớp tài nguyên mới, thông qua việc truyền qua connector. Bây giờ, chúng ta có thể triển khai logic trong các lớp tài nguyên, nhưng chúng ta cần thêm một phương thức khác vào connector của chúng ta sau này.
<?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()
{
//
}
}
Đây là DatabaseResource của chúng ta; chúng ta đã tạo ra các phương thức mẫu mà chúng ta muốn triển khai. Bạn có thể làm điều tương tự cho BackupResource. Nó sẽ trông tương tự.
Vì vậy, kết quả có thể được phân trang trong danh sách cơ sở dữ liệu. Tuy nhiên, ở đây tôi sẽ không xử lý phân trang – tôi sẽ dựa vào Saloon cho điều này, vì triển khai của nó cho kết quả phân trang rất tuyệt vời. Trong ví dụ này, chúng ta sẽ không quan tâm đến phân trang. Trước khi hoàn thiện DatabaseResource, chúng ta cần thêm một phương thức nữa vào PlanetscaleConnector để gửi các yêu cầu một cách dễ dàng. Để làm điều này, tôi đang sử dụng gói của tôi được gọi là juststeveking/http-helpers, nó có một enum cho tất cả các phương thức HTTP thông thường mà tôi sử dụng.
public function send(Method $method, string $uri, array $options = []): Response
{
return $this->request->send(
method: $method->value,
url: $uri,
options: $options,
)->throw();
}
Bây giờ chúng ta có thể quay lại DatabaseResource và bắt đầu điền vào logic cho phương thức 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()
{
//
}
}
Phương thức list
của chúng ta chấp nhận tham số organization
để chuyển tiếp vào việc liệt kê cơ sở dữ liệu theo tổ chức. Sau đó, chúng ta sử dụng tham số này để tạo URL cụ thể và gửi yêu cầu thông qua connector. Đặt nó trong một khối try-catch cho phép chúng ta bắt các ngoại lệ có thể xảy ra từ phương thức sendRequest
của connector. Cuối cùng, chúng ta có thể trả về một tập hợp (collection) từ phương thức để làm việc với nó trong ứng dụng của chúng ta.
Chúng ta có thể đi vào chi tiết hơn về yêu cầu này, vì chúng ta có thể bắt đầu ánh xạ dữ liệu từ mảng sang một cái gì đó mang tính ngữ cảnh sử dụng DTOs (Data Transfer Objects). Tôi đã viết về điều này ở đây, vì vậy tôi sẽ không lặp lại ở đây.
Bây giờ, hãy nhanh chóng xem qua BackupResource
để tìm hiểu nhiều hơn về các yêu cầu không chỉ là yêu cầu GET.
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');
}
}
Phương thức create
của chúng ta chấp nhận một lớp thực thể (entity class), tôi sử dụng nó để chuyển dữ liệu qua ứng dụng khi cần thiết. Điều này hữu ích khi URL cần một tập hợp các tham số và chúng ta cần gửi một phần thân yêu cầu thông qua.
Ở đây, tôi chưa bàn về việc kiểm thử (testing), nhưng tôi đã viết một hướng dẫn về cách kiểm thử các điểm cuối JSON:API bằng PestPHP ở đây, nơi có các khái niệm tương tự để kiểm thử một tích hợp như thế này.
Tôi có thể tạo ra các tích hợp đáng tin cậy và có thể mở rộng với các bên thứ ba sử dụng phương pháp này. Nó được chia thành các phần hợp lý, vì vậy tôi có thể xử lý lượng logic. Thông thường, tôi sẽ có nhiều tích hợp hơn, vì vậy một số phần logic này có thể được chia sẻ và trích xuất thành traits để kế thừa hành vi giữa các tích hợp.