Laravel 13 合同模式深度解析

合同模式是 Laravel 框架的核心设计理念之一,通过定义清晰的接口契约来解耦系统组件。本文将深入探讨 Laravel 13 中合同模式的高级用法。

合同模式基础

什么是合同模式

合同模式通过定义接口来规定组件之间的交互契约,确保代码的解耦和可测试性。

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

namespace App\Contracts;

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

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

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

namespace App\Contracts;

interface NotificationService
{
public function send(string $to, string $subject, string $body): bool;

public function sendTemplate(string $to, string $template, array $data = []): bool;

public function queue(string $to, string $subject, string $body): void;
}

Laravel 内置合同

核心合同列表

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

namespace Illuminate\Contracts;

interface Queue
{
public function push($job, $data = '', $queue = null);

public function pushRaw($payload, $queue = null, array $options = []);

public function later($delay, $job, $data = '', $queue = null);

public function pop($queue = null);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace Illuminate\Contracts\Cache;

interface Repository
{
public function get($key, $default = null);

public function put($key, $value, $ttl = null);

public function has($key): bool;

public function forget($key): bool;

public function flush(): bool;
}

使用内置合同

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

namespace App\Services;

use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Illuminate\Contracts\Queue\Queue as QueueContract;

class DataProcessor
{
public function __construct(
protected CacheRepository $cache,
protected QueueContract $queue
) {}

public function process(string $key): mixed
{
return $this->cache->remember($key, 3600, function () {
return $this->fetchData();
});
}

public function dispatchJob(string $job): void
{
$this->queue->push($job);
}
}

自定义合同设计

仓储合同

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

namespace App\Contracts\Repositories;

interface UserRepository
{
public function find(int $id): ?User;

public function findByEmail(string $email): ?User;

public function create(array $data): User;

public function update(int $id, array $data): bool;

public function delete(int $id): bool;

public function paginate(int $perPage = 15): LengthAwarePaginator;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Contracts\Repositories;

interface OrderRepository
{
public function find(int $id): ?Order;

public function findByOrderNumber(string $orderNumber): ?Order;

public function findUserOrders(int $userId): Collection;

public function create(array $data): Order;

public function updateStatus(int $id, string $status): bool;

public function getRecentOrders(int $limit = 10): Collection;
}

服务合同

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

namespace App\Contracts\Services;

interface EmailService
{
public function send(string $to, string $subject, string $body): bool;

public function sendWithAttachment(
string $to,
string $subject,
string $body,
array $attachments
): bool;

public function queue(string $to, string $subject, string $body): void;

public function sendTemplate(string $to, string $template, array $data): bool;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Contracts\Services;

interface FileStorage
{
public function put(string $path, $contents, array $options = []): bool;

public function get(string $path): ?string;

public function exists(string $path): bool;

public function delete(string $path): bool;

public function url(string $path): string;

public function temporaryUrl(string $path, DateTimeInterface $expiration): 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
<?php

namespace App\Repositories\Eloquent;

use App\Models\User;
use App\Contracts\Repositories\UserRepository as UserRepositoryContract;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;

class UserRepository implements UserRepositoryContract
{
public function find(int $id): ?User
{
return User::find($id);
}

public function findByEmail(string $email): ?User
{
return User::where('email', $email)->first();
}

public function create(array $data): User
{
return User::create($data);
}

public function update(int $id, array $data): bool
{
return User::where('id', $id)->update($data) > 0;
}

public function delete(int $id): bool
{
return User::destroy($id) > 0;
}

public function paginate(int $perPage = 15): LengthAwarePaginator
{
return User::query()->paginate($perPage);
}
}

服务实现

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

namespace App\Services;

use App\Contracts\Services\EmailService;
use Illuminate\Support\Facades\Mail;
use Illuminate\Mail\Mailable;

class SmtpEmailService implements EmailService
{
public function send(string $to, string $subject, string $body): bool
{
try {
Mail::raw($body, function ($message) use ($to, $subject) {
$message->to($to)->subject($subject);
});
return true;
} catch (\Exception $e) {
return false;
}
}

public function sendWithAttachment(
string $to,
string $subject,
string $body,
array $attachments
): bool {
try {
Mail::raw($body, function ($message) use ($to, $subject, $attachments) {
$message->to($to)->subject($subject);
foreach ($attachments as $attachment) {
$message->attach($attachment);
}
});
return true;
} catch (\Exception $e) {
return false;
}
}

public function queue(string $to, string $subject, string $body): void
{
Mail::to($to)->queue(new GenericMailable($subject, $body));
}

public function sendTemplate(string $to, string $template, array $data): bool
{
try {
Mail::send($template, $data, function ($message) use ($to) {
$message->to($to);
});
return true;
} catch (\Exception $e) {
return false;
}
}
}

服务容器绑定

绑定接口到实现

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

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Contracts\Repositories\UserRepository;
use App\Repositories\Eloquent\UserRepository as EloquentUserRepository;
use App\Contracts\Services\EmailService;
use App\Services\SmtpEmailService;
use App\Contracts\Services\FileStorage;
use App\Services\S3FileStorage;

class RepositoryServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(UserRepository::class, EloquentUserRepository::class);

$this->app->bind(EmailService::class, function ($app) {
return match(config('mail.driver')) {
'smtp' => new SmtpEmailService(),
'sendgrid' => new SendgridEmailService(),
'mailgun' => new MailgunEmailService(),
default => new SmtpEmailService(),
};
});

$this->app->singleton(FileStorage::class, function ($app) {
return match(config('filesystems.default')) {
's3' => new S3FileStorage(),
'local' => new LocalFileStorage(),
'ftp' => new FtpFileStorage(),
default => new LocalFileStorage(),
};
});
}
}

上下文绑定

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\Providers;

use Illuminate\Support\ServiceProvider;
use App\Contracts\LoggerInterface;
use App\Loggers\FileLogger;
use App\Loggers\DatabaseLogger;
use App\Services\PaymentService;
use App\Services\AuditService;

class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->when(PaymentService::class)
->needs(LoggerInterface::class)
->give(FileLogger::class);

$this->app->when(AuditService::class)
->needs(LoggerInterface::class)
->give(DatabaseLogger::class);
}
}

