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 原则。