Laravel 13 第三方 API 集成完全指南

现代应用经常需要与各种第三方 API 进行集成。本文将深入探讨 Laravel 13 中集成第三方 API 的最佳实践和高级技巧。

HTTP 客户端基础

基础请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Illuminate\Support\Facades\Http;

// GET 请求
$response = Http::get('https://api.example.com/users');

// POST 请求
$response = Http::post('https://api.example.com/users', [
'name' => 'John Doe',
'email' => 'john@example.com',
]);

// PUT 请求
$response = Http::put('https://api.example.com/users/1', [
'name' => 'Jane Doe',
]);

// DELETE 请求
$response = Http::delete('https://api.example.com/users/1');

请求选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$response = Http::withHeaders([
'Accept' => 'application/json',
'X-Custom-Header' => 'value',
])->get('https://api.example.com/users');

$response = Http::withToken('your-api-token')
->get('https://api.example.com/users');

$response = Http::withBasicAuth('username', 'password')
->get('https://api.example.com/users');

$response = Http::withDigestAuth('username', 'password')
->get('https://api.example.com/users');

$response = Http::withQueryParameters([
'page' => 1,
'per_page' => 15,
'sort' => 'name',
])->get('https://api.example.com/users');

处理响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$response = Http::get('https://api.example.com/users');

// 状态码
$statusCode = $response->status();
$ok = $response->ok();
$successful = $response->successful();
$failed = $response->failed();
$clientError = $response->clientError();
$serverError = $response->serverError();

// 响应体
$body = $response->body();
$json = $response->json();
$data = $response->object();

// 获取特定字段
$name = $response->json('data.user.name');
$users = $response->json('data.*.name');

// 响应头
$headers = $response->headers();
$contentType = $response->header('Content-Type');

封装 API 服务

创建 API 服务类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

namespace App\Services\Api;

use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\PendingRequest;

abstract class BaseApiService
{
protected string $baseUrl;
protected int $timeout = 30;
protected int $retryTimes = 3;
protected int $retrySleep = 100;

protected function client(): PendingRequest
{
return Http::baseUrl($this->baseUrl)
->timeout($this->timeout)
->retry($this->retryTimes, $this->retrySleep)
->withHeaders($this->getDefaultHeaders());
}

protected function getDefaultHeaders(): array
{
return [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
];
}

protected function get(string $uri, array $query = []): array
{
return $this->client()
->get($uri, $query)
->throw()
->json();
}

protected function post(string $uri, array $data = []): array
{
return $this->client()
->post($uri, $data)
->throw()
->json();
}

protected function put(string $uri, array $data = []): array
{
return $this->client()
->put($uri, $data)
->throw()
->json();
}

protected function delete(string $uri): array
{
return $this->client()
->delete($uri)
->throw()
->json();
}
}

具体服务实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php

namespace App\Services\Api;

class GitHubApiService extends BaseApiService
{
protected string $baseUrl = 'https://api.github.com';

public function __construct(
protected string $token
) {}

protected function getDefaultHeaders(): array
{
return array_merge(parent::getDefaultHeaders(), [
'Authorization' => "Bearer {$this->token}",
'X-GitHub-Api-Version' => '2022-11-28',
]);
}

public function getUser(string $username): array
{
return $this->get("users/{$username}");
}

public function getRepositories(string $username, array $options = []): array
{
return $this->get("users/{$username}/repos", $options);
}

public function getRepository(string $owner, string $repo): array
{
return $this->get("repos/{$owner}/{$repo}");
}

public function getIssues(string $owner, string $repo, array $filters = []): array
{
return $this->get("repos/{$owner}/{$repo}/issues", $filters);
}

public function createIssue(string $owner, string $repo, array $data): array
{
return $this->post("repos/{$owner}/{$repo}/issues", $data);
}
}

OAuth 集成

OAuth 服务类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php

namespace App\Services\OAuth;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class OAuthService
{
protected string $authorizeUrl;
protected string $tokenUrl;
protected string $clientId;
protected string $clientSecret;
protected string $redirectUri;

public function getAuthorizationUrl(string $state = null): string
{
$params = http_build_query([
'client_id' => $this->clientId,
'redirect_uri' => $this->redirectUri,
'response_type' => 'code',
'scope' => 'read write',
'state' => $state ?? Str::random(40),
]);

return "{$this->authorizeUrl}?{$params}";
}

public function getAccessToken(string $code): array
{
$response = Http::asForm()->post($this->tokenUrl, [
'grant_type' => 'authorization_code',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'redirect_uri' => $this->redirectUri,
'code' => $code,
]);

return $response->json();
}

public function refreshAccessToken(string $refreshToken): array
{
$response = Http::asForm()->post($this->tokenUrl, [
'grant_type' => 'refresh_token',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'refresh_token' => $refreshToken,
]);

return $response->json();
}
}

