@anhth2/spec-driven-dev-plugin 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +243 -0
- package/bin/build.js +230 -0
- package/bin/index.js +311 -0
- package/commands/debug.md +374 -0
- package/commands/debug.tmpl +77 -0
- package/commands/define-product.md +451 -0
- package/commands/define-product.tmpl +154 -0
- package/commands/fix-bug.md +379 -0
- package/commands/fix-bug.tmpl +82 -0
- package/commands/generate-bdd.md +591 -0
- package/commands/generate-bdd.tmpl +294 -0
- package/commands/generate-code.md +395 -0
- package/commands/generate-code.tmpl +98 -0
- package/commands/generate-prd.md +488 -0
- package/commands/generate-prd.tmpl +191 -0
- package/commands/generate-tech-docs.md +362 -0
- package/commands/generate-tech-docs.tmpl +65 -0
- package/commands/generate-tests.md +377 -0
- package/commands/generate-tests.tmpl +80 -0
- package/commands/refine-prd.md +408 -0
- package/commands/refine-prd.tmpl +111 -0
- package/commands/review-code.md +354 -0
- package/commands/review-code.tmpl +57 -0
- package/commands/review-context.md +646 -0
- package/commands/review-context.tmpl +349 -0
- package/commands/review-tech-docs.md +518 -0
- package/commands/review-tech-docs.tmpl +221 -0
- package/commands/run-tests.md +343 -0
- package/commands/run-tests.tmpl +46 -0
- package/commands/setup-ai-first.md +278 -0
- package/commands/setup-ai-first.tmpl +197 -0
- package/commands/smoke-test.md +366 -0
- package/commands/smoke-test.tmpl +69 -0
- package/commands/validate-traces.md +529 -0
- package/commands/validate-traces.tmpl +232 -0
- package/core/FRAMEWORK_VERSION +1 -0
- package/core/commands/debug.md +374 -0
- package/core/commands/define-product.md +451 -0
- package/core/commands/fix-bug.md +379 -0
- package/core/commands/generate-bdd.md +591 -0
- package/core/commands/generate-code.md +395 -0
- package/core/commands/generate-prd.md +488 -0
- package/core/commands/generate-tech-docs.md +362 -0
- package/core/commands/generate-tests.md +377 -0
- package/core/commands/refine-prd.md +408 -0
- package/core/commands/review-code.md +354 -0
- package/core/commands/review-context.md +646 -0
- package/core/commands/review-tech-docs.md +518 -0
- package/core/commands/run-tests.md +343 -0
- package/core/commands/setup-ai-first.md +278 -0
- package/core/commands/smoke-test.md +366 -0
- package/core/commands/validate-traces.md +529 -0
- package/core/hooks/data-guard.js +141 -0
- package/core/hooks/settings.json +18 -0
- package/core/modules/angular/architecture-snippets/component-patterns.md +187 -0
- package/core/modules/angular/module.yaml +6 -0
- package/core/modules/angular/stack-profile.yaml +38 -0
- package/core/modules/context-engineering/architecture-snippets/context-design.md +119 -0
- package/core/modules/context-engineering/module.yaml +9 -0
- package/core/modules/context-engineering/stack-profile.yaml +61 -0
- package/core/modules/dotnet/architecture-snippets/clean-arch.md +160 -0
- package/core/modules/dotnet/module.yaml +6 -0
- package/core/modules/dotnet/stack-profile.yaml +50 -0
- package/core/modules/golang/architecture-snippets/domain-layout.md +283 -0
- package/core/modules/golang/module.yaml +6 -0
- package/core/modules/golang/stack-profile.yaml +40 -0
- package/core/modules/java-spring/architecture-snippets/layered-arch.md +201 -0
- package/core/modules/java-spring/module.yaml +15 -0
- package/core/modules/java-spring/stack-profile.yaml +28 -0
- package/core/modules/nextjs/architecture-snippets/app-router-patterns.md +269 -0
- package/core/modules/nextjs/module.yaml +14 -0
- package/core/modules/nextjs/stack-profile.yaml +74 -0
- package/core/modules/php-laravel/architecture-snippets/service-repository.md +302 -0
- package/core/modules/php-laravel/module.yaml +15 -0
- package/core/modules/php-laravel/stack-profile.yaml +56 -0
- package/core/modules/react/architecture-snippets/hooks-query-patterns.md +254 -0
- package/core/modules/react/module.yaml +14 -0
- package/core/modules/react/stack-profile.yaml +63 -0
- package/core/rules/data-protection.md +80 -0
- package/core/rules/workflow.md +44 -0
- package/core/skills/code/SKILL.md +526 -0
- package/core/skills/debug/SKILL.md +584 -0
- package/core/skills/discovery/SKILL.md +363 -0
- package/core/skills/prd/SKILL.md +456 -0
- package/core/skills/setup-ai-first/SKILL.md +160 -0
- package/core/skills/spec/SKILL.md +361 -0
- package/core/skills/test/SKILL.md +862 -0
- package/core/steps/context-loader.md +163 -0
- package/core/steps/gate.md +81 -0
- package/core/steps/report-footer.md +53 -0
- package/core/steps/spawn-agent.md +123 -0
- package/core/templates/architecture.template.md +113 -0
- package/core/templates/feature.template +259 -0
- package/core/templates/platform-guide.template.md +145 -0
- package/core/templates/prd.template.md +312 -0
- package/core/templates/product-definition.template.md +168 -0
- package/core/templates/project-context.yaml +78 -0
- package/hooks/data-guard.js +141 -0
- package/hooks/settings.json +18 -0
- package/modules/angular/architecture-snippets/component-patterns.md +187 -0
- package/modules/angular/module.yaml +6 -0
- package/modules/angular/stack-profile.yaml +38 -0
- package/modules/context-engineering/architecture-snippets/context-design.md +119 -0
- package/modules/context-engineering/module.yaml +9 -0
- package/modules/context-engineering/stack-profile.yaml +61 -0
- package/modules/dotnet/architecture-snippets/clean-arch.md +160 -0
- package/modules/dotnet/module.yaml +6 -0
- package/modules/dotnet/stack-profile.yaml +50 -0
- package/modules/golang/architecture-snippets/domain-layout.md +283 -0
- package/modules/golang/module.yaml +6 -0
- package/modules/golang/stack-profile.yaml +40 -0
- package/modules/java-spring/architecture-snippets/layered-arch.md +201 -0
- package/modules/java-spring/module.yaml +15 -0
- package/modules/java-spring/stack-profile.yaml +28 -0
- package/modules/nextjs/architecture-snippets/app-router-patterns.md +269 -0
- package/modules/nextjs/module.yaml +14 -0
- package/modules/nextjs/stack-profile.yaml +74 -0
- package/modules/php-laravel/architecture-snippets/service-repository.md +302 -0
- package/modules/php-laravel/module.yaml +15 -0
- package/modules/php-laravel/stack-profile.yaml +56 -0
- package/modules/react/architecture-snippets/hooks-query-patterns.md +254 -0
- package/modules/react/module.yaml +14 -0
- package/modules/react/stack-profile.yaml +63 -0
- package/package.json +42 -0
- package/rules/data-protection.md +80 -0
- package/rules/workflow.md +44 -0
- package/scripts/init.sh +49 -0
- package/scripts/upgrade.sh +94 -0
- package/skills/code/SKILL.md +526 -0
- package/skills/code/SKILL.tmpl +176 -0
- package/skills/debug/SKILL.md +584 -0
- package/skills/debug/SKILL.tmpl +262 -0
- package/skills/discovery/SKILL.md +363 -0
- package/skills/discovery/SKILL.tmpl +147 -0
- package/skills/prd/SKILL.md +456 -0
- package/skills/prd/SKILL.tmpl +188 -0
- package/skills/setup-ai-first/SKILL.md +160 -0
- package/skills/setup-ai-first/SKILL.tmpl +107 -0
- package/skills/spec/SKILL.md +361 -0
- package/skills/spec/SKILL.tmpl +174 -0
- package/skills/test/SKILL.md +862 -0
- package/skills/test/SKILL.tmpl +296 -0
- package/steps/context-loader.md +163 -0
- package/steps/gate.md +81 -0
- package/steps/report-footer.md +53 -0
- package/steps/spawn-agent.md +123 -0
- package/templates/architecture.template.md +113 -0
- package/templates/feature.template +259 -0
- package/templates/platform-guide.template.md +145 -0
- package/templates/prd.template.md +312 -0
- package/templates/product-definition.template.md +168 -0
- package/templates/project-context.yaml +78 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# PHP Laravel — Service-Repository Architecture Patterns
|
|
2
|
+
|
|
3
|
+
## Form Request (Validation Layer)
|
|
4
|
+
|
|
5
|
+
```php
|
|
6
|
+
<?php
|
|
7
|
+
// app/Http/Requests/Order/CreateOrderRequest.php
|
|
8
|
+
|
|
9
|
+
namespace App\Http\Requests\Order;
|
|
10
|
+
|
|
11
|
+
use Illuminate\Foundation\Http\FormRequest;
|
|
12
|
+
|
|
13
|
+
class CreateOrderRequest extends FormRequest
|
|
14
|
+
{
|
|
15
|
+
public function authorize(): bool
|
|
16
|
+
{
|
|
17
|
+
return true; // auth handled by middleware
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public function rules(): array
|
|
21
|
+
{
|
|
22
|
+
return [
|
|
23
|
+
'customer_id' => ['required', 'integer', 'exists:customers,id'],
|
|
24
|
+
'items' => ['required', 'array', 'min:1'],
|
|
25
|
+
'items.*.product_id' => ['required', 'integer', 'exists:products,id'],
|
|
26
|
+
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public function messages(): array
|
|
31
|
+
{
|
|
32
|
+
return [
|
|
33
|
+
'items.required' => 'At least one order item is required.',
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Repository Interface + Eloquent Implementation
|
|
40
|
+
|
|
41
|
+
```php
|
|
42
|
+
<?php
|
|
43
|
+
// app/Repositories/OrderRepositoryInterface.php
|
|
44
|
+
|
|
45
|
+
namespace App\Repositories;
|
|
46
|
+
|
|
47
|
+
use App\Models\Order;
|
|
48
|
+
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
49
|
+
|
|
50
|
+
interface OrderRepositoryInterface
|
|
51
|
+
{
|
|
52
|
+
public function findById(int $id): ?Order;
|
|
53
|
+
public function findByCustomer(int $customerId, int $perPage = 15): LengthAwarePaginator;
|
|
54
|
+
public function create(array $data): Order;
|
|
55
|
+
public function updateStatus(int $id, string $status): bool;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```php
|
|
60
|
+
<?php
|
|
61
|
+
// app/Repositories/Eloquent/OrderRepository.php
|
|
62
|
+
|
|
63
|
+
namespace App\Repositories\Eloquent;
|
|
64
|
+
|
|
65
|
+
use App\Models\Order;
|
|
66
|
+
use App\Repositories\OrderRepositoryInterface;
|
|
67
|
+
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
68
|
+
|
|
69
|
+
class OrderRepository implements OrderRepositoryInterface
|
|
70
|
+
{
|
|
71
|
+
public function findById(int $id): ?Order
|
|
72
|
+
{
|
|
73
|
+
return Order::with(['items.product', 'customer'])->find($id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public function findByCustomer(int $customerId, int $perPage = 15): LengthAwarePaginator
|
|
77
|
+
{
|
|
78
|
+
return Order::where('customer_id', $customerId)
|
|
79
|
+
->with('items')
|
|
80
|
+
->latest()
|
|
81
|
+
->paginate($perPage);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public function create(array $data): Order
|
|
85
|
+
{
|
|
86
|
+
return Order::create($data);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public function updateStatus(int $id, string $status): bool
|
|
90
|
+
{
|
|
91
|
+
return Order::where('id', $id)->update(['status' => $status]) > 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Service Layer
|
|
97
|
+
|
|
98
|
+
```php
|
|
99
|
+
<?php
|
|
100
|
+
// app/Services/Order/OrderService.php
|
|
101
|
+
|
|
102
|
+
namespace App\Services\Order;
|
|
103
|
+
|
|
104
|
+
use App\Exceptions\BusinessException;
|
|
105
|
+
use App\Models\Order;
|
|
106
|
+
use App\Repositories\OrderRepositoryInterface;
|
|
107
|
+
use Illuminate\Support\Facades\DB;
|
|
108
|
+
|
|
109
|
+
class OrderService
|
|
110
|
+
{
|
|
111
|
+
// @trace.implements=ORD-UC1-SC1
|
|
112
|
+
// @trace.source=specs/bdd/order/ORD-UC1.feature
|
|
113
|
+
public function __construct(
|
|
114
|
+
private readonly OrderRepositoryInterface $orderRepository
|
|
115
|
+
) {}
|
|
116
|
+
|
|
117
|
+
public function createOrder(int $customerId, array $items): Order
|
|
118
|
+
{
|
|
119
|
+
return DB::transaction(function () use ($customerId, $items) {
|
|
120
|
+
$order = $this->orderRepository->create([
|
|
121
|
+
'customer_id' => $customerId,
|
|
122
|
+
'status' => 'pending',
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
foreach ($items as $item) {
|
|
126
|
+
$order->items()->create([
|
|
127
|
+
'product_id' => $item['product_id'],
|
|
128
|
+
'quantity' => $item['quantity'],
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return $order->load('items.product');
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public function cancelOrder(int $orderId): void
|
|
137
|
+
{
|
|
138
|
+
$order = $this->orderRepository->findById($orderId);
|
|
139
|
+
|
|
140
|
+
if (!$order) {
|
|
141
|
+
throw new BusinessException("Order #{$orderId} not found.", 404);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if ($order->status !== 'pending') {
|
|
145
|
+
throw new BusinessException("Only pending orders can be cancelled.", 422);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
$this->orderRepository->updateStatus($orderId, 'cancelled');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Resource Controller
|
|
154
|
+
|
|
155
|
+
```php
|
|
156
|
+
<?php
|
|
157
|
+
// app/Http/Controllers/Order/OrderController.php
|
|
158
|
+
|
|
159
|
+
namespace App\Http\Controllers\Order;
|
|
160
|
+
|
|
161
|
+
use App\Http\Controllers\Controller;
|
|
162
|
+
use App\Http\Requests\Order\CreateOrderRequest;
|
|
163
|
+
use App\Http\Resources\Order\OrderResource;
|
|
164
|
+
use App\Services\Order\OrderService;
|
|
165
|
+
use Illuminate\Http\JsonResponse;
|
|
166
|
+
use Illuminate\Http\Request;
|
|
167
|
+
|
|
168
|
+
class OrderController extends Controller
|
|
169
|
+
{
|
|
170
|
+
public function __construct(
|
|
171
|
+
private readonly OrderService $orderService
|
|
172
|
+
) {}
|
|
173
|
+
|
|
174
|
+
// @trace.implements=ORD-UC1-SC1
|
|
175
|
+
// @trace.source=specs/bdd/order/ORD-UC1.feature
|
|
176
|
+
public function store(CreateOrderRequest $request): JsonResponse
|
|
177
|
+
{
|
|
178
|
+
$order = $this->orderService->createOrder(
|
|
179
|
+
customerId: $request->validated('customer_id'),
|
|
180
|
+
items: $request->validated('items')
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return response()->json([
|
|
184
|
+
'status' => 'success',
|
|
185
|
+
'message' => 'Order created successfully.',
|
|
186
|
+
'data' => new OrderResource($order),
|
|
187
|
+
], 201);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// @trace.implements=ORD-UC2-SC1
|
|
191
|
+
// @trace.source=specs/bdd/order/ORD-UC2.feature
|
|
192
|
+
public function destroy(int $id): JsonResponse
|
|
193
|
+
{
|
|
194
|
+
$this->orderService->cancelOrder($id);
|
|
195
|
+
|
|
196
|
+
return response()->json([
|
|
197
|
+
'status' => 'success',
|
|
198
|
+
'message' => 'Order cancelled.',
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## API Resource (Response Transformer)
|
|
205
|
+
|
|
206
|
+
```php
|
|
207
|
+
<?php
|
|
208
|
+
// app/Http/Resources/Order/OrderResource.php
|
|
209
|
+
|
|
210
|
+
namespace App\Http\Resources\Order;
|
|
211
|
+
|
|
212
|
+
use Illuminate\Http\Request;
|
|
213
|
+
use Illuminate\Http\Resources\Json\JsonResource;
|
|
214
|
+
|
|
215
|
+
class OrderResource extends JsonResource
|
|
216
|
+
{
|
|
217
|
+
public function toArray(Request $request): array
|
|
218
|
+
{
|
|
219
|
+
return [
|
|
220
|
+
'id' => $this->id,
|
|
221
|
+
'status' => $this->status,
|
|
222
|
+
'customer_id' => $this->customer_id,
|
|
223
|
+
'items' => OrderItemResource::collection($this->whenLoaded('items')),
|
|
224
|
+
'created_at' => $this->created_at->toISOString(),
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Feature Test (HTTP Layer)
|
|
231
|
+
|
|
232
|
+
```php
|
|
233
|
+
<?php
|
|
234
|
+
// tests/Feature/Order/CreateOrderTest.php
|
|
235
|
+
|
|
236
|
+
namespace Tests\Feature\Order;
|
|
237
|
+
|
|
238
|
+
use App\Models\Customer;
|
|
239
|
+
use App\Models\Product;
|
|
240
|
+
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
241
|
+
use Tests\TestCase;
|
|
242
|
+
|
|
243
|
+
// @trace.verifies=ORD-UC1
|
|
244
|
+
// @trace.test_type=feature
|
|
245
|
+
class CreateOrderTest extends TestCase
|
|
246
|
+
{
|
|
247
|
+
use RefreshDatabase;
|
|
248
|
+
|
|
249
|
+
/** @test */
|
|
250
|
+
public function it_creates_order_successfully(): void
|
|
251
|
+
{
|
|
252
|
+
$customer = Customer::factory()->create();
|
|
253
|
+
$product = Product::factory()->create(['stock' => 10]);
|
|
254
|
+
|
|
255
|
+
$response = $this->actingAs($customer)
|
|
256
|
+
->postJson('/api/v1/orders', [
|
|
257
|
+
'customer_id' => $customer->id,
|
|
258
|
+
'items' => [
|
|
259
|
+
['product_id' => $product->id, 'quantity' => 2],
|
|
260
|
+
],
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
$response->assertStatus(201)
|
|
264
|
+
->assertJsonStructure(['status', 'data' => ['id', 'status', 'items']]);
|
|
265
|
+
|
|
266
|
+
$this->assertDatabaseHas('orders', ['customer_id' => $customer->id, 'status' => 'pending']);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** @test */
|
|
270
|
+
public function it_returns_422_when_items_are_empty(): void
|
|
271
|
+
{
|
|
272
|
+
$customer = Customer::factory()->create();
|
|
273
|
+
|
|
274
|
+
$response = $this->actingAs($customer)
|
|
275
|
+
->postJson('/api/v1/orders', ['customer_id' => $customer->id, 'items' => []]);
|
|
276
|
+
|
|
277
|
+
$response->assertStatus(422)
|
|
278
|
+
->assertJsonValidationErrors(['items']);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Service Provider Binding
|
|
284
|
+
|
|
285
|
+
```php
|
|
286
|
+
<?php
|
|
287
|
+
// app/Providers/RepositoryServiceProvider.php
|
|
288
|
+
|
|
289
|
+
namespace App\Providers;
|
|
290
|
+
|
|
291
|
+
use App\Repositories\Eloquent\OrderRepository;
|
|
292
|
+
use App\Repositories\OrderRepositoryInterface;
|
|
293
|
+
use Illuminate\Support\ServiceProvider;
|
|
294
|
+
|
|
295
|
+
class RepositoryServiceProvider extends ServiceProvider
|
|
296
|
+
{
|
|
297
|
+
public function register(): void
|
|
298
|
+
{
|
|
299
|
+
$this->app->bind(OrderRepositoryInterface::class, OrderRepository::class);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: "PHP Laravel"
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: "Laravel 11 backend with service-repository pattern"
|
|
4
|
+
language: "PHP"
|
|
5
|
+
framework: "Laravel"
|
|
6
|
+
stack_type: "backend"
|
|
7
|
+
default_layer_order:
|
|
8
|
+
- Form Request (validation)
|
|
9
|
+
- Model / Eloquent
|
|
10
|
+
- Repository interface
|
|
11
|
+
- Repository impl
|
|
12
|
+
- Service
|
|
13
|
+
- Controller (Resource Controller)
|
|
14
|
+
- Resource (API response transformer)
|
|
15
|
+
test_framework: "PHPUnit + Pest"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
build:
|
|
2
|
+
compile: "composer install"
|
|
3
|
+
test: "php artisan test"
|
|
4
|
+
run: "php artisan serve"
|
|
5
|
+
migrate: "php artisan migrate"
|
|
6
|
+
seed: "php artisan db:seed"
|
|
7
|
+
|
|
8
|
+
architecture:
|
|
9
|
+
style: "Service-Repository (Controller → Service → Repository → Model)"
|
|
10
|
+
key_rules:
|
|
11
|
+
- "Controllers must not contain business logic — delegate to Services"
|
|
12
|
+
- "Services own business logic and transaction boundaries"
|
|
13
|
+
- "Repositories abstract all Eloquent/DB queries"
|
|
14
|
+
- "Form Requests handle all input validation (never validate in controller)"
|
|
15
|
+
- "API Resources transform Model output (never return raw Model/array)"
|
|
16
|
+
- "Use dependency injection via constructor, not Facades in classes"
|
|
17
|
+
|
|
18
|
+
coding_standards:
|
|
19
|
+
naming:
|
|
20
|
+
controllers: "PascalCase + Controller suffix — Resource style (e.g., OrderController)"
|
|
21
|
+
services: "PascalCase + Service suffix (e.g., OrderService)"
|
|
22
|
+
repositories:
|
|
23
|
+
interface: "PascalCase + RepositoryInterface (e.g., OrderRepositoryInterface)"
|
|
24
|
+
impl: "PascalCase + Repository (e.g., EloquentOrderRepository)"
|
|
25
|
+
models: "PascalCase singular (e.g., Order)"
|
|
26
|
+
form_requests: "PascalCase + Request suffix (e.g., CreateOrderRequest)"
|
|
27
|
+
resources: "PascalCase + Resource suffix (e.g., OrderResource)"
|
|
28
|
+
files:
|
|
29
|
+
controller: "app/Http/Controllers/{Domain}/{Name}Controller.php"
|
|
30
|
+
service: "app/Services/{Domain}/{Name}Service.php"
|
|
31
|
+
repository_interface: "app/Repositories/{Name}RepositoryInterface.php"
|
|
32
|
+
repository_impl: "app/Repositories/Eloquent/{Name}Repository.php"
|
|
33
|
+
model: "app/Models/{Name}.php"
|
|
34
|
+
form_request: "app/Http/Requests/{Domain}/{Action}{Name}Request.php"
|
|
35
|
+
resource: "app/Http/Resources/{Domain}/{Name}Resource.php"
|
|
36
|
+
patterns:
|
|
37
|
+
response_wrapper: "JsonResponse with status/data/message structure"
|
|
38
|
+
pagination: "Laravel paginate() — returns LengthAwarePaginator"
|
|
39
|
+
exception_base: "App\\Exceptions\\BusinessException"
|
|
40
|
+
transactions: "DB::transaction(fn () => ...) in Service layer"
|
|
41
|
+
events: "Laravel Events + Listeners for domain events"
|
|
42
|
+
|
|
43
|
+
testing:
|
|
44
|
+
unit: "PHPUnit or Pest"
|
|
45
|
+
feature: "Laravel Feature Tests (HTTP layer with database)"
|
|
46
|
+
patterns:
|
|
47
|
+
- "Use RefreshDatabase or DatabaseTransactions trait"
|
|
48
|
+
- "Fake external services with Http::fake() or mock()"
|
|
49
|
+
- "Use actingAs(\$user) for auth context"
|
|
50
|
+
- "Assert JSON structure with assertJson() / assertJsonStructure()"
|
|
51
|
+
|
|
52
|
+
trace_tags:
|
|
53
|
+
implements: "// @trace.implements={UC-ID}-SC{N}"
|
|
54
|
+
source: "// @trace.source=specs/bdd/{domain}/{UC-ID}.feature"
|
|
55
|
+
verifies: "// @trace.verifies={UC-ID}"
|
|
56
|
+
test_type: "// @trace.test_type=unit|feature"
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# React — Hooks, React Query & Zustand Patterns
|
|
2
|
+
|
|
3
|
+
## React Query — Query Hook
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// features/order/queries/order.queries.ts
|
|
7
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import { orderApi } from '@/api/order.api';
|
|
9
|
+
import type { CreateOrderPayload, Order } from '../types';
|
|
10
|
+
|
|
11
|
+
// Query key factory — single source of truth for cache invalidation
|
|
12
|
+
export const orderKeys = {
|
|
13
|
+
all: ['ORDERS'] as const,
|
|
14
|
+
lists: () => [...orderKeys.all, 'list'] as const,
|
|
15
|
+
list: (customerId: number) => [...orderKeys.lists(), { customerId }] as const,
|
|
16
|
+
detail: (id: number) => [...orderKeys.all, 'detail', id] as const,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// @trace.implements=ORD-UC1-SC1
|
|
20
|
+
// @trace.source=specs/bdd/order/ORD-UC1.feature
|
|
21
|
+
export function useOrderList(customerId: number) {
|
|
22
|
+
return useQuery({
|
|
23
|
+
queryKey: orderKeys.list(customerId),
|
|
24
|
+
queryFn: () => orderApi.getByCustomer(customerId),
|
|
25
|
+
staleTime: 30_000,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// @trace.implements=ORD-UC2-SC1
|
|
30
|
+
// @trace.source=specs/bdd/order/ORD-UC2.feature
|
|
31
|
+
export function useCreateOrder() {
|
|
32
|
+
const queryClient = useQueryClient();
|
|
33
|
+
|
|
34
|
+
return useMutation({
|
|
35
|
+
mutationFn: (payload: CreateOrderPayload) => orderApi.create(payload),
|
|
36
|
+
onSuccess: (_, variables) => {
|
|
37
|
+
// Invalidate order list for this customer
|
|
38
|
+
queryClient.invalidateQueries({ queryKey: orderKeys.list(variables.customerId) });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API Client (Axios)
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// api/order.api.ts
|
|
48
|
+
import { apiClient } from '@/api/client';
|
|
49
|
+
import type { CreateOrderPayload, Order, PaginatedResponse } from '@/features/order/types';
|
|
50
|
+
|
|
51
|
+
export const orderApi = {
|
|
52
|
+
getByCustomer: (customerId: number) =>
|
|
53
|
+
apiClient.get<PaginatedResponse<Order>>('/v1/orders', { params: { customerId } })
|
|
54
|
+
.then(res => res.data),
|
|
55
|
+
|
|
56
|
+
getById: (id: number) =>
|
|
57
|
+
apiClient.get<Order>(`/v1/orders/${id}`).then(res => res.data),
|
|
58
|
+
|
|
59
|
+
create: (payload: CreateOrderPayload) =>
|
|
60
|
+
apiClient.post<Order>('/v1/orders', payload).then(res => res.data),
|
|
61
|
+
|
|
62
|
+
cancel: (id: number) =>
|
|
63
|
+
apiClient.patch<void>(`/v1/orders/${id}/cancel`).then(res => res.data),
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// api/client.ts — Axios instance with auth interceptor
|
|
69
|
+
import axios from 'axios';
|
|
70
|
+
|
|
71
|
+
export const apiClient = axios.create({
|
|
72
|
+
baseURL: import.meta.env.VITE_API_URL,
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
apiClient.interceptors.request.use(config => {
|
|
77
|
+
const token = localStorage.getItem('access_token');
|
|
78
|
+
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
79
|
+
return config;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
apiClient.interceptors.response.use(
|
|
83
|
+
res => res,
|
|
84
|
+
error => {
|
|
85
|
+
if (error.response?.status === 401) {
|
|
86
|
+
// redirect to login
|
|
87
|
+
window.location.href = '/login';
|
|
88
|
+
}
|
|
89
|
+
return Promise.reject(error);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Zustand Store (Client State)
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// features/cart/store/cart.store.ts
|
|
98
|
+
import { create } from 'zustand';
|
|
99
|
+
import { persist } from 'zustand/middleware';
|
|
100
|
+
import type { CartItem } from '../types';
|
|
101
|
+
|
|
102
|
+
interface CartStore {
|
|
103
|
+
items: CartItem[];
|
|
104
|
+
addItem: (item: CartItem) => void;
|
|
105
|
+
removeItem: (productId: number) => void;
|
|
106
|
+
clearCart: () => void;
|
|
107
|
+
total: () => number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const useCartStore = create<CartStore>()(
|
|
111
|
+
persist(
|
|
112
|
+
(set, get) => ({
|
|
113
|
+
items: [],
|
|
114
|
+
|
|
115
|
+
addItem: (item) =>
|
|
116
|
+
set(state => {
|
|
117
|
+
const existing = state.items.find(i => i.productId === item.productId);
|
|
118
|
+
if (existing) {
|
|
119
|
+
return {
|
|
120
|
+
items: state.items.map(i =>
|
|
121
|
+
i.productId === item.productId
|
|
122
|
+
? { ...i, quantity: i.quantity + item.quantity }
|
|
123
|
+
: i
|
|
124
|
+
),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return { items: [...state.items, item] };
|
|
128
|
+
}),
|
|
129
|
+
|
|
130
|
+
removeItem: (productId) =>
|
|
131
|
+
set(state => ({ items: state.items.filter(i => i.productId !== productId) })),
|
|
132
|
+
|
|
133
|
+
clearCart: () => set({ items: [] }),
|
|
134
|
+
|
|
135
|
+
total: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
|
|
136
|
+
}),
|
|
137
|
+
{ name: 'cart-storage' }
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Feature Component (Container)
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// features/order/components/OrderListPage.tsx
|
|
146
|
+
// @trace.implements=ORD-UC1-SC1
|
|
147
|
+
// @trace.source=specs/bdd/order/ORD-UC1.feature
|
|
148
|
+
import { useAuth } from '@/hooks/useAuth';
|
|
149
|
+
import { useOrderList } from '../queries/order.queries';
|
|
150
|
+
import { OrderCard } from './OrderCard';
|
|
151
|
+
|
|
152
|
+
export function OrderListPage() {
|
|
153
|
+
const { user } = useAuth();
|
|
154
|
+
const { data: orders, isLoading, isError } = useOrderList(user.id);
|
|
155
|
+
|
|
156
|
+
if (isLoading) return <div>Loading...</div>;
|
|
157
|
+
if (isError) return <div>Failed to load orders.</div>;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className="order-list">
|
|
161
|
+
{orders?.map(order => (
|
|
162
|
+
<OrderCard key={order.id} order={order} />
|
|
163
|
+
))}
|
|
164
|
+
{orders?.length === 0 && <p>No orders yet.</p>}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## React Hook Form + Zod
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
// features/order/components/CreateOrderForm.tsx
|
|
174
|
+
import { useForm } from 'react-hook-form';
|
|
175
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
176
|
+
import { z } from 'zod';
|
|
177
|
+
import { useCreateOrder } from '../queries/order.queries';
|
|
178
|
+
|
|
179
|
+
const createOrderSchema = z.object({
|
|
180
|
+
customerId: z.number().positive(),
|
|
181
|
+
items: z.array(z.object({
|
|
182
|
+
productId: z.number().positive(),
|
|
183
|
+
quantity: z.number().int().min(1),
|
|
184
|
+
})).min(1, 'At least one item required'),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
type CreateOrderForm = z.infer<typeof createOrderSchema>;
|
|
188
|
+
|
|
189
|
+
export function CreateOrderForm({ customerId }: { customerId: number }) {
|
|
190
|
+
const { mutate: createOrder, isPending } = useCreateOrder();
|
|
191
|
+
|
|
192
|
+
const { register, handleSubmit, formState: { errors } } = useForm<CreateOrderForm>({
|
|
193
|
+
resolver: zodResolver(createOrderSchema),
|
|
194
|
+
defaultValues: { customerId, items: [] },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const onSubmit = (data: CreateOrderForm) => createOrder(data);
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
201
|
+
{/* form fields */}
|
|
202
|
+
{errors.items && <p className="error">{errors.items.message}</p>}
|
|
203
|
+
<button type="submit" disabled={isPending}>
|
|
204
|
+
{isPending ? 'Creating...' : 'Create Order'}
|
|
205
|
+
</button>
|
|
206
|
+
</form>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Test with React Testing Library + MSW
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// features/order/__tests__/OrderListPage.test.tsx
|
|
215
|
+
// @trace.verifies=ORD-UC1
|
|
216
|
+
// @trace.test_type=integration
|
|
217
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
218
|
+
import { http, HttpResponse } from 'msw';
|
|
219
|
+
import { server } from '@/test/server';
|
|
220
|
+
import { renderWithProviders } from '@/test/utils';
|
|
221
|
+
import { OrderListPage } from '../components/OrderListPage';
|
|
222
|
+
|
|
223
|
+
describe('OrderListPage', () => {
|
|
224
|
+
it('displays orders after successful fetch', async () => {
|
|
225
|
+
server.use(
|
|
226
|
+
http.get('/v1/orders', () =>
|
|
227
|
+
HttpResponse.json([
|
|
228
|
+
{ id: 1, status: 'pending', customerId: 42, items: [] },
|
|
229
|
+
])
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
renderWithProviders(<OrderListPage />, { user: { id: 42 } });
|
|
234
|
+
|
|
235
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
236
|
+
|
|
237
|
+
await waitFor(() =>
|
|
238
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(screen.getByText('Order #1')).toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('shows empty state when no orders exist', async () => {
|
|
245
|
+
server.use(http.get('/v1/orders', () => HttpResponse.json([])));
|
|
246
|
+
|
|
247
|
+
renderWithProviders(<OrderListPage />, { user: { id: 42 } });
|
|
248
|
+
|
|
249
|
+
await waitFor(() =>
|
|
250
|
+
expect(screen.getByText('No orders yet.')).toBeInTheDocument()
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: "React"
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: "React 18 SPA with hooks, React Query, and Zustand"
|
|
4
|
+
language: "TypeScript"
|
|
5
|
+
framework: "React"
|
|
6
|
+
stack_type: "frontend"
|
|
7
|
+
default_layer_order:
|
|
8
|
+
- Types / interfaces
|
|
9
|
+
- API client (React Query hooks)
|
|
10
|
+
- State store (Zustand)
|
|
11
|
+
- Custom hooks (business logic)
|
|
12
|
+
- UI components (presentational)
|
|
13
|
+
- Page / feature components (container)
|
|
14
|
+
test_framework: "Vitest + React Testing Library"
|