@famgia/omnify-laravel 0.0.87 → 0.0.89
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/dist/{chunk-YVVAJA3T.js → chunk-V7LWJ6OM.js} +178 -12
- package/dist/chunk-V7LWJ6OM.js.map +1 -0
- package/dist/index.cjs +180 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.js +5 -1
- package/dist/plugin.cjs +176 -11
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.js +1 -1
- package/package.json +5 -5
- package/scripts/postinstall.js +29 -36
- package/stubs/ai-guides/claude-agents/architect.md.stub +150 -0
- package/stubs/ai-guides/claude-agents/developer.md.stub +190 -0
- package/stubs/ai-guides/claude-agents/reviewer.md.stub +134 -0
- package/stubs/ai-guides/claude-agents/tester.md.stub +196 -0
- package/stubs/ai-guides/claude-checklists/backend.md.stub +112 -0
- package/stubs/ai-guides/claude-omnify/antdesign-guide.md.stub +401 -0
- package/stubs/ai-guides/claude-omnify/config-guide.md.stub +253 -0
- package/stubs/ai-guides/claude-omnify/japan-guide.md.stub +186 -0
- package/stubs/ai-guides/claude-omnify/laravel-guide.md.stub +61 -0
- package/stubs/ai-guides/claude-omnify/schema-guide.md.stub +115 -0
- package/stubs/ai-guides/claude-omnify/typescript-guide.md.stub +310 -0
- package/stubs/ai-guides/claude-rules/naming.md.stub +364 -0
- package/stubs/ai-guides/claude-rules/performance.md.stub +251 -0
- package/stubs/ai-guides/claude-rules/security.md.stub +159 -0
- package/stubs/ai-guides/claude-workflows/bug-fix.md.stub +201 -0
- package/stubs/ai-guides/claude-workflows/code-review.md.stub +164 -0
- package/stubs/ai-guides/claude-workflows/new-feature.md.stub +327 -0
- package/stubs/ai-guides/cursor/laravel-controller.mdc.stub +391 -0
- package/stubs/ai-guides/cursor/laravel-request.mdc.stub +112 -0
- package/stubs/ai-guides/cursor/laravel-resource.mdc.stub +73 -0
- package/stubs/ai-guides/cursor/laravel-review.mdc.stub +69 -0
- package/stubs/ai-guides/cursor/laravel-testing.mdc.stub +138 -0
- package/stubs/ai-guides/cursor/laravel.mdc.stub +82 -0
- package/stubs/ai-guides/laravel/README.md.stub +59 -0
- package/stubs/ai-guides/laravel/architecture.md.stub +424 -0
- package/stubs/ai-guides/laravel/controller.md.stub +484 -0
- package/stubs/ai-guides/laravel/datetime.md.stub +334 -0
- package/stubs/ai-guides/laravel/openapi.md.stub +369 -0
- package/stubs/ai-guides/laravel/request.md.stub +450 -0
- package/stubs/ai-guides/laravel/resource.md.stub +516 -0
- package/stubs/ai-guides/laravel/service.md.stub +503 -0
- package/stubs/ai-guides/laravel/testing.md.stub +1504 -0
- package/ai-guides/laravel-guide.md +0 -461
- package/dist/chunk-YVVAJA3T.js.map +0 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "PEST testing rules - 正常系/異常系, naming conventions"
|
|
3
|
+
globs: ["tests/**", "**/*Test.php"]
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Testing Rules (PEST)
|
|
8
|
+
|
|
9
|
+
> **Agent:** Act as **@tester** agent
|
|
10
|
+
> - Read `.claude/guides/laravel/testing.md` for full guide
|
|
11
|
+
|
|
12
|
+
## How to Run Tests
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Run ALL tests (from project root - uses Docker wrapper)
|
|
16
|
+
./artisan test
|
|
17
|
+
|
|
18
|
+
# Run specific test file
|
|
19
|
+
./artisan test --filter=UserControllerTest
|
|
20
|
+
|
|
21
|
+
# Run specific test method
|
|
22
|
+
./artisan test --filter="creates user with valid data"
|
|
23
|
+
|
|
24
|
+
# Run with coverage (if xdebug installed)
|
|
25
|
+
./artisan test --coverage
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> **Note:** The root `./artisan` script is a wrapper that runs commands inside Docker container.
|
|
29
|
+
|
|
30
|
+
## Testing Process
|
|
31
|
+
|
|
32
|
+
1. **Before writing tests:**
|
|
33
|
+
- Read the Controller to understand endpoints
|
|
34
|
+
- Read the Request to understand validation rules
|
|
35
|
+
- Read the Resource to understand response structure
|
|
36
|
+
|
|
37
|
+
2. **Write tests covering:**
|
|
38
|
+
- 正常系 (Normal cases) - success scenarios
|
|
39
|
+
- 異常系 (Abnormal cases) - failure scenarios
|
|
40
|
+
|
|
41
|
+
3. **Run tests to verify:**
|
|
42
|
+
```bash
|
|
43
|
+
cd backend && ./artisan test
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
4. **All tests must pass before committing**
|
|
47
|
+
|
|
48
|
+
## Critical Rules
|
|
49
|
+
|
|
50
|
+
1. **PEST Syntax** - Use `describe()` + `it()`, not PHPUnit
|
|
51
|
+
2. **正常系 + 異常系** - Cover both success and failure
|
|
52
|
+
3. **Naming** - Use `正常:` and `異常:` prefixes
|
|
53
|
+
4. **Coverage** - All endpoints, all validation rules
|
|
54
|
+
5. **RefreshDatabase** - MUST use for SQLite in-memory
|
|
55
|
+
|
|
56
|
+
## Database Trait - IMPORTANT
|
|
57
|
+
|
|
58
|
+
```php
|
|
59
|
+
// ✅ CORRECT - Use RefreshDatabase for SQLite in-memory
|
|
60
|
+
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
61
|
+
uses(RefreshDatabase::class);
|
|
62
|
+
|
|
63
|
+
// ❌ WRONG - DatabaseTransactions doesn't run migrations
|
|
64
|
+
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
65
|
+
uses(DatabaseTransactions::class); // Will fail with "no such table"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Trait | When to Use | Notes |
|
|
69
|
+
| ---------------------- | --------------------------------- | ------------------------------- |
|
|
70
|
+
| `RefreshDatabase` | SQLite in-memory (default) | Runs migrations, then truncates |
|
|
71
|
+
| `DatabaseTransactions` | MySQL/PostgreSQL with existing DB | Only wraps in transaction |
|
|
72
|
+
|
|
73
|
+
## Test Naming
|
|
74
|
+
|
|
75
|
+
```php
|
|
76
|
+
// 正常系 (Normal) - success behavior
|
|
77
|
+
it('正常: returns paginated users')
|
|
78
|
+
it('正常: creates user with valid data')
|
|
79
|
+
it('正常: deletes user')
|
|
80
|
+
|
|
81
|
+
// 異常系 (Abnormal) - failure behavior
|
|
82
|
+
it('異常: fails to create user with missing email')
|
|
83
|
+
it('異常: fails to create user with invalid kana format')
|
|
84
|
+
it('異常: returns 404 when user not found')
|
|
85
|
+
it('異常: returns 401 when not authenticated')
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Coverage Matrix
|
|
89
|
+
|
|
90
|
+
| Endpoint | 正常系 | 異常系 |
|
|
91
|
+
| -------- | ------------------ | --------------------- |
|
|
92
|
+
| index | List, filter, sort | Empty, invalid params |
|
|
93
|
+
| store | Creates → 201 | 422 (each field) |
|
|
94
|
+
| show | Returns → 200 | 404 |
|
|
95
|
+
| update | Updates → 200 | 404, 422 |
|
|
96
|
+
| destroy | Deletes → 204 | 404 |
|
|
97
|
+
|
|
98
|
+
## Template
|
|
99
|
+
|
|
100
|
+
```php
|
|
101
|
+
describe('POST /api/users', function () {
|
|
102
|
+
// 正常系
|
|
103
|
+
it('正常: creates user with valid data', function () {
|
|
104
|
+
$response = $this->postJson('/api/users', validUserData());
|
|
105
|
+
|
|
106
|
+
$response->assertCreated();
|
|
107
|
+
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 異常系 - Required fields
|
|
111
|
+
it('異常: fails to create user with missing email', function () {
|
|
112
|
+
$data = validUserData();
|
|
113
|
+
unset($data['email']);
|
|
114
|
+
|
|
115
|
+
$this->postJson('/api/users', $data)
|
|
116
|
+
->assertUnprocessable()
|
|
117
|
+
->assertJsonValidationErrors(['email']);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// 異常系 - Format validation
|
|
121
|
+
it('異常: fails to create user with invalid email format', function () {
|
|
122
|
+
$this->postJson('/api/users', validUserData(['email' => 'invalid']))
|
|
123
|
+
->assertUnprocessable()
|
|
124
|
+
->assertJsonValidationErrors(['email']);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Japanese Field Tests
|
|
130
|
+
|
|
131
|
+
```php
|
|
132
|
+
it('異常: fails with hiragana in kana field', function () {
|
|
133
|
+
$this->postJson('/api/users', validUserData([
|
|
134
|
+
'name_kana_lastname' => 'たなか' // hiragana - should fail
|
|
135
|
+
]))->assertUnprocessable()
|
|
136
|
+
->assertJsonValidationErrors(['name_kana_lastname']);
|
|
137
|
+
});
|
|
138
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Laravel backend development rules - security, performance, patterns"
|
|
3
|
+
globs: ["{{LARAVEL_BASE}}/**"]
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Laravel Rules
|
|
8
|
+
|
|
9
|
+
> **Documentation:** `.claude/guides/laravel/`
|
|
10
|
+
> **Specific Rules:** See also `laravel-controller.mdc`, `laravel-resource.mdc`, `laravel-request.mdc`, `laravel-testing.mdc`
|
|
11
|
+
|
|
12
|
+
## Critical Rules
|
|
13
|
+
|
|
14
|
+
1. **Thin Controller** - Validate → Delegate → Respond (see `laravel-controller.mdc`)
|
|
15
|
+
2. **Schema-First** - Use Omnify schemas, don't create migrations manually
|
|
16
|
+
3. **Security** - Always use `$request->validated()`, never `$request->all()`
|
|
17
|
+
4. **Performance** - Use `with()` for relations, `paginate()` for lists
|
|
18
|
+
5. **Dates** - Store UTC, return `->toISOString()`
|
|
19
|
+
6. **Testing** - Write 正常系 + 異常系 tests (see `laravel-testing.mdc`)
|
|
20
|
+
7. **Imports** - Always `use` and short class names, never FQCN inline
|
|
21
|
+
|
|
22
|
+
## File-Specific Rules
|
|
23
|
+
|
|
24
|
+
| File Type | Rule File | Key Focus |
|
|
25
|
+
| ---------- | ----------------------- | ---------------------------- |
|
|
26
|
+
| Controller | `laravel-controller.mdc` | Thin, Query Builder, OpenAPI |
|
|
27
|
+
| Resource | `laravel-resource.mdc` | Schema matches output |
|
|
28
|
+
| Request | `laravel-request.mdc` | Schema matches validation |
|
|
29
|
+
| Test | `laravel-testing.mdc` | 正常系 + 異常系 coverage |
|
|
30
|
+
|
|
31
|
+
## Security Checklist
|
|
32
|
+
|
|
33
|
+
- [ ] `$fillable` defined in Model
|
|
34
|
+
- [ ] `$hidden` for sensitive fields
|
|
35
|
+
- [ ] `$request->validated()` not `$request->all()`
|
|
36
|
+
- [ ] No raw SQL with user input
|
|
37
|
+
- [ ] `with()` for eager loading
|
|
38
|
+
- [ ] `whenLoaded()` in Resources
|
|
39
|
+
- [ ] `paginate()` for list endpoints
|
|
40
|
+
|
|
41
|
+
## When to Use What
|
|
42
|
+
|
|
43
|
+
| Scenario | Solution |
|
|
44
|
+
| ---------------- | ------------------ |
|
|
45
|
+
| Simple CRUD | Controller + Model |
|
|
46
|
+
| Multi-step logic | Service |
|
|
47
|
+
| Reusable action | Action class |
|
|
48
|
+
| Async task | Job |
|
|
49
|
+
|
|
50
|
+
## Pre-Edit Checklist
|
|
51
|
+
|
|
52
|
+
**BEFORE editing any file, MUST:**
|
|
53
|
+
|
|
54
|
+
1. **Read the file first** - Check existing imports, patterns, style
|
|
55
|
+
2. **Check imports** - Add `use` if class not imported
|
|
56
|
+
3. **Follow existing style** - Match indentation, naming, patterns
|
|
57
|
+
4. **Never use FQCN inline** - Always import first
|
|
58
|
+
|
|
59
|
+
## Imports Rule
|
|
60
|
+
|
|
61
|
+
```php
|
|
62
|
+
// ❌ WRONG: Inline FQCN
|
|
63
|
+
public function store(): \Illuminate\Http\JsonResponse
|
|
64
|
+
|
|
65
|
+
// ✅ CORRECT: Import and use short name
|
|
66
|
+
use Illuminate\Http\JsonResponse;
|
|
67
|
+
public function store(): JsonResponse
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Don't Over-Engineer
|
|
71
|
+
|
|
72
|
+
| ❌ DON'T | ✅ DO |
|
|
73
|
+
| ----------------------------------- | ------------------------- |
|
|
74
|
+
| Add workaround to hide bugs | Fix root cause or report |
|
|
75
|
+
| Repository for simple CRUD | Use Eloquent directly |
|
|
76
|
+
| Service that just wraps Model | Controller + Model |
|
|
77
|
+
| Interface with 1 implementation | Concrete class |
|
|
78
|
+
| Manual 404 check with route binding | Trust framework |
|
|
79
|
+
| "Improve" unrelated code | Change only what's needed |
|
|
80
|
+
| Build for future "just in case" | YAGNI - build when needed |
|
|
81
|
+
|
|
82
|
+
**Full examples:** `.claude/guides/laravel/architecture.md`
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Backend Guides
|
|
2
|
+
|
|
3
|
+
> Laravel 12, PHP 8.4, MySQL 8
|
|
4
|
+
|
|
5
|
+
## Quick Navigation
|
|
6
|
+
|
|
7
|
+
| Guide | Description |
|
|
8
|
+
| ------------------------------------ | --------------------------------------------- |
|
|
9
|
+
| [architecture.md](./architecture.md) | Design philosophy, when to use Service/Action |
|
|
10
|
+
| [controller.md](./controller.md) | Thin controller pattern, CRUD template |
|
|
11
|
+
| [request.md](./request.md) | Form validation, FormRequest |
|
|
12
|
+
| [resource.md](./resource.md) | API response format, dates |
|
|
13
|
+
| [service.md](./service.md) | When & how to use services |
|
|
14
|
+
| [testing.md](./testing.md) | PEST, 正常系/異常系 |
|
|
15
|
+
| [openapi.md](./openapi.md) | Swagger documentation |
|
|
16
|
+
| [datetime.md](./datetime.md) | Carbon, UTC handling |
|
|
17
|
+
|
|
18
|
+
## Related
|
|
19
|
+
|
|
20
|
+
| Topic | Location |
|
|
21
|
+
| ------------------------ | ----------------------------------------------------------- |
|
|
22
|
+
| **Security rules** | [/rules/security.md](../../rules/security.md) |
|
|
23
|
+
| **Performance rules** | [/rules/performance.md](../../rules/performance.md) |
|
|
24
|
+
| **Naming conventions** | [/rules/naming.md](../../rules/naming.md) |
|
|
25
|
+
| **Checklist** | [/checklists/backend.md](../../checklists/backend.md) |
|
|
26
|
+
| **New feature workflow** | [/workflows/new-feature.md](../../workflows/new-feature.md) |
|
|
27
|
+
|
|
28
|
+
## Quick Patterns
|
|
29
|
+
|
|
30
|
+
### Thin Controller
|
|
31
|
+
|
|
32
|
+
```php
|
|
33
|
+
public function store(UserStoreRequest $request): UserResource
|
|
34
|
+
{
|
|
35
|
+
return new UserResource(User::create($request->validated()));
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Resource with Dates
|
|
40
|
+
|
|
41
|
+
```php
|
|
42
|
+
public function toArray($request): array
|
|
43
|
+
{
|
|
44
|
+
return [
|
|
45
|
+
'id' => $this->id,
|
|
46
|
+
'name' => $this->name,
|
|
47
|
+
'created_at' => $this->created_at?->toISOString(),
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### When to Use What
|
|
53
|
+
|
|
54
|
+
| Question | Answer |
|
|
55
|
+
| -------------------------- | --------------- |
|
|
56
|
+
| Simple CRUD? | Controller only |
|
|
57
|
+
| Multi-step business logic? | Service |
|
|
58
|
+
| Single reusable action? | Action class |
|
|
59
|
+
| Async task? | Job |
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# Design Philosophy
|
|
2
|
+
|
|
3
|
+
> This document explains the architectural decisions and design principles for this Laravel backend.
|
|
4
|
+
|
|
5
|
+
## Architecture: Thin Controller + Service (When Needed)
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
flowchart TD
|
|
9
|
+
subgraph HTTP["HTTP Layer"]
|
|
10
|
+
Route[Route] --> Middleware --> FormRequest
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
FormRequest --> Controller
|
|
14
|
+
|
|
15
|
+
subgraph Controller["Controller Layer (Thin)"]
|
|
16
|
+
C[Orchestrate request → response]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Controller --> Model
|
|
20
|
+
Controller --> Service
|
|
21
|
+
|
|
22
|
+
subgraph Data["Data Layer"]
|
|
23
|
+
Model["Model (Simple CRUD)"]
|
|
24
|
+
Service["Service (Complex ops)"]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Model --> Resource
|
|
28
|
+
Service --> Resource
|
|
29
|
+
|
|
30
|
+
subgraph Output["Response Layer"]
|
|
31
|
+
Resource[Resource - JSON format]
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Layer | Responsibility | Rules |
|
|
36
|
+
| ---------- | ------------------------------ | ----------------------------------- |
|
|
37
|
+
| HTTP | Routing, auth, validation | No business logic |
|
|
38
|
+
| Controller | Orchestrate request → response | Delegate to Model or Service |
|
|
39
|
+
| Model | Simple CRUD | `User::create()`, `$user->update()` |
|
|
40
|
+
| Service | Complex operations | `OrderService`, `PaymentService` |
|
|
41
|
+
| Resource | Format JSON response | Dates to ISO 8601 |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Core Principle: Don't Over-Engineer
|
|
46
|
+
|
|
47
|
+
### ❌ BAD: Over-Engineering Simple CRUD
|
|
48
|
+
|
|
49
|
+
```php
|
|
50
|
+
// DON'T create all these for simple CRUD:
|
|
51
|
+
app/
|
|
52
|
+
├── Repositories/
|
|
53
|
+
│ ├── UserRepositoryInterface.php
|
|
54
|
+
│ └── UserRepository.php
|
|
55
|
+
├── Services/
|
|
56
|
+
│ └── UserService.php
|
|
57
|
+
├── DTOs/
|
|
58
|
+
│ └── UserDTO.php
|
|
59
|
+
└── Contracts/
|
|
60
|
+
└── UserServiceInterface.php
|
|
61
|
+
|
|
62
|
+
// Controller calls Service calls Repository calls Model
|
|
63
|
+
// 4 layers for a simple User::create()!
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### ✅ GOOD: Simple CRUD = Controller + Model
|
|
67
|
+
|
|
68
|
+
```php
|
|
69
|
+
// Simple CRUD - Controller is enough
|
|
70
|
+
class UserController extends Controller
|
|
71
|
+
{
|
|
72
|
+
public function store(UserStoreRequest $request): UserResource
|
|
73
|
+
{
|
|
74
|
+
$user = User::create($request->validated());
|
|
75
|
+
return new UserResource($user);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// That's it! No extra layers needed.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## When to Add Each Layer
|
|
85
|
+
|
|
86
|
+
### Layer Decision Matrix
|
|
87
|
+
|
|
88
|
+
| Scenario | Controller | Service | Action | Job |
|
|
89
|
+
| ------------------------- | ---------- | ------- | ------ | --- |
|
|
90
|
+
| Simple CRUD | ✅ | ❌ | ❌ | ❌ |
|
|
91
|
+
| CRUD + send email | ✅ | ❌ | ❌ | ✅ |
|
|
92
|
+
| Multi-step business logic | ✅ | ✅ | ❌ | ❌ |
|
|
93
|
+
| Reusable single operation | ✅ | ❌ | ✅ | ❌ |
|
|
94
|
+
| Long-running task | ✅ | ❌ | ❌ | ✅ |
|
|
95
|
+
|
|
96
|
+
### Detailed Examples
|
|
97
|
+
|
|
98
|
+
#### 1. Simple CRUD → Controller Only
|
|
99
|
+
|
|
100
|
+
```php
|
|
101
|
+
// ✅ Direct model operations
|
|
102
|
+
public function store(UserStoreRequest $request): UserResource
|
|
103
|
+
{
|
|
104
|
+
$user = User::create($request->validated());
|
|
105
|
+
return new UserResource($user);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public function update(UserUpdateRequest $request, User $user): UserResource
|
|
109
|
+
{
|
|
110
|
+
$user->update($request->validated());
|
|
111
|
+
return new UserResource($user);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### 2. CRUD + Side Effects → Controller + Job/Event
|
|
116
|
+
|
|
117
|
+
```php
|
|
118
|
+
// ✅ Use Job for async operations
|
|
119
|
+
public function store(UserStoreRequest $request): UserResource
|
|
120
|
+
{
|
|
121
|
+
$user = User::create($request->validated());
|
|
122
|
+
|
|
123
|
+
SendWelcomeEmail::dispatch($user); // Async job
|
|
124
|
+
event(new UserRegistered($user)); // Or event
|
|
125
|
+
|
|
126
|
+
return new UserResource($user);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### 3. Complex Business Logic → Service
|
|
131
|
+
|
|
132
|
+
```php
|
|
133
|
+
// ✅ Use Service for multi-step operations
|
|
134
|
+
class OrderController extends Controller
|
|
135
|
+
{
|
|
136
|
+
public function __construct(
|
|
137
|
+
private OrderService $orderService
|
|
138
|
+
) {}
|
|
139
|
+
|
|
140
|
+
public function store(OrderRequest $request): OrderResource
|
|
141
|
+
{
|
|
142
|
+
$order = $this->orderService->checkout(
|
|
143
|
+
cart: Cart::find($request->cart_id),
|
|
144
|
+
user: $request->user(),
|
|
145
|
+
paymentMethod: $request->payment_method
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return new OrderResource($order);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Service handles complex logic
|
|
153
|
+
class OrderService
|
|
154
|
+
{
|
|
155
|
+
public function checkout(Cart $cart, User $user, string $paymentMethod): Order
|
|
156
|
+
{
|
|
157
|
+
// 1. Validate cart items in stock
|
|
158
|
+
$this->validateInventory($cart);
|
|
159
|
+
|
|
160
|
+
// 2. Calculate totals
|
|
161
|
+
$totals = $this->calculateTotals($cart);
|
|
162
|
+
|
|
163
|
+
// 3. Process payment
|
|
164
|
+
$payment = $this->paymentService->charge($user, $totals, $paymentMethod);
|
|
165
|
+
|
|
166
|
+
// 4. Create order
|
|
167
|
+
$order = Order::create([...]);
|
|
168
|
+
|
|
169
|
+
// 5. Update inventory
|
|
170
|
+
$this->updateInventory($cart);
|
|
171
|
+
|
|
172
|
+
// 6. Send notifications
|
|
173
|
+
event(new OrderPlaced($order));
|
|
174
|
+
|
|
175
|
+
return $order;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### 4. Single Reusable Operation → Action
|
|
181
|
+
|
|
182
|
+
```php
|
|
183
|
+
// ✅ Use Action for reusable single-purpose operations
|
|
184
|
+
class CreateInvoiceAction
|
|
185
|
+
{
|
|
186
|
+
public function execute(Order $order): Invoice
|
|
187
|
+
{
|
|
188
|
+
$invoice = Invoice::create([
|
|
189
|
+
'order_id' => $order->id,
|
|
190
|
+
'number' => $this->generateNumber(),
|
|
191
|
+
'total' => $order->total,
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
GenerateInvoicePdf::dispatch($invoice);
|
|
195
|
+
|
|
196
|
+
return $invoice;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Used in multiple places
|
|
201
|
+
class OrderController
|
|
202
|
+
{
|
|
203
|
+
public function store(OrderRequest $request, CreateInvoiceAction $createInvoice)
|
|
204
|
+
{
|
|
205
|
+
$order = Order::create($request->validated());
|
|
206
|
+
$createInvoice->execute($order);
|
|
207
|
+
return new OrderResource($order);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
class RecurringBillingJob
|
|
212
|
+
{
|
|
213
|
+
public function handle(CreateInvoiceAction $createInvoice)
|
|
214
|
+
{
|
|
215
|
+
foreach ($this->getSubscriptions() as $subscription) {
|
|
216
|
+
$order = $subscription->createRenewalOrder();
|
|
217
|
+
$createInvoice->execute($order);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Why NOT Repository Pattern?
|
|
226
|
+
|
|
227
|
+
### Problem: Eloquent IS Already a Repository
|
|
228
|
+
|
|
229
|
+
```php
|
|
230
|
+
// Eloquent provides repository-like methods
|
|
231
|
+
User::find($id);
|
|
232
|
+
User::where('email', $email)->first();
|
|
233
|
+
User::create($data);
|
|
234
|
+
$user->update($data);
|
|
235
|
+
$user->delete();
|
|
236
|
+
|
|
237
|
+
// Adding Repository layer = duplicate abstraction
|
|
238
|
+
interface UserRepositoryInterface {
|
|
239
|
+
public function find(int $id): ?User;
|
|
240
|
+
public function findByEmail(string $email): ?User;
|
|
241
|
+
public function create(array $data): User;
|
|
242
|
+
// ... same methods as Eloquent!
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### When Repository MIGHT Make Sense
|
|
247
|
+
|
|
248
|
+
```php
|
|
249
|
+
// Only if you genuinely need to swap data sources
|
|
250
|
+
// (rare in real projects)
|
|
251
|
+
|
|
252
|
+
interface ProductCatalogInterface {
|
|
253
|
+
public function search(string $query): Collection;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
class EloquentProductCatalog implements ProductCatalogInterface { ... }
|
|
257
|
+
class ElasticsearchProductCatalog implements ProductCatalogInterface { ... }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Reality**: 99% of Laravel projects never swap databases. Don't add abstraction for hypothetical future needs.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Design Principles
|
|
265
|
+
|
|
266
|
+
### 1. YAGNI (You Aren't Gonna Need It)
|
|
267
|
+
|
|
268
|
+
```php
|
|
269
|
+
// ❌ DON'T: Create interfaces "just in case"
|
|
270
|
+
interface UserServiceInterface { ... }
|
|
271
|
+
class UserService implements UserServiceInterface { ... }
|
|
272
|
+
|
|
273
|
+
// ✅ DO: Add abstraction only when needed
|
|
274
|
+
class UserService { ... } // Concrete class is fine
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### 2. Single Responsibility
|
|
278
|
+
|
|
279
|
+
```php
|
|
280
|
+
// ❌ DON'T: Fat controller
|
|
281
|
+
class UserController
|
|
282
|
+
{
|
|
283
|
+
public function store(Request $request)
|
|
284
|
+
{
|
|
285
|
+
$validated = $request->validate([...]); // Validation in controller
|
|
286
|
+
$user = User::create($validated);
|
|
287
|
+
Mail::send(...); // Email logic in controller
|
|
288
|
+
$this->calculatePoints($user); // Business logic in controller
|
|
289
|
+
return response()->json($user); // Manual JSON formatting
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ✅ DO: Each layer has one job
|
|
294
|
+
class UserController
|
|
295
|
+
{
|
|
296
|
+
public function store(UserStoreRequest $request): UserResource // Type-hinted
|
|
297
|
+
{
|
|
298
|
+
$user = User::create($request->validated()); // FormRequest validates
|
|
299
|
+
event(new UserRegistered($user)); // Event handles side effects
|
|
300
|
+
return new UserResource($user); // Resource formats output
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### 3. Explicit Over Implicit
|
|
306
|
+
|
|
307
|
+
```php
|
|
308
|
+
// ❌ DON'T: Magic methods, hidden behavior
|
|
309
|
+
class User extends Model
|
|
310
|
+
{
|
|
311
|
+
public function __call($method, $args) { ... } // Magic
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ✅ DO: Clear, readable code
|
|
315
|
+
class User extends Model
|
|
316
|
+
{
|
|
317
|
+
public function orders(): HasMany { ... } // Explicit relationship
|
|
318
|
+
public function scopeActive($query) { ... } // Clear scope
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### 4. Fail Fast
|
|
323
|
+
|
|
324
|
+
```php
|
|
325
|
+
// ✅ Validate early with FormRequest
|
|
326
|
+
class OrderStoreRequest extends FormRequest
|
|
327
|
+
{
|
|
328
|
+
public function rules(): array
|
|
329
|
+
{
|
|
330
|
+
return [
|
|
331
|
+
'cart_id' => ['required', 'exists:carts,id'],
|
|
332
|
+
'payment_method' => ['required', 'in:card,bank'],
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ✅ Throw exceptions for invalid states
|
|
338
|
+
class OrderService
|
|
339
|
+
{
|
|
340
|
+
public function checkout(Cart $cart): Order
|
|
341
|
+
{
|
|
342
|
+
if ($cart->items->isEmpty()) {
|
|
343
|
+
throw new EmptyCartException();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!$this->hasInventory($cart)) {
|
|
347
|
+
throw new InsufficientInventoryException();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ... proceed with valid cart
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Anti-Patterns to Avoid
|
|
358
|
+
|
|
359
|
+
### 1. ❌ Repository for Everything
|
|
360
|
+
|
|
361
|
+
```php
|
|
362
|
+
// DON'T
|
|
363
|
+
class UserRepository {
|
|
364
|
+
public function all() { return User::all(); }
|
|
365
|
+
public function find($id) { return User::find($id); }
|
|
366
|
+
// Pointless wrapper around Eloquent
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 2. ❌ Service for Simple CRUD
|
|
371
|
+
|
|
372
|
+
```php
|
|
373
|
+
// DON'T
|
|
374
|
+
class UserService {
|
|
375
|
+
public function create(array $data) {
|
|
376
|
+
return User::create($data); // Just wrapping model method
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 3. ❌ DTOs for Request Data
|
|
382
|
+
|
|
383
|
+
```php
|
|
384
|
+
// DON'T
|
|
385
|
+
class UserDTO {
|
|
386
|
+
public function __construct(
|
|
387
|
+
public string $name,
|
|
388
|
+
public string $email,
|
|
389
|
+
) {}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// FormRequest already does this!
|
|
393
|
+
$request->validated(); // Returns validated array
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 4. ❌ Interfaces Without Implementations
|
|
397
|
+
|
|
398
|
+
```php
|
|
399
|
+
// DON'T create interface for single implementation
|
|
400
|
+
interface UserServiceInterface { ... }
|
|
401
|
+
class UserService implements UserServiceInterface { ... }
|
|
402
|
+
|
|
403
|
+
// DO use interface only when you have multiple implementations
|
|
404
|
+
interface PaymentGatewayInterface { ... }
|
|
405
|
+
class StripePaymentGateway implements PaymentGatewayInterface { ... }
|
|
406
|
+
class PayPalPaymentGateway implements PaymentGatewayInterface { ... }
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Summary
|
|
412
|
+
|
|
413
|
+
| Principle | Guideline |
|
|
414
|
+
| ------------------- | --------------------------------- |
|
|
415
|
+
| **Simple CRUD** | Controller + Model + Resource |
|
|
416
|
+
| **Side effects** | Events, Jobs, Observers |
|
|
417
|
+
| **Complex logic** | Service (only when truly complex) |
|
|
418
|
+
| **Reusable action** | Action class |
|
|
419
|
+
| **Validation** | FormRequest (always) |
|
|
420
|
+
| **Response format** | Resource (always) |
|
|
421
|
+
| **Repository** | ❌ Don't use (Eloquent is enough) |
|
|
422
|
+
| **Interfaces** | Only for multiple implementations |
|
|
423
|
+
|
|
424
|
+
**Golden Rule**: Start simple. Add layers only when complexity demands it.
|