@famgia/omnify-laravel 0.0.88 → 0.0.90
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-2QSKZS63.js} +188 -12
- package/dist/chunk-2QSKZS63.js.map +1 -0
- package/dist/index.cjs +190 -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 +186 -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/README.md.stub +95 -0
- 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/react-form-guide.md.stub +259 -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/cursor/omnify.mdc.stub +58 -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,334 @@
|
|
|
1
|
+
# DateTime Handling Guide
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md) | [Resource Guide](./resource-guide.md)
|
|
4
|
+
|
|
5
|
+
## Golden Rule: "Store UTC, Respond UTC, Accept UTC"
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Database (UTC) ← Carbon (UTC) → API Response (ISO 8601 UTC)
|
|
9
|
+
↑
|
|
10
|
+
API Request (UTC)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
### 1. Set Application Timezone to UTC
|
|
18
|
+
|
|
19
|
+
**`config/app.php`:**
|
|
20
|
+
|
|
21
|
+
```php
|
|
22
|
+
'timezone' => 'UTC',
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> ⚠️ NEVER change this to local timezone. Always use UTC.
|
|
26
|
+
|
|
27
|
+
### 2. Database Timezone
|
|
28
|
+
|
|
29
|
+
**MySQL** - Ensure UTC:
|
|
30
|
+
|
|
31
|
+
```sql
|
|
32
|
+
SET GLOBAL time_zone = '+00:00';
|
|
33
|
+
SET time_zone = '+00:00';
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or in `my.cnf`:
|
|
37
|
+
```ini
|
|
38
|
+
[mysqld]
|
|
39
|
+
default-time-zone = '+00:00'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Carbon Usage Rules
|
|
45
|
+
|
|
46
|
+
### Always Use Carbon (Never raw DateTime)
|
|
47
|
+
|
|
48
|
+
```php
|
|
49
|
+
use Illuminate\Support\Carbon;
|
|
50
|
+
|
|
51
|
+
// ✅ Correct
|
|
52
|
+
$now = Carbon::now(); // Current UTC time
|
|
53
|
+
$date = Carbon::parse($input); // Parse with UTC
|
|
54
|
+
|
|
55
|
+
// ❌ Wrong
|
|
56
|
+
$now = new \DateTime(); // Don't use raw DateTime
|
|
57
|
+
$now = date('Y-m-d H:i:s'); // Don't use date()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### API Response Format
|
|
61
|
+
|
|
62
|
+
**Always return ISO 8601 with Z suffix:**
|
|
63
|
+
|
|
64
|
+
```php
|
|
65
|
+
// In Model - cast dates properly
|
|
66
|
+
protected $casts = [
|
|
67
|
+
'email_verified_at' => 'datetime',
|
|
68
|
+
'scheduled_at' => 'datetime',
|
|
69
|
+
'created_at' => 'datetime',
|
|
70
|
+
'updated_at' => 'datetime',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// In Resource - format as ISO 8601 UTC
|
|
74
|
+
public function toArray($request): array
|
|
75
|
+
{
|
|
76
|
+
return [
|
|
77
|
+
'id' => $this->id,
|
|
78
|
+
'title' => $this->title,
|
|
79
|
+
'scheduled_at' => $this->scheduled_at?->toISOString(), // "2024-01-15T10:30:00.000000Z"
|
|
80
|
+
'created_at' => $this->created_at?->toISOString(),
|
|
81
|
+
'updated_at' => $this->updated_at?->toISOString(),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Accepting Date Input
|
|
87
|
+
|
|
88
|
+
```php
|
|
89
|
+
// In FormRequest
|
|
90
|
+
public function rules(): array
|
|
91
|
+
{
|
|
92
|
+
return [
|
|
93
|
+
'scheduled_at' => ['required', 'date'], // Accepts ISO 8601
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// In Controller - Carbon auto-parses UTC
|
|
98
|
+
public function store(StoreEventRequest $request): JsonResponse
|
|
99
|
+
{
|
|
100
|
+
$event = Event::create([
|
|
101
|
+
'title' => $request->title,
|
|
102
|
+
'scheduled_at' => Carbon::parse($request->scheduled_at), // Already UTC
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
return new EventResource($event);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Common Patterns
|
|
112
|
+
|
|
113
|
+
### Compare Dates
|
|
114
|
+
|
|
115
|
+
```php
|
|
116
|
+
use Illuminate\Support\Carbon;
|
|
117
|
+
|
|
118
|
+
// Current UTC time
|
|
119
|
+
$now = Carbon::now();
|
|
120
|
+
|
|
121
|
+
// Check if past
|
|
122
|
+
if ($event->scheduled_at->isPast()) {
|
|
123
|
+
// Event has passed
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check if within range
|
|
127
|
+
$start = Carbon::parse($request->start_date);
|
|
128
|
+
$end = Carbon::parse($request->end_date);
|
|
129
|
+
|
|
130
|
+
Event::whereBetween('scheduled_at', [$start, $end])->get();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Query by Date Range
|
|
134
|
+
|
|
135
|
+
```php
|
|
136
|
+
// Frontend sends UTC strings
|
|
137
|
+
// "2024-01-01T00:00:00Z" to "2024-01-31T23:59:59Z"
|
|
138
|
+
|
|
139
|
+
public function index(Request $request)
|
|
140
|
+
{
|
|
141
|
+
$query = Event::query();
|
|
142
|
+
|
|
143
|
+
if ($request->filled('start_date')) {
|
|
144
|
+
$query->where('scheduled_at', '>=', Carbon::parse($request->start_date));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if ($request->filled('end_date')) {
|
|
148
|
+
$query->where('scheduled_at', '<=', Carbon::parse($request->end_date));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return EventResource::collection($query->paginate());
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Display for Specific Timezone (if needed)
|
|
156
|
+
|
|
157
|
+
```php
|
|
158
|
+
// Only when generating reports for specific timezone
|
|
159
|
+
$userTimezone = 'Asia/Tokyo';
|
|
160
|
+
|
|
161
|
+
$localTime = $event->scheduled_at->setTimezone($userTimezone);
|
|
162
|
+
// Keep original as UTC, create copy for display
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Model Setup
|
|
168
|
+
|
|
169
|
+
### Recommended Model Structure
|
|
170
|
+
|
|
171
|
+
```php
|
|
172
|
+
<?php
|
|
173
|
+
|
|
174
|
+
namespace App\Models;
|
|
175
|
+
|
|
176
|
+
use Illuminate\Database\Eloquent\Model;
|
|
177
|
+
use Illuminate\Support\Carbon;
|
|
178
|
+
|
|
179
|
+
class Event extends Model
|
|
180
|
+
{
|
|
181
|
+
protected $fillable = [
|
|
182
|
+
'title',
|
|
183
|
+
'scheduled_at',
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
protected $casts = [
|
|
187
|
+
'scheduled_at' => 'datetime',
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
// Accessor for formatted date (if needed internally)
|
|
191
|
+
public function getScheduledAtFormattedAttribute(): string
|
|
192
|
+
{
|
|
193
|
+
return $this->scheduled_at?->toISOString() ?? '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Scope for upcoming events
|
|
197
|
+
public function scopeUpcoming($query)
|
|
198
|
+
{
|
|
199
|
+
return $query->where('scheduled_at', '>', Carbon::now());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Scope for past events
|
|
203
|
+
public function scopePast($query)
|
|
204
|
+
{
|
|
205
|
+
return $query->where('scheduled_at', '<', Carbon::now());
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Resource Format
|
|
211
|
+
|
|
212
|
+
```php
|
|
213
|
+
<?php
|
|
214
|
+
|
|
215
|
+
namespace App\Http\Resources;
|
|
216
|
+
|
|
217
|
+
use Illuminate\Http\Resources\Json\JsonResource;
|
|
218
|
+
|
|
219
|
+
class EventResource extends JsonResource
|
|
220
|
+
{
|
|
221
|
+
public function toArray($request): array
|
|
222
|
+
{
|
|
223
|
+
return [
|
|
224
|
+
'id' => $this->id,
|
|
225
|
+
'title' => $this->title,
|
|
226
|
+
'scheduled_at' => $this->scheduled_at?->toISOString(),
|
|
227
|
+
'is_past' => $this->scheduled_at?->isPast() ?? false,
|
|
228
|
+
'created_at' => $this->created_at?->toISOString(),
|
|
229
|
+
'updated_at' => $this->updated_at?->toISOString(),
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Migration Example
|
|
238
|
+
|
|
239
|
+
```php
|
|
240
|
+
Schema::create('events', function (Blueprint $table) {
|
|
241
|
+
$table->id();
|
|
242
|
+
$table->string('title');
|
|
243
|
+
$table->timestamp('scheduled_at'); // Use timestamp, not datetime
|
|
244
|
+
$table->timestamps(); // created_at, updated_at
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
> **Note:** `timestamp` columns in MySQL are stored as UTC internally.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Anti-Patterns ❌
|
|
253
|
+
|
|
254
|
+
```php
|
|
255
|
+
// ❌ DON'T: Set local timezone in config
|
|
256
|
+
'timezone' => 'Asia/Tokyo', // Wrong!
|
|
257
|
+
|
|
258
|
+
// ❌ DON'T: Use raw PHP date functions
|
|
259
|
+
$date = date('Y-m-d H:i:s');
|
|
260
|
+
$date = new \DateTime();
|
|
261
|
+
|
|
262
|
+
// ❌ DON'T: Return formatted local time in API
|
|
263
|
+
return ['created_at' => $this->created_at->format('Y/m/d H:i')];
|
|
264
|
+
|
|
265
|
+
// ❌ DON'T: Store timezone offset in database
|
|
266
|
+
$event->scheduled_at = '2024-01-15 19:30:00+09:00';
|
|
267
|
+
|
|
268
|
+
// ❌ DON'T: Convert to local timezone before storing
|
|
269
|
+
$event->scheduled_at = Carbon::parse($input)->setTimezone('Asia/Tokyo');
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Correct Patterns ✅
|
|
273
|
+
|
|
274
|
+
```php
|
|
275
|
+
// ✅ DO: Keep UTC timezone
|
|
276
|
+
'timezone' => 'UTC',
|
|
277
|
+
|
|
278
|
+
// ✅ DO: Use Carbon everywhere
|
|
279
|
+
$now = Carbon::now();
|
|
280
|
+
$date = Carbon::parse($input);
|
|
281
|
+
|
|
282
|
+
// ✅ DO: Return ISO 8601 UTC in API
|
|
283
|
+
return ['created_at' => $this->created_at?->toISOString()];
|
|
284
|
+
|
|
285
|
+
// ✅ DO: Store as UTC
|
|
286
|
+
$event->scheduled_at = Carbon::parse($input); // Input should be UTC from frontend
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## API Contract with Frontend
|
|
292
|
+
|
|
293
|
+
| Direction | Format | Example |
|
|
294
|
+
| -------------- | ------------- | ------------------------------- |
|
|
295
|
+
| API → Frontend | ISO 8601 UTC | `"2024-01-15T10:30:00.000000Z"` |
|
|
296
|
+
| Frontend → API | ISO 8601 UTC | `"2024-01-15T10:30:00.000Z"` |
|
|
297
|
+
| Database | UTC Timestamp | `2024-01-15 10:30:00` |
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Testing with Dates
|
|
302
|
+
|
|
303
|
+
```php
|
|
304
|
+
use Illuminate\Support\Carbon;
|
|
305
|
+
|
|
306
|
+
public function test_creates_event_with_correct_date(): void
|
|
307
|
+
{
|
|
308
|
+
Carbon::setTestNow('2024-01-15 10:00:00'); // Freeze time in UTC
|
|
309
|
+
|
|
310
|
+
$response = $this->postJson('/api/events', [
|
|
311
|
+
'title' => 'Test Event',
|
|
312
|
+
'scheduled_at' => '2024-01-20T15:00:00.000Z',
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
$response->assertCreated();
|
|
316
|
+
|
|
317
|
+
$this->assertDatabaseHas('events', [
|
|
318
|
+
'title' => 'Test Event',
|
|
319
|
+
'scheduled_at' => '2024-01-20 15:00:00', // Stored as UTC
|
|
320
|
+
]);
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Checklist
|
|
327
|
+
|
|
328
|
+
- [ ] `config/app.php` timezone is `UTC`
|
|
329
|
+
- [ ] MySQL timezone is UTC
|
|
330
|
+
- [ ] All models use `datetime` cast for date fields
|
|
331
|
+
- [ ] All Resources return `->toISOString()` for dates
|
|
332
|
+
- [ ] Never use raw `date()` or `DateTime` - always Carbon
|
|
333
|
+
- [ ] Never convert to local timezone before storing
|
|
334
|
+
- [ ] API documentation specifies UTC format
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# OpenAPI Documentation Guide
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md) | [Controller Guide](./controller-guide.md) | [Checklist](./checklist.md)
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This project uses [L5-Swagger](https://github.com/DarkaOnLine/L5-Swagger) with PHP 8 Attributes.
|
|
8
|
+
|
|
9
|
+
**Key files:**
|
|
10
|
+
- `app/OpenApi/Schemas.php` - Reusable components (parameters, responses)
|
|
11
|
+
- `app/Http/Controllers/*Controller.php` - Endpoint documentation
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
./artisan l5-swagger:generate # Generate OpenAPI JSON
|
|
17
|
+
# View at: https://api.{folder}.app/api/documentation
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
app/OpenApi/
|
|
26
|
+
└── Schemas.php ← Define reusable components HERE
|
|
27
|
+
|
|
28
|
+
app/Http/Controllers/
|
|
29
|
+
└── UserController.php ← Use $ref to reference components
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Step 1: Define Reusable Components
|
|
35
|
+
|
|
36
|
+
**File:** `app/OpenApi/Schemas.php`
|
|
37
|
+
|
|
38
|
+
```php
|
|
39
|
+
<?php
|
|
40
|
+
|
|
41
|
+
namespace App\OpenApi;
|
|
42
|
+
|
|
43
|
+
use OpenApi\Attributes as OA;
|
|
44
|
+
|
|
45
|
+
#[OA\Info(
|
|
46
|
+
version: '1.0.0',
|
|
47
|
+
title: 'My API',
|
|
48
|
+
description: 'API documentation'
|
|
49
|
+
)]
|
|
50
|
+
#[OA\Server(url: '/api', description: 'API Server')]
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// COMMON PARAMETERS
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
#[OA\Parameter(
|
|
57
|
+
parameter: 'QuerySearch',
|
|
58
|
+
name: 'search',
|
|
59
|
+
in: 'query',
|
|
60
|
+
description: 'Search term',
|
|
61
|
+
schema: new OA\Schema(type: 'string')
|
|
62
|
+
)]
|
|
63
|
+
#[OA\Parameter(
|
|
64
|
+
parameter: 'QueryPage',
|
|
65
|
+
name: 'page',
|
|
66
|
+
in: 'query',
|
|
67
|
+
description: 'Page number',
|
|
68
|
+
schema: new OA\Schema(type: 'integer', default: 1, minimum: 1)
|
|
69
|
+
)]
|
|
70
|
+
#[OA\Parameter(
|
|
71
|
+
parameter: 'QueryPerPage',
|
|
72
|
+
name: 'per_page',
|
|
73
|
+
in: 'query',
|
|
74
|
+
description: 'Items per page',
|
|
75
|
+
schema: new OA\Schema(type: 'integer', default: 10, minimum: 1, maximum: 100)
|
|
76
|
+
)]
|
|
77
|
+
#[OA\Parameter(
|
|
78
|
+
parameter: 'QuerySortBy',
|
|
79
|
+
name: 'sort_by',
|
|
80
|
+
in: 'query',
|
|
81
|
+
description: 'Sort field',
|
|
82
|
+
schema: new OA\Schema(type: 'string', default: 'id')
|
|
83
|
+
)]
|
|
84
|
+
#[OA\Parameter(
|
|
85
|
+
parameter: 'QuerySortOrder',
|
|
86
|
+
name: 'sort_order',
|
|
87
|
+
in: 'query',
|
|
88
|
+
description: 'Sort direction',
|
|
89
|
+
schema: new OA\Schema(type: 'string', enum: ['asc', 'desc'], default: 'desc')
|
|
90
|
+
)]
|
|
91
|
+
#[OA\Parameter(
|
|
92
|
+
parameter: 'PathId',
|
|
93
|
+
name: 'id',
|
|
94
|
+
in: 'path',
|
|
95
|
+
required: true,
|
|
96
|
+
description: 'Resource ID',
|
|
97
|
+
schema: new OA\Schema(type: 'integer', minimum: 1)
|
|
98
|
+
)]
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// COMMON RESPONSES
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
#[OA\Response(response: 'Success', description: 'Successful operation')]
|
|
105
|
+
#[OA\Response(response: 'Created', description: 'Resource created successfully')]
|
|
106
|
+
#[OA\Response(response: 'NoContent', description: 'Successfully deleted')]
|
|
107
|
+
#[OA\Response(response: 'NotFound', description: 'Resource not found')]
|
|
108
|
+
#[OA\Response(response: 'Unauthorized', description: 'Unauthenticated')]
|
|
109
|
+
#[OA\Response(response: 'Forbidden', description: 'Forbidden')]
|
|
110
|
+
#[OA\Response(
|
|
111
|
+
response: 'ValidationError',
|
|
112
|
+
description: 'Validation failed',
|
|
113
|
+
content: new OA\JsonContent(
|
|
114
|
+
properties: [
|
|
115
|
+
new OA\Property(property: 'message', type: 'string', example: 'The given data was invalid.'),
|
|
116
|
+
new OA\Property(property: 'errors', type: 'object', example: ['email' => ['The email has already been taken.']]),
|
|
117
|
+
]
|
|
118
|
+
)
|
|
119
|
+
)]
|
|
120
|
+
class Schemas
|
|
121
|
+
{
|
|
122
|
+
// This class exists only to hold OpenAPI attributes
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Step 2: Use $ref in Controllers
|
|
129
|
+
|
|
130
|
+
### Index (GET list)
|
|
131
|
+
|
|
132
|
+
```php
|
|
133
|
+
#[OA\Get(
|
|
134
|
+
path: '/api/users',
|
|
135
|
+
summary: 'List users',
|
|
136
|
+
description: 'Paginated list with search and sorting',
|
|
137
|
+
tags: ['Users'],
|
|
138
|
+
parameters: [
|
|
139
|
+
new OA\Parameter(ref: '#/components/parameters/QuerySearch'),
|
|
140
|
+
new OA\Parameter(ref: '#/components/parameters/QueryPage'),
|
|
141
|
+
new OA\Parameter(ref: '#/components/parameters/QueryPerPage'),
|
|
142
|
+
new OA\Parameter(ref: '#/components/parameters/QuerySortBy'),
|
|
143
|
+
new OA\Parameter(ref: '#/components/parameters/QuerySortOrder'),
|
|
144
|
+
],
|
|
145
|
+
responses: [
|
|
146
|
+
new OA\Response(ref: '#/components/responses/Success', response: 200),
|
|
147
|
+
]
|
|
148
|
+
)]
|
|
149
|
+
public function index(Request $request): AnonymousResourceCollection
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Store (POST)
|
|
153
|
+
|
|
154
|
+
```php
|
|
155
|
+
#[OA\Post(
|
|
156
|
+
path: '/api/users',
|
|
157
|
+
summary: 'Create user',
|
|
158
|
+
description: 'Create a new user account',
|
|
159
|
+
tags: ['Users'],
|
|
160
|
+
requestBody: new OA\RequestBody(
|
|
161
|
+
required: true,
|
|
162
|
+
content: new OA\JsonContent(
|
|
163
|
+
required: ['name_lastname', 'name_firstname', 'email', 'password'],
|
|
164
|
+
properties: [
|
|
165
|
+
new OA\Property(property: 'name_lastname', type: 'string', maxLength: 50, example: '田中'),
|
|
166
|
+
new OA\Property(property: 'name_firstname', type: 'string', maxLength: 50, example: '太郎'),
|
|
167
|
+
new OA\Property(property: 'email', type: 'string', format: 'email', example: 'tanaka@example.com'),
|
|
168
|
+
new OA\Property(property: 'password', type: 'string', format: 'password', minLength: 8),
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
),
|
|
172
|
+
responses: [
|
|
173
|
+
new OA\Response(ref: '#/components/responses/Created', response: 201),
|
|
174
|
+
new OA\Response(ref: '#/components/responses/ValidationError', response: 422),
|
|
175
|
+
]
|
|
176
|
+
)]
|
|
177
|
+
public function store(UserStoreRequest $request): UserResource
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Show (GET single)
|
|
181
|
+
|
|
182
|
+
```php
|
|
183
|
+
#[OA\Get(
|
|
184
|
+
path: '/api/users/{id}',
|
|
185
|
+
summary: 'Get user',
|
|
186
|
+
description: 'Get user by ID',
|
|
187
|
+
tags: ['Users'],
|
|
188
|
+
parameters: [
|
|
189
|
+
new OA\Parameter(ref: '#/components/parameters/PathId'),
|
|
190
|
+
],
|
|
191
|
+
responses: [
|
|
192
|
+
new OA\Response(ref: '#/components/responses/Success', response: 200),
|
|
193
|
+
new OA\Response(ref: '#/components/responses/NotFound', response: 404),
|
|
194
|
+
]
|
|
195
|
+
)]
|
|
196
|
+
public function show(User $user): UserResource
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Update (PUT)
|
|
200
|
+
|
|
201
|
+
```php
|
|
202
|
+
#[OA\Put(
|
|
203
|
+
path: '/api/users/{id}',
|
|
204
|
+
summary: 'Update user',
|
|
205
|
+
description: 'Update user (partial update supported)',
|
|
206
|
+
tags: ['Users'],
|
|
207
|
+
parameters: [
|
|
208
|
+
new OA\Parameter(ref: '#/components/parameters/PathId'),
|
|
209
|
+
],
|
|
210
|
+
requestBody: new OA\RequestBody(
|
|
211
|
+
content: new OA\JsonContent(
|
|
212
|
+
properties: [
|
|
213
|
+
new OA\Property(property: 'name_lastname', type: 'string', maxLength: 50),
|
|
214
|
+
new OA\Property(property: 'name_firstname', type: 'string', maxLength: 50),
|
|
215
|
+
new OA\Property(property: 'email', type: 'string', format: 'email'),
|
|
216
|
+
new OA\Property(property: 'password', type: 'string', format: 'password', minLength: 8),
|
|
217
|
+
]
|
|
218
|
+
)
|
|
219
|
+
),
|
|
220
|
+
responses: [
|
|
221
|
+
new OA\Response(ref: '#/components/responses/Success', response: 200),
|
|
222
|
+
new OA\Response(ref: '#/components/responses/NotFound', response: 404),
|
|
223
|
+
new OA\Response(ref: '#/components/responses/ValidationError', response: 422),
|
|
224
|
+
]
|
|
225
|
+
)]
|
|
226
|
+
public function update(UserUpdateRequest $request, User $user): UserResource
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Destroy (DELETE)
|
|
230
|
+
|
|
231
|
+
```php
|
|
232
|
+
#[OA\Delete(
|
|
233
|
+
path: '/api/users/{id}',
|
|
234
|
+
summary: 'Delete user',
|
|
235
|
+
description: 'Permanently delete user',
|
|
236
|
+
tags: ['Users'],
|
|
237
|
+
parameters: [
|
|
238
|
+
new OA\Parameter(ref: '#/components/parameters/PathId'),
|
|
239
|
+
],
|
|
240
|
+
responses: [
|
|
241
|
+
new OA\Response(ref: '#/components/responses/NoContent', response: 204),
|
|
242
|
+
new OA\Response(ref: '#/components/responses/NotFound', response: 404),
|
|
243
|
+
]
|
|
244
|
+
)]
|
|
245
|
+
public function destroy(User $user): JsonResponse
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Available Components
|
|
251
|
+
|
|
252
|
+
### Parameters (use with `ref: '#/components/parameters/...'`)
|
|
253
|
+
|
|
254
|
+
| Name | Description |
|
|
255
|
+
| ---------------- | -------------------------------------- |
|
|
256
|
+
| `QuerySearch` | Search term |
|
|
257
|
+
| `QueryPage` | Page number (default: 1) |
|
|
258
|
+
| `QueryPerPage` | Items per page (default: 10, max: 100) |
|
|
259
|
+
| `QuerySortBy` | Sort field (default: id) |
|
|
260
|
+
| `QuerySortOrder` | Sort direction (asc/desc) |
|
|
261
|
+
| `PathId` | Resource ID in path |
|
|
262
|
+
|
|
263
|
+
### Responses (use with `ref: '#/components/responses/...'`)
|
|
264
|
+
|
|
265
|
+
| Name | HTTP Code | Description |
|
|
266
|
+
| ----------------- | --------- | -------------------- |
|
|
267
|
+
| `Success` | 200 | Successful operation |
|
|
268
|
+
| `Created` | 201 | Resource created |
|
|
269
|
+
| `NoContent` | 204 | Successfully deleted |
|
|
270
|
+
| `NotFound` | 404 | Resource not found |
|
|
271
|
+
| `ValidationError` | 422 | Validation failed |
|
|
272
|
+
| `Unauthorized` | 401 | Unauthenticated |
|
|
273
|
+
| `Forbidden` | 403 | Forbidden |
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## ⚠️ Important: Verify Fields Before Writing
|
|
278
|
+
|
|
279
|
+
**DO NOT make up fields!** Check these files first:
|
|
280
|
+
|
|
281
|
+
| What to Document | Check This File |
|
|
282
|
+
| ------------------- | ------------------------------------------------------------------- |
|
|
283
|
+
| Request body fields | `app/Http/Requests/OmnifyBase/*RequestBase.php` → `schemaRules()` |
|
|
284
|
+
| Response fields | `app/Http/Resources/OmnifyBase/*ResourceBase.php` → `schemaArray()` |
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Adding New Components
|
|
289
|
+
|
|
290
|
+
### New Parameter
|
|
291
|
+
|
|
292
|
+
```php
|
|
293
|
+
// In app/OpenApi/Schemas.php
|
|
294
|
+
#[OA\Parameter(
|
|
295
|
+
parameter: 'QueryStatus', // Unique name
|
|
296
|
+
name: 'status', // Query param name
|
|
297
|
+
in: 'query',
|
|
298
|
+
description: 'Filter by status',
|
|
299
|
+
schema: new OA\Schema(type: 'string', enum: ['active', 'inactive'])
|
|
300
|
+
)]
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### New Response
|
|
304
|
+
|
|
305
|
+
```php
|
|
306
|
+
// In app/OpenApi/Schemas.php
|
|
307
|
+
#[OA\Response(
|
|
308
|
+
response: 'PaymentRequired',
|
|
309
|
+
description: 'Payment required'
|
|
310
|
+
)]
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Checklist
|
|
316
|
+
|
|
317
|
+
### Before Writing
|
|
318
|
+
|
|
319
|
+
- [ ] Check `OmnifyBase/*RequestBase.php` for request fields
|
|
320
|
+
- [ ] Check `OmnifyBase/*ResourceBase.php` for response fields
|
|
321
|
+
- [ ] DON'T make up fields that don't exist!
|
|
322
|
+
|
|
323
|
+
### Writing OpenAPI
|
|
324
|
+
|
|
325
|
+
- [ ] Add `#[OA\Tag]` to controller class
|
|
326
|
+
- [ ] Use `$ref` for common parameters (QuerySearch, QueryPage, etc.)
|
|
327
|
+
- [ ] Use `$ref` for common responses (Success, NotFound, etc.)
|
|
328
|
+
- [ ] Only write `requestBody` properties manually (match FormRequest)
|
|
329
|
+
- [ ] Use Japanese examples for JapaneseName fields
|
|
330
|
+
|
|
331
|
+
### After Writing
|
|
332
|
+
|
|
333
|
+
- [ ] Run `./artisan l5-swagger:generate`
|
|
334
|
+
- [ ] Verify at `/api/documentation`
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Anti-Patterns
|
|
339
|
+
|
|
340
|
+
```php
|
|
341
|
+
// ❌ BAD: Repeating common parameters
|
|
342
|
+
parameters: [
|
|
343
|
+
new OA\Parameter(name: 'page', in: 'query', schema: new OA\Schema(type: 'integer')),
|
|
344
|
+
new OA\Parameter(name: 'per_page', in: 'query', schema: new OA\Schema(type: 'integer')),
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
// ✅ GOOD: Use $ref
|
|
348
|
+
parameters: [
|
|
349
|
+
new OA\Parameter(ref: '#/components/parameters/QueryPage'),
|
|
350
|
+
new OA\Parameter(ref: '#/components/parameters/QueryPerPage'),
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
// ❌ BAD: Repeating response definitions
|
|
354
|
+
responses: [
|
|
355
|
+
new OA\Response(response: 404, description: 'Resource not found'),
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
// ✅ GOOD: Use $ref
|
|
359
|
+
responses: [
|
|
360
|
+
new OA\Response(ref: '#/components/responses/NotFound', response: 404),
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
// ❌ BAD: Making up fields
|
|
364
|
+
new OA\Property(property: 'username', ...) // Does this exist?
|
|
365
|
+
|
|
366
|
+
// ✅ GOOD: Check OmnifyBase first, then write
|
|
367
|
+
// Checked: OmnifyBase/UserStoreRequestBase.php has name_lastname, name_firstname...
|
|
368
|
+
new OA\Property(property: 'name_lastname', type: 'string', example: '田中'),
|
|
369
|
+
```
|