高级合同模式

可扩展合同

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

namespace App\Contracts;

interface Exportable
{
public function toArray(): array;

public function toJson(int $options = 0): string;

public function toCsv(): string;

public function toXml(): string;
}
1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace App\Contracts;

interface Importable
{
public static function fromArray(array $data): self;

public static function fromJson(string $json): self;

public static function fromCsv(string $csv): self;
}

事件感知合同

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

namespace App\Contracts;

interface EventDispatcher
{
public function dispatch(object $event): void;

public function listen(string $event, $listener): void;

public function subscribe(object $subscriber): void;

public function forget(string $event): void;
}
1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace App\Contracts;

interface EventAware
{
public function setEventDispatcher(EventDispatcher $dispatcher): void;

public function getEventDispatcher(): EventDispatcher;

public function dispatchEvent(object $event): void;
}

缓存感知合同

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

namespace App\Contracts;

use DateTimeInterface;

interface Cacheable
{
public function getCacheKey(): string;

public function getCacheTtl(): DateTimeInterface|int|null;

public function getCacheTags(): array;

public function shouldCache(): bool;
}
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\Models;

use App\Contracts\Cacheable;
use Illuminate\Database\Eloquent\Model;

class Product extends Model implements Cacheable
{
public function getCacheKey(): string
{
return "product:{$this->id}";
}

public function getCacheTtl(): int
{
return 3600;
}

public function getCacheTags(): array
{
return ['products', "product:{$this->id}"];
}

public function shouldCache(): bool
{
return $this->is_active;
}
}

策略合同

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

namespace App\Contracts\Strategies;

interface PricingStrategy
{
public function calculate(float $basePrice, array $context = []): float;

public function getName(): string;

public function getDescription(): string;
}
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\Strategies\Pricing;

use App\Contracts\Strategies\PricingStrategy;

class StandardPricing implements PricingStrategy
{
public function calculate(float $basePrice, array $context = []): float
{
return $basePrice;
}

public function getName(): string
{
return 'standard';
}

public function getDescription(): string
{
return 'Standard pricing without any discounts';
}
}
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\Strategies\Pricing;

use App\Contracts\Strategies\PricingStrategy;

