Laravel 13 仓储模式深度解析

仓储模式是领域驱动设计(DDD)中的核心模式之一,它将数据访问逻辑从业务逻辑中分离出来。本文将深入探讨 Laravel 13 中仓储模式的高级用法。

仓储模式基础

什么是仓储模式

仓储模式提供了一个抽象层,用于封装数据访问逻辑,使业务代码不直接依赖具体的数据存储实现。

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

namespace App\Contracts\Repositories;

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

public function all(): Collection;

public function create(array $data): Model;

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

public function delete(int $id): 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
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
<?php

namespace App\Repositories;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;

abstract class BaseRepository
{
protected Model $model;

public function __construct(Model $model)
{
$this->model = $model;
}

public function find(int $id): ?Model
{
return $this->model->find($id);
}

public function findOrFail(int $id): Model
{
return $this->model->findOrFail($id);
}

public function all(): Collection
{
return $this->model->all();
}

public function create(array $data): Model
{
return $this->model->create($data);
}

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

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

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

public function findBy(string $field, mixed $value): ?Model
{
return $this->model->where($field, $value)->first();
}

public function findAllBy(string $field, mixed $value): Collection
{
return $this->model->where($field, $value)->get();
}

protected function newQuery()
{
return $this->model->newQuery();
}
}

用户仓储实现

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

namespace App\Repositories\Eloquent;

use App\Models\User;
use App\Repositories\BaseRepository;
use App\Contracts\Repositories\UserRepositoryInterface;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;

class UserRepository extends BaseRepository implements UserRepositoryInterface
{
public function __construct(User $model)
{
parent::__construct($model);
}

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

public function findActive(): Collection
{
return $this->model->where('is_active', true)->get();
}

public function findWithRoles(int $id): ?User
{
return $this->model->with('roles')->find($id);
}

public function search(string $query, int $perPage = 15): LengthAwarePaginator
{
return $this->model
->where('name', 'like', "%{$query}%")
->orWhere('email', 'like', "%{$query}%")
->paginate($perPage);
}

public function attachRole(int $userId, int $roleId): void
{
$user = $this->find($userId);
$user?->roles()->attach($roleId);
}

public function syncRoles(int $userId, array $roleIds): void
{
$user = $this->find($userId);
$user?->roles()->sync($roleIds);
}
}

高级仓储模式

缓存仓储

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

namespace App\Repositories\Decorators;

use Illuminate\Contracts\Cache\Repository as CacheRepository;
use App\Contracts\Repositories\UserRepositoryInterface;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;

