Laravel 13 适配器模式深度解析

适配器模式是一种结构型设计模式,它允许不兼容的接口之间能够协同工作。本文将深入探讨 Laravel 13 中适配器模式的高级用法。

适配器模式基础

什么是适配器模式

适配器模式将一个类的接口转换成客户端期望的另一个接口,使原本因接口不兼容而不能一起工作的类可以一起工作。

1
2
3
4
5
6
7
8
<?php

namespace App\Contracts;

interface TargetInterface
{
public function request(): string;
}
1
2
3
4
5
6
7
8
9
10
11
<?php

namespace App\Services;

class Adaptee
{
public function specificRequest(): string
{
return "Specific request from Adaptee";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Adapters;

use App\Contracts\TargetInterface;
use App\Services\Adaptee;

class Adapter implements TargetInterface
{
protected Adaptee $adaptee;

public function __construct(Adaptee $adaptee)
{
$this->adaptee = $adaptee;
}

public function request(): string
{
return $this->adaptee->specificRequest();
}
}

支付网关适配器

统一支付接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

namespace App\Contracts\Payments;

interface PaymentGatewayInterface
{
public function charge(float $amount, array $options = []): PaymentResult;

public function refund(string $transactionId, float $amount = null): bool;

public function getStatus(string $transactionId): string;

public function getTransaction(string $transactionId): ?array;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace App\DTOs;

class PaymentResult
{
public function __construct(
public bool $success,
public ?string $transactionId = null,
public ?string $message = null,
public array $data = []
) {}
}

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
79
<?php

namespace App\Adapters\Payments;

use App\Contracts\Payments\PaymentGatewayInterface;
use App\DTOs\PaymentResult;
use Stripe\StripeClient;

class StripeAdapter implements PaymentGatewayInterface
{
protected StripeClient $stripe;

public function __construct(array $config)
{
$this->stripe = new StripeClient($config['secret_key']);
}

public function charge(float $amount, array $options = []): PaymentResult
{
try {
$charge = $this->stripe->charges->create([
'amount' => (int)($amount * 100),
'currency' => $options['currency'] ?? 'usd',
'source' => $options['token'],
'description' => $options['description'] ?? '',
'metadata' => $options['metadata'] ?? [],
]);

return new PaymentResult(
success: $charge->status === 'succeeded',
transactionId: $charge->id,
message: 'Payment successful',
data: $charge->toArray()
);
} catch (\Exception $e) {
return new PaymentResult(
success: false,
message: $e->getMessage()
);
}
}

public function refund(string $transactionId, float $amount = null): bool
{
try {
$params = ['charge' => $transactionId];

if ($amount !== null) {
$params['amount'] = (int)($amount * 100);
}

$refund = $this->stripe->refunds->create($params);

return $refund->status === 'succeeded';
} catch (\Exception $e) {
return false;
}
}

public function getStatus(string $transactionId): string
{
try {
$charge = $this->stripe->charges->retrieve($transactionId);
return $charge->status;
} catch (\Exception $e) {
return 'unknown';
}
}

public function getTransaction(string $transactionId): ?array
{
try {
$charge = $this->stripe->charges->retrieve($transactionId);
return $charge->toArray();
} catch (\Exception $e) {
return null;
}
}
}

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
88
89
90
91
92
93
94
95
96
97
<?php

namespace App\Adapters\Payments;

use App\Contracts\Payments\PaymentGatewayInterface;
use App\DTOs\PaymentResult;
use PayPal\Rest\ApiContext;
use PayPal\Api\Payment;
use PayPal\Api\Amount;
use PayPal\Api\Transaction;
use PayPal\Api\Charge;

class PayPalAdapter implements PaymentGatewayInterface
{
protected ApiContext $apiContext;

public function __construct(array $config)
{
$this->apiContext = new ApiContext(
new \PayPal\Auth\OAuthTokenCredential(
$config['client_id'],
$config['secret']
)
);

$this->apiContext->setConfig([
'mode' => $config['mode'] ?? 'sandbox',
]);
}

public function charge(float $amount, array $options = []): PaymentResult
{
try {
$payer = new \PayPal\Api\Payer();
$payer->setPaymentMethod('paypal');

$amountObj = new Amount();
$amountObj->setTotal((string)$amount)
->setCurrency($options['currency'] ?? 'USD');

$transaction = new Transaction();
$transaction->setAmount($amountObj)
->setDescription($options['description'] ?? '');

$payment = new Payment();
$payment->setIntent('sale')
->setPayer($payer)
->setTransactions([$transaction]);

$payment->create($this->apiContext);

return new PaymentResult(
success: $payment->getState() === 'approved',
transactionId: $payment->getId(),
message: 'Payment successful',
data: $payment->toArray()
);
} catch (\Exception $e) {
return new PaymentResult(
success: false,
message: $e->getMessage()
);
}
}

public function refund(string $transactionId, float $amount = null): bool
{
try {
$sale = \PayPal\Api\Sale::get($transactionId, $this->apiContext);
$refund = $sale->refund(new \PayPal\Api\Refund(), $this->apiContext);

return $refund->getState() === 'completed';
} catch (\Exception $e) {
return false;
}
}

public function getStatus(string $transactionId): string
{
try {
$payment = Payment::get($transactionId, $this->apiContext);
return $payment->getState();
} catch (\Exception $e) {
return 'unknown';
}
}

public function getTransaction(string $transactionId): ?array
{
try {
$payment = Payment::get($transactionId, $this->apiContext);
return $payment->toArray();
} catch (\Exception $e) {
return null;
}
}
}

缓存适配器

统一缓存接口

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

namespace App\Contracts\Cache;

interface CacheInterface
{
public function get(string $key, mixed $default = null): mixed;

public function set(string $key, mixed $value, ?int $ttl = null): bool;

public function delete(string $key): bool;

public function has(string $key): bool;

public function clear(): bool;

public function increment(string $key, int $value = 1): int;

public function decrement(string $key, int $value = 1): int;
}

Redis 适配器

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
<?php

namespace App\Adapters\Cache;

use App\Contracts\Cache\CacheInterface;
use Redis;

class RedisAdapter implements CacheInterface
{
protected Redis $redis;

public function __construct(array $config)
{
$this->redis = new Redis();
$this->redis->connect(
$config['host'] ?? '127.0.0.1',
$config['port'] ?? 6379
);

if (isset($config['password'])) {
$this->redis->auth($config['password']);
}

if (isset($config['database'])) {
$this->redis->select($config['database']);
}
}

public function get(string $key, mixed $default = null): mixed
{
$value = $this->redis->get($key);

if ($value === false) {
return $default;
}

return unserialize($value);
}

public function set(string $key, mixed $value, ?int $ttl = null): bool
{
$serialized = serialize($value);

if ($ttl !== null) {
return $this->redis->setex($key, $ttl, $serialized);
}

return $this->redis->set($key, $serialized);
}

public function delete(string $key): bool
{
return $this->redis->del($key) > 0;
}

public function has(string $key): bool
{
return $this->redis->exists($key) > 0;
}

public function clear(): bool
{
return $this->redis->flushDB();
}

public function increment(string $key, int $value = 1): int
{
return $this->redis->incrBy($key, $value);
}

public function decrement(string $key, int $value = 1): int
{
return $this->redis->decrBy($key, $value);
}
}

Memcached 适配器

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
<?php

namespace App\Adapters\Cache;

use App\Contracts\Cache\CacheInterface;
use Memcached;

class MemcachedAdapter implements CacheInterface
{
protected Memcached $memcached;

public function __construct(array $config)
{
$this->memcached = new Memcached();
$this->memcached->addServers($config['servers'] ?? [['127.0.0.1', 11211]]);
}

public function get(string $key, mixed $default = null): mixed
{
$value = $this->memcached->get($key);

if ($this->memcached->getResultCode() === Memcached::RES_NOTFOUND) {
return $default;
}

return $value;
}

public function set(string $key, mixed $value, ?int $ttl = null): bool
{
return $this->memcached->set($key, $value, $ttl ?? 0);
}

public function delete(string $key): bool
{
return $this->memcached->delete($key);
}

public function has(string $key): bool
{
$this->memcached->get($key);
return $this->memcached->getResultCode() !== Memcached::RES_NOTFOUND;
}

public function clear(): bool
{
return $this->memcached->flush();
}

public function increment(string $key, int $value = 1): int
{
$result = $this->memcached->increment($key, $value);
return $result === false ? 0 : $result;
}

public function decrement(string $key, int $value = 1): int
{
$result = $this->memcached->decrement($key, $value);
return $result === false ? 0 : $result;
}
}

文件系统适配器

统一文件系统接口

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
<?php

namespace App\Contracts\Storage;

interface StorageInterface
{
public function read(string $path): ?string;

public function write(string $path, string $contents, array $options = []): bool;

public function delete(string $path): bool;

public function exists(string $path): bool;

public function size(string $path): int;

public function lastModified(string $path): int;

public function copy(string $from, string $to): bool;

public function move(string $from, string $to): bool;

public function files(string $directory): array;

public function directories(string $directory): array;

public function url(string $path): string;
}

本地存储适配器

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<?php

namespace App\Adapters\Storage;

use App\Contracts\Storage\StorageInterface;

class LocalStorageAdapter implements StorageInterface
{
protected string $root;

public function __construct(string $root)
{
$this->root = rtrim($root, '/\\');
}

public function read(string $path): ?string
{
$fullPath = $this->getFullPath($path);

if (!file_exists($fullPath)) {
return null;
}

return file_get_contents($fullPath);
}

public function write(string $path, string $contents, array $options = []): bool
{
$fullPath = $this->getFullPath($path);

$directory = dirname($fullPath);
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}

return file_put_contents($fullPath, $contents, $options['flags'] ?? 0) !== false;
}

public function delete(string $path): bool
{
$fullPath = $this->getFullPath($path);

if (is_dir($fullPath)) {
return rmdir($fullPath);
}

return unlink($fullPath);
}

public function exists(string $path): bool
{
return file_exists($this->getFullPath($path));
}

public function size(string $path): int
{
return filesize($this->getFullPath($path));
}

public function lastModified(string $path): int
{
return filemtime($this->getFullPath($path));
}

public function copy(string $from, string $to): bool
{
return copy($this->getFullPath($from), $this->getFullPath($to));
}

public function move(string $from, string $to): bool
{
return rename($this->getFullPath($from), $this->getFullPath($to));
}

public function files(string $directory): array
{
$fullPath = $this->getFullPath($directory);
$files = [];

foreach (scandir($fullPath) as $item) {
if ($item !== '.' && $item !== '..' && is_file($fullPath . '/' . $item)) {
$files[] = $item;
}
}

return $files;
}

public function directories(string $directory): array
{
$fullPath = $this->getFullPath($directory);
$dirs = [];

foreach (scandir($fullPath) as $item) {
if ($item !== '.' && $item !== '..' && is_dir($fullPath . '/' . $item)) {
$dirs[] = $item;
}
}

return $dirs;
}

public function url(string $path): string
{
return url('storage/' . $path);
}

protected function getFullPath(string $path): string
{
return $this->root . '/' . ltrim($path, '/\\');
}
}

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<?php

namespace App\Adapters\Storage;

use App\Contracts\Storage\StorageInterface;
use Aws\S3\S3Client;

class S3StorageAdapter implements StorageInterface
{
protected S3Client $client;
protected string $bucket;

public function __construct(array $config)
{
$this->client = new S3Client([
'region' => $config['region'],
'version' => 'latest',
'credentials' => [
'key' => $config['key'],
'secret' => $config['secret'],
],
]);

$this->bucket = $config['bucket'];
}

public function read(string $path): ?string
{
try {
$result = $this->client->getObject([
'Bucket' => $this->bucket,
'Key' => $path,
]);

return (string)$result['Body'];
} catch (\Exception $e) {
return null;
}
}

public function write(string $path, string $contents, array $options = []): bool
{
try {
$this->client->putObject([
'Bucket' => $this->bucket,
'Key' => $path,
'Body' => $contents,
'ContentType' => $options['content_type'] ?? null,
'ACL' => $options['acl'] ?? 'private',
]);

return true;
} catch (\Exception $e) {
return false;
}
}

public function delete(string $path): bool
{
try {
$this->client->deleteObject([
'Bucket' => $this->bucket,
'Key' => $path,
]);

return true;
} catch (\Exception $e) {
return false;
}
}

public function exists(string $path): bool
{
return $this->client->doesObjectExist($this->bucket, $path);
}

public function size(string $path): int
{
$result = $this->client->headObject([
'Bucket' => $this->bucket,
'Key' => $path,
]);

return $result['ContentLength'];
}

public function lastModified(string $path): int
{
$result = $this->client->headObject([
'Bucket' => $this->bucket,
'Key' => $path,
]);

return $result['LastModified']->getTimestamp();
}

public function copy(string $from, string $to): bool
{
try {
$this->client->copyObject([
'Bucket' => $this->bucket,
'Key' => $to,
'CopySource' => $this->bucket . '/' . $from,
]);

return true;
} catch (\Exception $e) {
return false;
}
}

public function move(string $from, string $to): bool
{
if ($this->copy($from, $to)) {
return $this->delete($from);
}

return false;
}

public function files(string $directory): array
{
$result = $this->client->listObjectsV2([
'Bucket' => $this->bucket,
'Prefix' => rtrim($directory, '/') . '/',
]);

return collect($result['Contents'] ?? [])
->map(fn($item) => basename($item['Key']))
->toArray();
}

public function directories(string $directory): array
{
$result = $this->client->listObjectsV2([
'Bucket' => $this->bucket,
'Prefix' => rtrim($directory, '/') . '/',
'Delimiter' => '/',
]);

return collect($result['CommonPrefixes'] ?? [])
->map(fn($item) => trim($item['Prefix'], '/'))
->toArray();
}

public function url(string $path): string
{
return $this->client->getObjectUrl($this->bucket, $path);
}
}

消息队列适配器

统一队列接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

namespace App\Contracts\Queue;

interface QueueInterface
{
public function push(string $queue, mixed $payload): bool;

public function pop(string $queue): ?mixed;

public function size(string $queue): int;

public function clear(string $queue): bool;
}

Redis 队列适配器

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
<?php

namespace App\Adapters\Queue;

use App\Contracts\Queue\QueueInterface;
use Redis;

class RedisQueueAdapter implements QueueInterface
{
protected Redis $redis;

public function __construct(Redis $redis)
{
$this->redis = $redis;
}

public function push(string $queue, mixed $payload): bool
{
return $this->redis->rPush($this->getQueueKey($queue), serialize($payload)) > 0;
}

public function pop(string $queue): ?mixed
{
$payload = $this->redis->lPop($this->getQueueKey($queue));

if ($payload === false) {
return null;
}

return unserialize($payload);
}

public function size(string $queue): int
{
return $this->redis->lLen($this->getQueueKey($queue));
}

public function clear(string $queue): bool
{
return $this->redis->del($this->getQueueKey($queue)) > 0;
}

protected function getQueueKey(string $queue): string
{
return "queue:{$queue}";
}
}

日志适配器

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

namespace App\Contracts\Logging;

interface LoggerInterface
{
public function emergency(string $message, array $context = []): void;

public function alert(string $message, array $context = []): void;

public function critical(string $message, array $context = []): void;

public function error(string $message, array $context = []): void;

public function warning(string $message, array $context = []): void;

public function notice(string $message, array $context = []): void;

public function info(string $message, array $context = []): void;

public function debug(string $message, array $context = []): void;

public function log(string $level, string $message, array $context = []): void;
}
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
<?php

namespace App\Adapters\Logging;

use App\Contracts\Logging\LoggerInterface;
use Monolog\Logger;

class MonologAdapter implements LoggerInterface
{
protected Logger $logger;

public function __construct(Logger $logger)
{
$this->logger = $logger;
}

public function emergency(string $message, array $context = []): void
{
$this->logger->emergency($message, $context);
}

public function alert(string $message, array $context = []): void
{
$this->logger->alert($message, $context);
}

public function critical(string $message, array $context = []): void
{
$this->logger->critical($message, $context);
}

public function error(string $message, array $context = []): void
{
$this->logger->error($message, $context);
}

public function warning(string $message, array $context = []): void
{
$this->logger->warning($message, $context);
}

public function notice(string $message, array $context = []): void
{
$this->logger->notice($message, $context);
}

public function info(string $message, array $context = []): void
{
$this->logger->info($message, $context);
}

public function debug(string $message, array $context = []): void
{
$this->logger->debug($message, $context);
}

public function log(string $level, string $message, array $context = []): void
{
$this->logger->log($level, $message, $context);
}
}

适配器工厂

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

namespace App\Factories;

use App\Contracts\Payments\PaymentGatewayInterface;
use App\Adapters\Payments\StripeAdapter;
use App\Adapters\Payments\PayPalAdapter;
use InvalidArgumentException;

class PaymentAdapterFactory
{
public static function create(string $gateway, array $config): PaymentGatewayInterface
{
return match ($gateway) {
'stripe' => new StripeAdapter($config),
'paypal' => new PayPalAdapter($config),
default => throw new InvalidArgumentException("Unsupported payment gateway: {$gateway}"),
};
}
}

测试适配器

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
<?php

namespace Tests\Unit\Adapters;

use Tests\TestCase;
use App\Adapters\Cache\RedisAdapter;
use App\Adapters\Cache\MemcachedAdapter;

class CacheAdapterTest extends TestCase
{
public function test_redis_adapter_operations(): void
{
$adapter = new RedisAdapter([
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', 6379),
]);

$adapter->set('test_key', 'test_value', 60);
$this->assertEquals('test_value', $adapter->get('test_key'));

$this->assertTrue($adapter->has('test_key'));

$adapter->delete('test_key');
$this->assertFalse($adapter->has('test_key'));
}

public function test_adapter_implements_interface(): void
{
$redisAdapter = new RedisAdapter(['host' => '127.0.0.1']);

$this->assertInstanceOf(
\App\Contracts\Cache\CacheInterface::class,
$redisAdapter
);
}
}

最佳实践

1. 接口优先设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

interface ExternalServiceInterface
{
public function execute(): mixed;
}

class ExternalServiceAdapter implements ExternalServiceInterface
{
protected ThirdPartyService $service;

public function execute(): mixed
{
return $this->service->run();
}
}

2. 适配器应透明

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

class PaymentService
{
public function __construct(
protected PaymentGatewayInterface $gateway
) {}

public function process(float $amount): PaymentResult
{
return $this->gateway->charge($amount);
}
}

3. 错误处理统一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

class SafeAdapter implements TargetInterface
{
public function request(): mixed
{
try {
return $this->adaptee->specificRequest();
} catch (\Exception $e) {
throw new AdapterException(
"Adapter operation failed: " . $e->getMessage(),
0,
$e
);
}
}
}

总结

Laravel 13 的适配器模式提供了一种优雅的方式来集成第三方库和外部服务。通过合理使用适配器模式,可以使代码更加灵活、可维护,并降低系统耦合度。