Google OAuth 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

namespace App\Services\OAuth;

class GoogleOAuthService extends OAuthService
{
protected string $authorizeUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
protected string $tokenUrl = 'https://oauth2.googleapis.com/token';

public function __construct()
{
$this->clientId = config('services.google.client_id');
$this->clientSecret = config('services.google.client_secret');
$this->redirectUri = config('services.google.redirect');
}

public function getUserInfo(string $accessToken): array
{
return Http::withToken($accessToken)
->get('https://www.googleapis.com/oauth2/v2/userinfo')
->json();
}
}

支付网关集成

Stripe 集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php

namespace App\Services\Payment;

use Stripe\Stripe;
use Stripe\PaymentIntent;
use Stripe\Customer;
use Stripe\Refund;

class StripeService
{
public function __construct()
{
Stripe::setApiKey(config('services.stripe.secret'));
Stripe::setApiVersion('2023-10-16');
}

public function createCustomer(array $data): Customer
{
return Customer::create([
'email' => $data['email'],
'name' => $data['name'],
'metadata' => $data['metadata'] ?? [],
]);
}

public function createPaymentIntent(int $amount, string $currency = 'usd', array $options = []): PaymentIntent
{
return PaymentIntent::create([
'amount' => $amount,
'currency' => $currency,
'automatic_payment_methods' => [
'enabled' => true,
],
...$options,
]);
}

public function retrievePaymentIntent(string $id): PaymentIntent
{
return PaymentIntent::retrieve($id);
}

public function refund(string $paymentIntentId, ?int $amount = null): Refund
{
return Refund::create([
'payment_intent' => $paymentIntentId,
'amount' => $amount,
]);
}

public function handleWebhook(string $payload, string $signature): array
{
$event = \Stripe\Webhook::constructEvent(
$payload,
$signature,
config('services.stripe.webhook_secret')
);

return match ($event->type) {
'payment_intent.succeeded' => $this->handleSuccessfulPayment($event->data->object),
'payment_intent.payment_failed' => $this->handleFailedPayment($event->data->object),
default => ['status' => 'ignored'],
};
}

protected function handleSuccessfulPayment($paymentIntent): array
{
// 处理成功支付
return ['status' => 'processed'];
}

protected function handleFailedPayment($paymentIntent): array
{
// 处理失败支付
return ['status' => 'failed'];
}
}

PayPal 集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<?php

namespace App\Services\Payment;

use Illuminate\Support\Facades\Http;

class PayPalService
{
protected string $baseUrl;
protected string $clientId;
protected string $clientSecret;
protected ?string $accessToken = null;

public function __construct()
{
$this->clientId = config('services.paypal.client_id');
$this->clientSecret = config('services.paypal.secret');
$this->baseUrl = config('services.paypal.sandbox')
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';
}

protected function getAccessToken(): string
{
if ($this->accessToken) {
return $this->accessToken;
}

$response = Http::withBasicAuth($this->clientId, $this->clientSecret)
->asForm()
->post("{$this->baseUrl}/v1/oauth2/token", [
'grant_type' => 'client_credentials',
]);

$this->accessToken = $response->json('access_token');

return $this->accessToken;
}

protected function client(): PendingRequest
{
return Http::withToken($this->getAccessToken())
->baseUrl($this->baseUrl)
->withHeaders([
'Content-Type' => 'application/json',
]);
}

public function createOrder(float $amount, string $currency = 'USD'): array
{
return $this->client()->post('/v2/checkout/orders', [
'intent' => 'CAPTURE',
'purchase_units' => [
[
'amount' => [
'currency_code' => $currency,
'value' => number_format($amount, 2, '.', ''),
],
],
],
])->json();
}

public function captureOrder(string $orderId): array
{
return $this->client()
->post("/v2/checkout/orders/{$orderId}/capture")
->json();
}

public function getOrder(string $orderId): array
{
return $this->client()
->get("/v2/checkout/orders/{$orderId}")
->json();
}

public function refund(string $captureId, float $amount, string $currency = 'USD'): array
{
return $this->client()->post("/v2/payments/captures/{$captureId}/refund", [
'amount' => [
'value' => number_format($amount, 2, '.', ''),
'currency_code' => $currency,
],
])->json();
}
}

云存储集成

AWS S3 集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php

namespace App\Services\Storage;

use Aws\S3\S3Client;
use Aws\Exception\AwsException;