class CachedUserRepository implements UserRepositoryInterface
{
protected UserRepositoryInterface $repository;
protected CacheRepository $cache;
protected int $ttl = 3600;

public function __construct(
UserRepositoryInterface $repository,
CacheRepository $cache
) {
$this->repository = $repository;
$this->cache = $cache;
}

public function find(int $id): ?User
{
return $this->cache->remember("user.{$id}", $this->ttl, function () use ($id) {
return $this->repository->find($id);
});
}

public function findByEmail(string $email): ?User
{
return $this->cache->remember("user.email.{$email}", $this->ttl, function () use ($email) {
return $this->repository->findByEmail($email);
});
}

public function create(array $data): User
{
$user = $this->repository->create($data);
$this->cache->put("user.{$user->id}", $user, $this->ttl);
$this->cache->forget('users.all');
return $user;
}

public function update(int $id, array $data): bool
{
$result = $this->repository->update($id, $data);
$this->cache->forget("user.{$id}");
return $result;
}

public function delete(int $id): bool
{
$result = $this->repository->delete($id);
$this->cache->forget("user.{$id}");
$this->cache->forget('users.all');
return $result;
}

public function all(): Collection
{
return $this->cache->remember('users.all', $this->ttl, function () {
return $this->repository->all();
});
}

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

规格模式结合

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

namespace App\Specifications;

interface Specification
{
public function isSatisfiedBy($candidate): bool;

public function toQuery($query);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Specifications\User;

use App\Specifications\Specification;

class ActiveUserSpecification implements Specification
{
public function isSatisfiedBy($user): bool
{
return $user->is_active === true;
}

public function toQuery($query)
{
return $query->where('is_active', true);
}
}
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
<?php

namespace App\Specifications\User;

use App\Specifications\Specification;

class HasRoleSpecification implements Specification
{
protected string $role;

public function __construct(string $role)
{
$this->role = $role;
}

public function isSatisfiedBy($user): bool
{
return $user->roles->contains('name', $this->role);
}

public function toQuery($query)
{
return $query->whereHas('roles', function ($q) {
$q->where('name', $this->role);
});
}
}
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\Repositories\Eloquent;

use App\Specifications\Specification;

trait SpecificationTrait
{
public function match(Specification $specification): Collection
{
return $specification->toQuery($this->newQuery())->get();
}

public function matchSingle(Specification $specification): ?Model
{
return $specification->toQuery($this->newQuery())->first();
}

public function matchPaginated(
Specification $specification,
int $perPage = 15
): LengthAwarePaginator {
return $specification->toQuery($this->newQuery())->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
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
<?php

namespace App\Repositories\Eloquent;

use App\Models\Product;
use App\Repositories\BaseRepository;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;

class ProductRepository extends BaseRepository
{
protected array $allowedFilters = [
'category',
'brand',
'price_min',
'price_max',
'in_stock',
'sort',
];

public function __construct(Product $model)
{
parent::__construct($model);
}

public function filter(array $filters, int $perPage = 15): LengthAwarePaginator
{
$query = $this->newQuery()->with(['category', 'brand']);

if (isset($filters['category'])) {
$query->where('category_id', $filters['category']);
}

if (isset($filters['brand'])) {
$query->where('brand_id', $filters['brand']);
}

if (isset($filters['price_min'])) {
$query->where('price', '>=', $filters['price_min']);
}

if (isset($filters['price_max'])) {
$query->where('price', '<=', $filters['price_max']);
}

if (isset($filters['in_stock']) && $filters['in_stock']) {
$query->where('stock', '>', 0);
}

if (isset($filters['sort'])) {
$query = $this->applySort($query, $filters['sort']);
}

return $query->paginate($perPage);
}

protected function applySort($query, string $sort)
{
$sortMap = [
'price_asc' => ['price', 'asc'],
'price_desc' => ['price', 'desc'],
'name_asc' => ['name', 'asc'],
'name_desc' => ['name', 'desc'],
'newest' => ['created_at', 'desc'],
];

if (isset($sortMap[$sort])) {
[$column, $direction] = $sortMap[$sort];
return $query->orderBy($column, $direction);
}

return $query->orderBy('created_at', 'desc');
}

public function findFeatured(): Collection
{
return $this->model
->where('is_featured', true)
->where('is_active', true)
->orderBy('featured_at', 'desc')
->get();
}

public function findRelated(int $productId, int $limit = 4): Collection
{
$product = $this->find($productId);

if (!$product) {
return collect();
}

return $this->model
->where('category_id', $product->category_id)
->where('id', '!=', $productId)
->limit($limit)
->get();
}

public function search(string $query, int $perPage = 15): LengthAwarePaginator
{
return $this->model
->where(function ($q) use ($query) {
$q->where('name', 'like', "%{$query}%")
->orWhere('description', 'like', "%{$query}%")
->orWhere('sku', 'like', "%{$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
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
<?php

namespace App\Repositories\Eloquent;

use App\Models\Order;
use App\Repositories\BaseRepository;
use App\Contracts\Repositories\OrderRepositoryInterface;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;

class OrderRepository extends BaseRepository implements OrderRepositoryInterface
{
public function __construct(Order $model)
{
parent::__construct($model);
}

public function findByOrderNumber(string $orderNumber): ?Order
{
return $this->model->where('order_number', $orderNumber)->first();
}

public function findUserOrders(int $userId): Collection
{
return $this->model
->where('user_id', $userId)
->with(['items', 'status'])
->orderBy('created_at', 'desc')
->get();
}

public function findUserOrdersPaginated(int $userId, int $perPage = 15): LengthAwarePaginator
{
return $this->model
->where('user_id', $userId)
->with(['items.product', 'status'])
->orderBy('created_at', 'desc')
->paginate($perPage);
}

public function findByStatus(string $status): Collection
{
return $this->model
->where('status', $status)
->orderBy('created_at', 'desc')
->get();
}

public function updateStatus(int $id, string $status): bool
{
return $this->model->where('id', $id)->update([
'status' => $status,
'status_updated_at' => now(),
]) > 0;
}

public function getRecentOrders(int $limit = 10): Collection
{
return $this->model
->with(['user', 'items'])
->orderBy('created_at', 'desc')
->limit($limit)
->get();
}

public function getDailyRevenue(string $date): float
{
return $this->model
->whereDate('created_at', $date)
->where('status', 'completed')
->sum('total');
}

public function getMonthlyRevenue(int $year, int $month): array
{
return $this->model
->whereYear('created_at', $year)
->whereMonth('created_at', $month)
->where('status', 'completed')
->selectRaw('DATE(created_at) as date, SUM(total) as revenue')
->groupBy('date')
->pluck('revenue', 'date')
->toArray();
}

public function findPendingPayment(): Collection
{
return $this->model
->where('status', 'pending_payment')
->where('created_at', '<', now()->subMinutes(30))
->get();
}
}

服务层集成

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;

use App\Contracts\Repositories\UserRepositoryInterface;
use App\Contracts\Repositories\OrderRepositoryInterface;
use App\Events\UserRegistered;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Collection;

class UserService
{
public function __construct(
protected UserRepositoryInterface $users,
protected OrderRepositoryInterface $orders
) {}

public function register(array $data): User
{
$data['password'] = Hash::make($data['password']);

$user = $this->users->create($data);

event(new UserRegistered($user));

return $user;
}

public function updateProfile(int $userId, array $data): User
{
if (isset($data['password'])) {
$data['password'] = Hash::make($data['password']);
}

$this->users->update($userId, $data);

return $this->users->find($userId);
}

public function getUserWithOrders(int $userId): array
{
$user = $this->users->findWithRoles($userId);
$orders = $this->orders->findUserOrders($userId);

return [
'user' => $user,
'orders' => $orders,
'total_orders' => $orders->count(),
'total_spent' => $orders->sum('total'),
];
}

public function deactivateUser(int $userId): bool
{
return $this->users->update($userId, [
'is_active' => false,
'deactivated_at' => now(),
]);
}

public function searchUsers(string $query, int $perPage = 15): LengthAwarePaginator
{
return $this->users->search($query, $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
<?php

namespace App\Repositories\Factory;

use App\Contracts\Repositories\RepositoryInterface;
use InvalidArgumentException;

class RepositoryFactory
{
protected array $repositories = [];

public function register(string $name, string $repositoryClass): void
{
$this->repositories[$name] = $repositoryClass;
}

public function create(string $name): RepositoryInterface
{
if (!isset($this->repositories[$name])) {
throw new InvalidArgumentException("Repository [{$name}] not registered.");
}

return app($this->repositories[$name]);
}

public function getRegisteredRepositories(): array
{
return array_keys($this->repositories);
}
}
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\Providers;

use Illuminate\Support\ServiceProvider;
use App\Repositories\Factory\RepositoryFactory;
use App\Contracts\Repositories\UserRepositoryInterface;
use App\Contracts\Repositories\OrderRepositoryInterface;
use App\Repositories\Eloquent\UserRepository;
use App\Repositories\Eloquent\OrderRepository;

class RepositoryServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(UserRepositoryInterface::class, UserRepository::class);
$this->app->singleton(OrderRepositoryInterface::class, OrderRepository::class);

$this->app->singleton(RepositoryFactory::class, function ($app) {
$factory = new RepositoryFactory();
$factory->register('user', UserRepositoryInterface::class);
$factory->register('order', OrderRepositoryInterface::class);
return $factory;
});
}
}

测试仓储

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

namespace Tests\Unit\Repositories;

use Tests\TestCase;
use App\Models\User;
use App\Repositories\Eloquent\UserRepository;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserRepositoryTest extends TestCase
{
use RefreshDatabase;

protected UserRepository $repository;

protected function setUp(): void
{
parent::setUp();
$this->repository = new UserRepository(new User());
}

public function test_can_create_user(): void
{
$data = [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => bcrypt('password'),
];

$user = $this->repository->create($data);

$this->assertInstanceOf(User::class, $user);
$this->assertEquals('John Doe', $user->name);
$this->assertEquals('john@example.com', $user->email);
}

public function test_can_find_user_by_email(): void
{
User::factory()->create(['email' => 'test@example.com']);

$user = $this->repository->findByEmail('test@example.com');

$this->assertNotNull($user);
$this->assertEquals('test@example.com', $user->email);
}

public function test_can_update_user(): void
{
$user = User::factory()->create();

$result = $this->repository->update($user->id, [
'name' => 'Updated Name',
]);

$this->assertTrue($result);
$this->assertEquals('Updated Name', $user->fresh()->name);
}

public function test_can_delete_user(): void
{
$user = User::factory()->create();

$result = $this->repository->delete($user->id);

$this->assertTrue($result);
$this->assertDatabaseMissing('users', ['id' => $user->id]);
}
}

最佳实践

1. 接口优先

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

namespace App\Contracts\Repositories;

interface ProductRepositoryInterface
{
public function find(int $id): ?Product;

public function findBySku(string $sku): ?Product;

public function findActive(): Collection;

public function filter(array $criteria): LengthAwarePaginator;
}

2. 单一职责

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

namespace App\Repositories\Eloquent;

class ProductReadRepository
{
public function find(int $id): ?Product { }
public function findBySku(string $sku): ?Product { }
public function filter(array $criteria): LengthAwarePaginator { }
}

class ProductWriteRepository
{
public function create(array $data): Product { }
public function update(int $id, array $data): bool { }
public function delete(int $id): bool { }
}

3. 预加载优化

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

namespace App\Repositories\Eloquent;

class OrderRepository extends BaseRepository
{
protected array $defaultRelations = ['user', 'items.product', 'status'];

public function findWithDetails(int $id): ?Order
{
return $this->model
->with($this->defaultRelations)
->find($id);
}

public function paginateWithRelations(int $perPage = 15): LengthAwarePaginator
{
return $this->model
->with($this->defaultRelations)
->paginate($perPage);
}
}

总结

Laravel 13 的仓储模式提供了一种优雅的方式来组织数据访问逻辑。通过合理使用仓储模式,可以创建可维护、可测试、可扩展的应用程序架构,同时保持业务逻辑与数据访问的清晰分离。