Xây Dựng API Quản Lý Hàng Tồn Kho Thân Thiện Với DDD Trong Laravel 12
Xây dựng một API trong Laravel giữ vững nguyên tắc thiết kế hướng miền (DDD) và kiến trúc sạch có thể cảm thấy như một nhiệm vụ khó khăn. Giữa việc giữ cho miền sạch sẽ, cung cấp một lớp HTTP thân thiện, và kết nối với các vấn đề hạ tầng, việc triển khai thường có thể đi lệch hướng rất nhanh. Hướng dẫn này sẽ ghi lại cách chúng tôi đã phát triển tính năng Mặt Hàng Tồn Kho (đăng ký + truy xuất) trên Laravel 12/PHP 8.4 với:
- Kiến trúc bốn lớp (Miền → Ứng dụng → Hạ tầng → Giao diện/HTTP)
- Các điểm cuối được bảo vệ bởi Sanctum và các chính sách
- Viết idempotency và giới hạn tần suất
- Các kho lưu trữ thân thiện với eager-loading (không sử dụng
Model::all()) - Kiểm thử đơn vị + tính năng với Pest.
Dưới đây là hành trình, cấu trúc thư mục, và lý do đứng sau mỗi lớp để bạn có thể tái sử dụng (hoặc remix) cách tiếp cận này trong các dự án của riêng bạn.
Mục Lục
- Miền Chính: Thực Thể, Đối Tượng Giá Trị, Chính Sách
- Lớp Ứng Dụng: Điều Phối & DTO
- Lớp Hạ Tầng: Các Bộ Kết Nối Dựa Trên Laravel
- Giao Diện / HTTP: Yêu Cầu, Bộ Điều Khiển, Chính Sách, Lộ Trình
- Xác Thực, Chính Sách & Giới Hạn Tần Suất
- Sơ Đồ Cơ Sở Dữ Liệu & Seeding
- Kiểm Thử: Pest cho Đầy Đủ Kiểm Thử Đơn Vị + Tính Năng
- Bài Học Được Rút Ra & Các Bước Tiếp Theo
- Kết Luận
1. Miền Chính: Thực Thể, Đối Tượng Giá Trị, Chính Sách
Mục tiêu: Biểu đạt ngôn ngữ phổ biến và các quy tắc mà không vô tình đưa Laravel vào.
Các phần chính nằm trong app/Domain/Inventory/*:
- Thực thể gốc
InventoryItemđảm bảo các bất biến (tên không được trống, tồn kho không âm, đã ngừng sản xuất ⇒ số lượng bằng không) bên trong các phương thức factory. - Các đối tượng giá trị (
Sku,Price) đóng gói định dạng, chuẩn hóa và so sánh để phần còn lại của mã nguồn không phải tiếp xúc với chuỗi thô. - Enum
InventoryItemStatusthay thế kiểu chuỗi cho trạng thái và giữ cho việc lọc an toàn. - Giao diện kho lưu trữ (
InventoryItemRepositoryInterface) trả về các thực thể miền hoặcPaginatedInventoryItems(một DTO phân trang nhỏ) để mã ứng dụng không bị rò rỉEloquent. - Các ngoại lệ miền (
InventoryItemNotFound,DuplicateSkuException,IdempotencyConflictException) tiêu chuẩn hóa các ngữ nghĩa thất bại.
Bằng cách giữ cho cây miền không phụ thuộc vào framework, các bài kiểm tra/đặc điểm gần như đọc như các quy tắc kinh doanh và vẫn nhanh chóng.
2. Lớp Ứng Dụng: Điều Phối & DTO
Mục tiêu: Điều phối các trường hợp sử dụng với kiến thức tối thiểu về giao hàng hoặc lưu trữ.
Trong app/Application/Inventory, chúng tôi đã thêm:
CreateInventoryItemCommand(DTO đầu vào bất biến) – trường hợp sử dụng chấp nhận một đối tượng lệnh đã được tạo hoàn chỉnh.RegisterInventoryItemResult+InventoryItemData– trường hợp sử dụng trả về một DTO có thể tuần tự hóa, không phải là thực thể, ngăn ngừa các sự cố tải lười sau này.- Trường hợp sử dụng
RegisterInventoryItem– điều phối xác thực, các cuộc gọi kho lưu trữ, tìm kiếm idempotency, và lưu trữ các mặt hàng mới trong một giao dịch. - Các hợp đồng hỗ trợ (
ClockInterface,TransactionManagerInterface,IdempotencyServiceInterface) và DTOProcessedIdempotencyResultđảm bảo chúng tôi có thể thay thế các triển khai hạ tầng.
Lưu ý rằng trường hợp sử dụng vẫn giữ nguyên trung lập với framework nhưng tận dụng các hợp đồng mà nó cần:
php
public function __invoke(CreateInventoryItemCommand $command): RegisterInventoryItemResult
{
return $this->transactionManager->run(function () use ($command) {
if ($stored = $this->idempotencyService->retrieve($command->userId, $command->idempotencyKey)) {
if ($stored->requestHash !== $command->payloadHash) {
throw IdempotencyConflictException::fromKey($command->idempotencyKey);
}
return new RegisterInventoryItemResult(
InventoryItemData::fromArray($stored->responseBody),
true,
$stored->statusCode,
);
}
// Ngăn chặn các SKU trùng lặp trong bảo vệ giao dịch
if ($this->repository->findBySku(Sku::fromString($command->sku))) {
throw DuplicateSkuException::withSku($command->sku);
}
$item = InventoryItem::create(
$command->id,
Sku::fromString($command->sku),
$command->name,
$command->description,
$command->quantity,
$command->reorderLevel,
Price::fromDecimal($command->price, $command->currency),
InventoryItemStatus::from($command->status),
$this->clock->now(),
);
$this->repository->save($item);
$result = InventoryItemData::fromEntity($item);
$this->idempotencyService->store(
$command->userId,
$command->idempotencyKey,
$command->payloadHash,
201,
$result->toArray(),
);
return new RegisterInventoryItemResult($result, false, 201);
});
}
3. Lớp Hạ Tầng: Các Bộ Kết Nối Dựa Trên Laravel
Mục tiêu: Cung cấp các triển khai cụ thể cho việc lưu trữ và các hợp đồng hệ thống mà không làm rò rỉ Eloquent lên trên.
Các điểm nổi bật:
InventoryItemModellà một mô hình Eloquent thông thường + factory, nhưng chúng tôi ghi đènewFactory()để kết nối factory tùy chỉnh và giữ cho tên gọi nhất quán.InventoryItemRepositorychuyển đổiInventoryItemModel↔ các đối tượng miền, xử lý phân trang với bộ lọc và sắp xếp do người dùng kiểm soát, và cập nhật/tạo bản ghi mà không gọi->all().DatabaseTransactionManager,SystemClock,DatabaseIdempotencyServicethực hiện các hợp đồng được sử dụng bởi lớp ứng dụng.- Các ràng buộc Container trong
AppServiceProviderkết nối mọi thứ với nhau.
php
public function register(): void
{
$this->app->bind(InventoryItemRepositoryInterface::class, InventoryItemRepository::class);
$this->app->bind(TransactionManagerInterface::class, DatabaseTransactionManager::class);
$this->app->bind(ClockInterface::class, SystemClock::class);
$this->app->bind(IdempotencyServiceInterface::class, DatabaseIdempotencyService::class);
}
và việc triển khai idempotency sử dụng bảng chuyên dụng (idempotency_keys) để phát lại các yêu cầu giống hệt ngay lập tức.
4. Giao Diện / HTTP: Yêu Cầu, Bộ Điều Khiển, Chính Sách, Lộ Trình
Mục tiêu: Giữ cho các bộ điều khiển mỏng, xác thực sớm, ánh xạ các ngoại lệ thành mã HTTP, và bảo vệ bề mặt API.
Các phần chính trong app/Interfaces/Http:
StoreInventoryItemRequestxử lý xác thực, tính toán một hàm băm payload SHA-256, và đảm bảo tiêu đềIdempotency-Keycó mặt. Nó cũng ủy quyền quaInventoryItemPolicy.FilterInventoryItemsRequestphân tích các tham số lọc/tìm kiếm/sắp xếp/phân trang thành một mảng giống DTO duy nhất.InventoryItemControllerđiều chỉnh các yêu cầu HTTP thành các lệnh/DTO của ứng dụng và trả về các phản hồiInventoryItemResource.- Middleware
TransformDomainExceptionsbắt các ngoại lệ miền đã biết và chuyển đổi chúng thành các phản hồi JSON với mã HTTP (422/404/409). InventoryItemPolicykiểm soát khả năngview,viewAny,createbằng cách sử dụng các khả năng token của Sanctum (inventory:read,inventory:write).routes/api.phpđăng ký tài nguyêninventory-itemsvới xác thực Sanctum, giới hạn tần suất và middleware.
Chúng tôi cũng thêm việc hiển thị các ngoại lệ ở cấp độ framework (trong bootstrap/app.php) để ánh xạ các ngoại lệ miền thành phản hồi JSON toàn cục – điều này giữ cho các bài kiểm thử tính năng biểu cảm mà không cần lặp lại các khối try/catch.
php
$exceptions->render(function (DuplicateSkuException|IdempotencyConflictException $exception, Request $request) {
if ($request->expectsJson()) {
return new JsonResponse(['message' => $exception->getMessage()], 409);
}
return null;
});
5. Xác Thực, Chính Sách & Giới Hạn Tần Suất
- Sanctum là bắt buộc cho mọi lộ trình HTTP (
Route::middleware('auth:sanctum', ...)) và mô hìnhUserbây giờ lại sử dụngHasApiTokens. AppServiceProvider::boot()đăng ký một bộ giới hạn tần suất (inventory-items) và ánh xạInventoryItemModel→InventoryItemPolicy.- Mỗi bài kiểm thử sử dụng
Sanctum::actingAs()với các khả năng phù hợp.
Điều này có nghĩa là ngay cả trong các bài kiểm thử tính năng, chính sách được thực thi và chúng tôi nhận được một phản hồi 403 tự nhiên nếu token thiếu inventory:write.
6. Sơ Đồ Cơ Sở Dữ Liệu & Seeding
Hai migration cung cấp hạ tầng:
- Bảng
inventory_items– khóa chính UUID, chỉ mục SKU + trạng thái, cột tiền tệ + số tiền, dấu thời gian. - Bảng
idempotency_keys– lưu trữ(user, key, request_hash, response)để phát lại hoặc phát hiện các yêu cầu xung đột.
Factory + seeder (InventoryItemSeeder) cho phép nhanh chóng tạo dữ liệu:
php
final class InventoryItemSeeder extends Seeder
{
public function run(): void
{
InventoryItemModel::factory()->count(20)->create();
}
}
DatabaseSeeder gọi seeder này sau khi tạo một người dùng demo.
7. Kiểm Thử: Pest cho Đầy Đủ Kiểm Thử Đơn Vị + Tính Năng
Chúng tôi dựa vào Pest để giữ cho các bài kiểm thử ngắn gọn:
- Các bài kiểm thử Đơn vị tập trung vào các đối tượng giá trị (
Sku,Price) và trường hợp sử dụngRegisterInventoryItem(bao gồm phát lại idempotency và phát hiện SKU trùng lặp). - Các bài kiểm thử Tính năng (
tests/Feature/Http/Inventory/InventoryItemApiTest.php) chạy trên một cơ sở dữ liệu được làm mới, xác thực qua Sanctum, và xác nhận:- việc đăng ký thành công trả về 201 với payload DTO và tiêu đề
Idempotent-Replay: false, - phát lại với cùng một khóa idempotency trả về 200 và
Idempotent-Replay: true, - các lỗi xác thực tạo ra 422,
- các SKU trùng lặp sản xuất 409 (sau khi thêm trình xử lý ngoại lệ của chúng tôi),
- việc truy xuất một ID không tồn tại kích hoạt 404 nhờ vào ánh xạ ngoại lệ miền.
- việc đăng ký thành công trả về 201 với payload DTO và tiêu đề
Mỗi bài kiểm thử tính năng đặt lại DB thông qua RefreshDatabase, vì vậy sơ đồ được di trú và sạch sẽ mỗi lần.
8. Bài Học Được Rút Ra & Các Bước Tiếp Theo
- Ranh giới miền quan trọng – một khi các thực thể và đối tượng giá trị trở nên thuần khiết, việc kiểm thử và tái cấu trúc trở nên dễ dàng.
- Hợp đồng > Facades trong lớp ứng dụng – dựa vào các giao diện giúp cho trường hợp sử dụng
RegisterInventoryItemcó thể kiểm thử mà không cần giả lập Laravel. - Middleware + hiển thị ngoại lệ toàn cầu giữ cho các bộ điều khiển mỏng và đẩy ngữ nghĩa lỗi ra ngoài.
- Idempotency xứng đáng với bảng bổ sung – các khách hàng (di động/web) có thể an toàn phát lại các POST mà không làm tràn ngập các bản sao.
- Chính sách + khả năng giữ cho lớp HTTP có thể thực thi; hãy nhớ đăng ký chúng trong một nhà cung cấp dịch vụ.
Ý tưởng để mở rộng:
- Giới thiệu thêm các sự kiện miền (ví dụ:
InventoryItemRegistered) để thông báo cho các bối cảnh ranh giới khác một cách không đồng bộ. - Thêm các điểm cuối PATCH cho điều chỉnh tồn kho với khóa giao dịch.
- Hoán đổi lớp idempotency sang Redis khi lưu lượng tăng lên để giảm thiểu sự cạnh tranh bảng.
- Tăng cường các mô hình đọc CQRS (ví dụ:
inventory_items_view) hoặc cache cho các điểm cuối danh sách.
Kết Luận
Mô hình này không chỉ đơn thuần là DDD học thuật – nó mở rộng API của bạn một cách cả về mặt kỹ thuật và tổ chức. Bằng cách cô lập các quy tắc miền, điều phối qua các dịch vụ ứng dụng, và coi Laravel như hạ tầng/driver, bạn sẽ có một mã nguồn dễ bảo trì, kiểm thử và phát triển.
Nếu bạn áp dụng các lớp tương tự, hãy liên hệ với chúng tôi với những điều chỉnh hoặc cải tiến. Chúc bạn thành công trong việc phát triển! 🚢