class S3Service
{
protected S3Client $client;
protected string $bucket;

public function __construct()
{
$this->client = new S3Client([
'region' => config('filesystems.disks.s3.region'),
'version' => 'latest',
'credentials' => [
'key' => config('filesystems.disks.s3.key'),
'secret' => config('filesystems.disks.s3.secret'),
],
]);
$this->bucket = config('filesystems.disks.s3.bucket');
}

public function upload(string $key, $content, array $options = []): string
{
$this->client->putObject([
'Bucket' => $this->bucket,
'Key' => $key,
'Body' => $content,
'ContentType' => $options['content_type'] ?? 'application/octet-stream',
'Metadata' => $options['metadata'] ?? [],
]);

return $key;
}

public function download(string $key): string
{
$result = $this->client->getObject([
'Bucket' => $this->bucket,
'Key' => $key,
]);

return $result['Body']->getContents();
}

public function delete(string $key): bool
{
try {
$this->client->deleteObject([
'Bucket' => $this->bucket,
'Key' => $key,
]);
return true;
} catch (AwsException $e) {
return false;
}
}

public function getPresignedUrl(string $key, int $expires = 3600): string
{
$command = $this->client->getCommand('GetObject', [
'Bucket' => $this->bucket,
'Key' => $key,
]);

return (string) $this->client->createPresignedRequest($command, "+{$expires} seconds")->getUri();
}

public function listObjects(string $prefix = ''): array
{
$result = $this->client->listObjectsV2([
'Bucket' => $this->bucket,
'Prefix' => $prefix,
]);

return $result->get('Contents') ?? [];
}
}

消息推送集成

Firebase Cloud Messaging

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php

namespace App\Services\Push;

use Illuminate\Support\Facades\Http;

class FirebaseService
{
protected string $projectId;
protected string $accessToken;

public function __construct()
{
$this->projectId = config('services.firebase.project_id');
$this->accessToken = $this->getAccessToken();
}

protected function getAccessToken(): string
{
$credentials = json_decode(
file_get_contents(config('services.firebase.credentials_file')),
true
);

$response = Http::asForm()->post('https://oauth2.googleapis.com/token', [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $this->createJwt($credentials),
]);

return $response->json('access_token');
}

public function sendNotification(string $token, array $notification, array $data = []): array
{
return Http::withToken($this->accessToken)
->post("https://fcm.googleapis.com/v1/projects/{$this->projectId}/messages:send", [
'message' => [
'token' => $token,
'notification' => $notification,
'data' => $data,
],
])
->json();
}

public function sendToTopic(string $topic, array $notification, array $data = []): array
{
return Http::withToken($this->accessToken)
->post("https://fcm.googleapis.com/v1/projects/{$this->projectId}/messages:send", [
'message' => [
'topic' => $topic,
'notification' => $notification,
'data' => $data,
],
])
->json();
}

public function subscribeToTopic(string $token, string $topic): array
{
return Http::withToken($this->accessToken)
->post("https://iid.googleapis.com/iid/v1/{$token}/rel/topics/{$topic}")
->json();
}
}

错误处理

API 异常类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

namespace App\Exceptions;

use Exception;

class ApiException extends Exception
{
protected array $context;

public function __construct(string $message, int $code = 0, array $context = [])
{
parent::__construct($message, $code);
$this->context = $context;
}

public function getContext(): array
{
return $this->context;
}

public function report(): void
{
\Log::error('API Error', [
'message' => $this->getMessage(),
'code' => $this->getCode(),
'context' => $this->context,
]);
}
}

错误处理中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Client\ConnectionException;
use App\Exceptions\ApiException;

class HandleApiErrors
{
public function handle($request, Closure $next)
{
try {
return $next($request);
} catch (ConnectionException $e) {
throw new ApiException('API 连接失败', 503, [
'original_message' => $e->getMessage(),
]);
} catch (\Illuminate\Http\Client\RequestException $e) {
throw new ApiException(
$e->response->json('message', 'API 请求失败'),
$e->response->status(),
[
'response' => $e->response->json(),
]
);
}
}
}

缓存与限流

API 响应缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

namespace App\Services\Api;

use Illuminate\Support\Facades\Cache;

trait CachesResponses
{
protected int $cacheTtl = 3600;

protected function cachedGet(string $uri, array $query = [], ?int $ttl = null): array
{
$cacheKey = $this->generateCacheKey($uri, $query);

return Cache::remember($cacheKey, $ttl ?? $this->cacheTtl, function () use ($uri, $query) {
return $this->get($uri, $query);
});
}

protected function generateCacheKey(string $uri, array $query = []): string
{
return 'api:' . md5($uri . serialize($query));
}

protected function clearCache(string $uri, array $query = []): void
{
Cache::forget($this->generateCacheKey($uri, $query));
}
}

总结

Laravel 13 的第三方 API 集成提供了:

  • 强大的 HTTP 客户端
  • 灵活的服务封装模式
  • OAuth 认证支持
  • 支付网关集成
  • 云存储集成
  • 消息推送集成
  • 完善的错误处理

合理封装第三方 API 可以提高代码的可维护性和可测试性。