class DiscountPricing implements PricingStrategy
{
protected float $discountRate;

public function __construct(float $discountRate = 0.1)
{
$this->discountRate = $discountRate;
}

public function calculate(float $basePrice, array $context = []): float
{
return $basePrice * (1 - $this->discountRate);
}

public function getName(): string
{
return 'discount';
}

public function getDescription(): string
{
return sprintf('Discount pricing with %.0f%% off', $this->discountRate * 100);
}
}
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
<?php

namespace App\Services;

use App\Contracts\Strategies\PricingStrategy;

class PricingService
{
protected array $strategies = [];

public function registerStrategy(string $name, PricingStrategy $strategy): void
{
$this->strategies[$name] = $strategy;
}

public function calculate(string $strategyName, float $basePrice, array $context = []): float
{
$strategy = $this->strategies[$strategyName] ?? throw new \InvalidArgumentException(
"Unknown pricing strategy: {$strategyName}"
);

return $strategy->calculate($basePrice, $context);
}

public function getAvailableStrategies(): array
{
return collect($this->strategies)->map(fn($s) => [
'name' => $s->getName(),
'description' => $s->getDescription(),
])->values()->toArray();
}
}

合同测试

合同测试基类

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

namespace Tests\Contracts;

use PHPUnit\Framework\TestCase;

abstract class ContractTest extends TestCase
{
abstract protected function createInstance();

abstract protected function getContractInterface(): string;

protected function assertImplementsContract($instance): void
{
$interface = $this->getContractInterface();

$this->assertInstanceOf(
$interface,
$instance,
"Instance must implement {$interface}"
);

$reflection = new \ReflectionClass($interface);
foreach ($reflection->getMethods() as $method) {
$this->assertTrue(
method_exists($instance, $method->getName()),
"Instance must implement method: {$method->getName()}"
);
}
}
}

具体合同测试

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

namespace Tests\Contracts\Repositories;

use Tests\Contracts\ContractTest;
use App\Contracts\Repositories\UserRepository;
use App\Repositories\Eloquent\UserRepository as EloquentUserRepository;

class UserRepositoryContractTest extends ContractTest
{
protected function createInstance()
{
return new EloquentUserRepository();
}

protected function getContractInterface(): string
{
return UserRepository::class;
}

public function test_implements_contract(): void
{
$instance = $this->createInstance();
$this->assertImplementsContract($instance);
}

public function test_find_returns_user_or_null(): void
{
$repository = $this->createInstance();

$result = $repository->find(999999);
$this->assertNull($result);

$user = $repository->create([
'name' => 'Test User',
'email' => 'test@example.com',
'password' => bcrypt('password'),
]);

$found = $repository->find($user->id);
$this->assertNotNull($found);
$this->assertEquals($user->id, $found->id);
}
}

最佳实践

1. 接口隔离

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

namespace App\Contracts;

interface ReadableRepository
{
public function find(int $id): ?Model;

public function all(): Collection;

public function paginate(int $perPage = 15): LengthAwarePaginator;
}

interface WritableRepository
{
public function create(array $data): Model;

public function update(int $id, array $data): bool;

public function delete(int $id): bool;
}

interface Repository extends ReadableRepository, WritableRepository
{
}

2. 依赖倒置

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

namespace App\Services;

use App\Contracts\Repositories\UserRepository;

class UserService
{
public function __construct(
protected UserRepository $users
) {}

public function register(array $data): User
{
return $this->users->create($data);
}
}

3. 显式依赖

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

namespace App\Http\Controllers;

use App\Contracts\Services\PaymentService;
use App\Contracts\Services\NotificationService;
use Illuminate\Http\Request;

class OrderController extends Controller
{
public function __construct(
protected PaymentService $payments,
protected NotificationService $notifications
) {}

public function store(Request $request)
{
$result = $this->payments->charge(
$request->input('amount'),
$request->input('options', [])
);

if ($result->success) {
$this->notifications->send(
$request->user()->email,
'Payment Successful',
'Your payment has been processed.'
);
}

return response()->json($result);
}
}

总结

Laravel 13 的合同模式提供了一种强大的方式来设计解耦、可测试的应用程序。通过定义清晰的接口契约,可以轻松切换实现、提高代码可维护性,并遵循 SOLID 原则。