omgkit 2.2.0 → 2.3.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/package.json +1 -1
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,81 +1,45 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: laravel
|
|
3
|
-
description:
|
|
4
|
-
category: frameworks
|
|
5
|
-
triggers:
|
|
6
|
-
- laravel
|
|
7
|
-
- php framework
|
|
8
|
-
- eloquent
|
|
9
|
-
- blade
|
|
10
|
-
- artisan
|
|
11
|
-
- php api
|
|
12
|
-
- laravel api
|
|
13
|
-
- lumen
|
|
2
|
+
name: building-laravel-apis
|
|
3
|
+
description: Builds enterprise Laravel applications with Eloquent, API Resources, Sanctum auth, and queue processing. Use when creating PHP backends, REST APIs, or full-stack Laravel applications.
|
|
14
4
|
---
|
|
15
5
|
|
|
16
6
|
# Laravel
|
|
17
7
|
|
|
18
|
-
|
|
8
|
+
## Quick Start
|
|
19
9
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
```php
|
|
11
|
+
// routes/api.php
|
|
12
|
+
Route::get('/health', fn () => ['status' => 'ok']);
|
|
23
13
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- Write comprehensive tests with PHPUnit
|
|
29
|
-
- Deploy production-ready applications
|
|
30
|
-
- Leverage queues for background processing
|
|
14
|
+
Route::middleware('auth:sanctum')->group(function () {
|
|
15
|
+
Route::apiResource('users', UserController::class);
|
|
16
|
+
});
|
|
17
|
+
```
|
|
31
18
|
|
|
32
19
|
## Features
|
|
33
20
|
|
|
34
|
-
|
|
21
|
+
| Feature | Description | Guide |
|
|
22
|
+
|---------|-------------|-------|
|
|
23
|
+
| Models | Eloquent ORM, relationships, scopes | [MODELS.md](MODELS.md) |
|
|
24
|
+
| Controllers | Resource controllers, form requests | [CONTROLLERS.md](CONTROLLERS.md) |
|
|
25
|
+
| API Resources | Response transformation | [RESOURCES.md](RESOURCES.md) |
|
|
26
|
+
| Auth | Sanctum, policies, gates | [AUTH.md](AUTH.md) |
|
|
27
|
+
| Queues | Jobs, events, listeners | [QUEUES.md](QUEUES.md) |
|
|
28
|
+
| Testing | Feature, unit tests | [TESTING.md](TESTING.md) |
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
<?php
|
|
38
|
-
// app/Models/User.php
|
|
39
|
-
namespace App\Models;
|
|
30
|
+
## Common Patterns
|
|
40
31
|
|
|
41
|
-
|
|
42
|
-
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
43
|
-
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
44
|
-
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
45
|
-
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
46
|
-
use Illuminate\Foundation\Auth\User as Authenticatable;
|
|
47
|
-
use Illuminate\Notifications\Notifiable;
|
|
48
|
-
use Laravel\Sanctum\HasApiTokens;
|
|
32
|
+
### Model with Relationships
|
|
49
33
|
|
|
34
|
+
```php
|
|
50
35
|
class User extends Authenticatable
|
|
51
36
|
{
|
|
52
|
-
use HasApiTokens, HasFactory,
|
|
53
|
-
|
|
54
|
-
protected $fillable = [
|
|
55
|
-
'name',
|
|
56
|
-
'email',
|
|
57
|
-
'password',
|
|
58
|
-
'role',
|
|
59
|
-
'is_active',
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
protected $hidden = [
|
|
63
|
-
'password',
|
|
64
|
-
'remember_token',
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
protected $casts = [
|
|
68
|
-
'email_verified_at' => 'datetime',
|
|
69
|
-
'password' => 'hashed',
|
|
70
|
-
'is_active' => 'boolean',
|
|
71
|
-
];
|
|
37
|
+
use HasApiTokens, HasFactory, SoftDeletes;
|
|
72
38
|
|
|
73
|
-
protected $
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
];
|
|
39
|
+
protected $fillable = ['name', 'email', 'password', 'role'];
|
|
40
|
+
protected $hidden = ['password', 'remember_token'];
|
|
41
|
+
protected $casts = ['email_verified_at' => 'datetime', 'password' => 'hashed'];
|
|
77
42
|
|
|
78
|
-
// Relationships
|
|
79
43
|
public function organizations(): BelongsToMany
|
|
80
44
|
{
|
|
81
45
|
return $this->belongsToMany(Organization::class, 'memberships')
|
|
@@ -83,1196 +47,132 @@ class User extends Authenticatable
|
|
|
83
47
|
->withTimestamps();
|
|
84
48
|
}
|
|
85
49
|
|
|
86
|
-
public function ownedOrganizations(): HasMany
|
|
87
|
-
{
|
|
88
|
-
return $this->hasMany(Organization::class, 'owner_id');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
public function projects(): HasMany
|
|
92
|
-
{
|
|
93
|
-
return $this->hasMany(Project::class, 'created_by');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Scopes
|
|
97
50
|
public function scopeActive($query)
|
|
98
51
|
{
|
|
99
52
|
return $query->where('is_active', true);
|
|
100
53
|
}
|
|
101
54
|
|
|
102
|
-
public function scopeRole($query, string $role)
|
|
103
|
-
{
|
|
104
|
-
return $query->where('role', $role);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
55
|
public function scopeSearch($query, ?string $search)
|
|
108
56
|
{
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return $query->where(function ($q) use ($search) {
|
|
114
|
-
$q->where('name', 'like', "%{$search}%")
|
|
115
|
-
->orWhere('email', 'like', "%{$search}%");
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Accessors & Mutators
|
|
120
|
-
protected function name(): Attribute
|
|
121
|
-
{
|
|
122
|
-
return Attribute::make(
|
|
123
|
-
get: fn (string $value) => ucwords($value),
|
|
124
|
-
set: fn (string $value) => strtolower($value),
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Methods
|
|
129
|
-
public function isAdmin(): bool
|
|
130
|
-
{
|
|
131
|
-
return $this->role === 'admin';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
public function belongsToOrganization(Organization $organization): bool
|
|
135
|
-
{
|
|
136
|
-
return $this->organizations()->where('organizations.id', $organization->id)->exists();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// app/Models/Organization.php
|
|
142
|
-
namespace App\Models;
|
|
143
|
-
|
|
144
|
-
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
|
145
|
-
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
146
|
-
use Illuminate\Database\Eloquent\Model;
|
|
147
|
-
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
148
|
-
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
149
|
-
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
150
|
-
|
|
151
|
-
class Organization extends Model
|
|
152
|
-
{
|
|
153
|
-
use HasFactory, HasUuids;
|
|
154
|
-
|
|
155
|
-
protected $fillable = [
|
|
156
|
-
'name',
|
|
157
|
-
'slug',
|
|
158
|
-
'owner_id',
|
|
159
|
-
];
|
|
160
|
-
|
|
161
|
-
// Relationships
|
|
162
|
-
public function owner(): BelongsTo
|
|
163
|
-
{
|
|
164
|
-
return $this->belongsTo(User::class, 'owner_id');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
public function members(): BelongsToMany
|
|
168
|
-
{
|
|
169
|
-
return $this->belongsToMany(User::class, 'memberships')
|
|
170
|
-
->withPivot('role')
|
|
171
|
-
->withTimestamps();
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
public function projects(): HasMany
|
|
175
|
-
{
|
|
176
|
-
return $this->hasMany(Project::class);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Scopes
|
|
180
|
-
public function scopeForUser($query, User $user)
|
|
181
|
-
{
|
|
182
|
-
return $query->whereHas('members', fn ($q) => $q->where('users.id', $user->id));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// app/Models/Project.php
|
|
188
|
-
namespace App\Models;
|
|
189
|
-
|
|
190
|
-
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
|
191
|
-
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
192
|
-
use Illuminate\Database\Eloquent\Model;
|
|
193
|
-
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
194
|
-
|
|
195
|
-
class Project extends Model
|
|
196
|
-
{
|
|
197
|
-
use HasFactory, HasUuids, SoftDeletes;
|
|
198
|
-
|
|
199
|
-
protected $fillable = [
|
|
200
|
-
'organization_id',
|
|
201
|
-
'name',
|
|
202
|
-
'description',
|
|
203
|
-
'status',
|
|
204
|
-
'created_by',
|
|
205
|
-
];
|
|
206
|
-
|
|
207
|
-
protected $casts = [
|
|
208
|
-
'status' => ProjectStatus::class,
|
|
209
|
-
];
|
|
210
|
-
|
|
211
|
-
protected $attributes = [
|
|
212
|
-
'status' => ProjectStatus::DRAFT,
|
|
213
|
-
];
|
|
214
|
-
|
|
215
|
-
// Relationships
|
|
216
|
-
public function organization()
|
|
217
|
-
{
|
|
218
|
-
return $this->belongsTo(Organization::class);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
public function creator()
|
|
222
|
-
{
|
|
223
|
-
return $this->belongsTo(User::class, 'created_by');
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
public function tasks()
|
|
227
|
-
{
|
|
228
|
-
return $this->hasMany(Task::class);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Scopes
|
|
232
|
-
public function scopeActive($query)
|
|
233
|
-
{
|
|
234
|
-
return $query->where('status', ProjectStatus::ACTIVE);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
public function scopeForOrganization($query, $organizationId)
|
|
238
|
-
{
|
|
239
|
-
return $query->where('organization_id', $organizationId);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// app/Enums/ProjectStatus.php
|
|
245
|
-
namespace App\Enums;
|
|
246
|
-
|
|
247
|
-
enum ProjectStatus: string
|
|
248
|
-
{
|
|
249
|
-
case DRAFT = 'draft';
|
|
250
|
-
case ACTIVE = 'active';
|
|
251
|
-
case COMPLETED = 'completed';
|
|
252
|
-
case ARCHIVED = 'archived';
|
|
253
|
-
|
|
254
|
-
public function label(): string
|
|
255
|
-
{
|
|
256
|
-
return match ($this) {
|
|
257
|
-
self::DRAFT => 'Draft',
|
|
258
|
-
self::ACTIVE => 'Active',
|
|
259
|
-
self::COMPLETED => 'Completed',
|
|
260
|
-
self::ARCHIVED => 'Archived',
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
### 2. API Resources and Collections
|
|
267
|
-
|
|
268
|
-
```php
|
|
269
|
-
<?php
|
|
270
|
-
// app/Http/Resources/UserResource.php
|
|
271
|
-
namespace App\Http\Resources;
|
|
272
|
-
|
|
273
|
-
use Illuminate\Http\Request;
|
|
274
|
-
use Illuminate\Http\Resources\Json\JsonResource;
|
|
275
|
-
|
|
276
|
-
class UserResource extends JsonResource
|
|
277
|
-
{
|
|
278
|
-
public function toArray(Request $request): array
|
|
279
|
-
{
|
|
280
|
-
return [
|
|
281
|
-
'id' => $this->id,
|
|
282
|
-
'name' => $this->name,
|
|
283
|
-
'email' => $this->email,
|
|
284
|
-
'role' => $this->role,
|
|
285
|
-
'is_active' => $this->is_active,
|
|
286
|
-
'email_verified_at' => $this->email_verified_at?->toIso8601String(),
|
|
287
|
-
'created_at' => $this->created_at->toIso8601String(),
|
|
288
|
-
'updated_at' => $this->updated_at->toIso8601String(),
|
|
289
|
-
|
|
290
|
-
// Conditional relationships
|
|
291
|
-
'organizations' => OrganizationResource::collection(
|
|
292
|
-
$this->whenLoaded('organizations')
|
|
293
|
-
),
|
|
294
|
-
'organization_count' => $this->when(
|
|
295
|
-
$this->organizations_count !== null,
|
|
296
|
-
$this->organizations_count
|
|
297
|
-
),
|
|
298
|
-
];
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
// app/Http/Resources/OrganizationResource.php
|
|
304
|
-
namespace App\Http\Resources;
|
|
305
|
-
|
|
306
|
-
use Illuminate\Http\Request;
|
|
307
|
-
use Illuminate\Http\Resources\Json\JsonResource;
|
|
308
|
-
|
|
309
|
-
class OrganizationResource extends JsonResource
|
|
310
|
-
{
|
|
311
|
-
public function toArray(Request $request): array
|
|
312
|
-
{
|
|
313
|
-
return [
|
|
314
|
-
'id' => $this->id,
|
|
315
|
-
'name' => $this->name,
|
|
316
|
-
'slug' => $this->slug,
|
|
317
|
-
'owner' => new UserResource($this->whenLoaded('owner')),
|
|
318
|
-
'member_count' => $this->when(
|
|
319
|
-
$this->members_count !== null,
|
|
320
|
-
$this->members_count
|
|
321
|
-
),
|
|
322
|
-
'created_at' => $this->created_at->toIso8601String(),
|
|
323
|
-
|
|
324
|
-
// Pivot data when loaded through relationship
|
|
325
|
-
'membership' => $this->when($this->pivot, [
|
|
326
|
-
'role' => $this->pivot?->role,
|
|
327
|
-
'joined_at' => $this->pivot?->created_at?->toIso8601String(),
|
|
328
|
-
]),
|
|
329
|
-
];
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
// app/Http/Resources/ProjectResource.php
|
|
335
|
-
namespace App\Http\Resources;
|
|
336
|
-
|
|
337
|
-
use Illuminate\Http\Request;
|
|
338
|
-
use Illuminate\Http\Resources\Json\JsonResource;
|
|
339
|
-
|
|
340
|
-
class ProjectResource extends JsonResource
|
|
341
|
-
{
|
|
342
|
-
public function toArray(Request $request): array
|
|
343
|
-
{
|
|
344
|
-
return [
|
|
345
|
-
'id' => $this->id,
|
|
346
|
-
'name' => $this->name,
|
|
347
|
-
'description' => $this->description,
|
|
348
|
-
'status' => [
|
|
349
|
-
'value' => $this->status->value,
|
|
350
|
-
'label' => $this->status->label(),
|
|
351
|
-
],
|
|
352
|
-
'organization' => new OrganizationResource($this->whenLoaded('organization')),
|
|
353
|
-
'creator' => new UserResource($this->whenLoaded('creator')),
|
|
354
|
-
'task_count' => $this->when(
|
|
355
|
-
$this->tasks_count !== null,
|
|
356
|
-
$this->tasks_count
|
|
357
|
-
),
|
|
358
|
-
'created_at' => $this->created_at->toIso8601String(),
|
|
359
|
-
'updated_at' => $this->updated_at->toIso8601String(),
|
|
360
|
-
];
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
// app/Http/Resources/PaginatedCollection.php
|
|
366
|
-
namespace App\Http\Resources;
|
|
367
|
-
|
|
368
|
-
use Illuminate\Http\Request;
|
|
369
|
-
use Illuminate\Http\Resources\Json\ResourceCollection;
|
|
370
|
-
|
|
371
|
-
class PaginatedCollection extends ResourceCollection
|
|
372
|
-
{
|
|
373
|
-
protected string $resourceClass;
|
|
374
|
-
|
|
375
|
-
public function __construct($resource, string $resourceClass)
|
|
376
|
-
{
|
|
377
|
-
parent::__construct($resource);
|
|
378
|
-
$this->resourceClass = $resourceClass;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
public function toArray(Request $request): array
|
|
382
|
-
{
|
|
383
|
-
return [
|
|
384
|
-
'data' => $this->resourceClass::collection($this->collection),
|
|
385
|
-
'pagination' => [
|
|
386
|
-
'current_page' => $this->currentPage(),
|
|
387
|
-
'per_page' => $this->perPage(),
|
|
388
|
-
'total' => $this->total(),
|
|
389
|
-
'total_pages' => $this->lastPage(),
|
|
390
|
-
'has_more' => $this->hasMorePages(),
|
|
391
|
-
],
|
|
392
|
-
];
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
### 3. Form Requests and Validation
|
|
398
|
-
|
|
399
|
-
```php
|
|
400
|
-
<?php
|
|
401
|
-
// app/Http/Requests/User/CreateUserRequest.php
|
|
402
|
-
namespace App\Http\Requests\User;
|
|
403
|
-
|
|
404
|
-
use Illuminate\Foundation\Http\FormRequest;
|
|
405
|
-
use Illuminate\Validation\Rules\Password;
|
|
406
|
-
|
|
407
|
-
class CreateUserRequest extends FormRequest
|
|
408
|
-
{
|
|
409
|
-
public function authorize(): bool
|
|
410
|
-
{
|
|
411
|
-
return $this->user()->isAdmin();
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
public function rules(): array
|
|
415
|
-
{
|
|
416
|
-
return [
|
|
417
|
-
'name' => ['required', 'string', 'min:2', 'max:100'],
|
|
418
|
-
'email' => ['required', 'email', 'unique:users,email'],
|
|
419
|
-
'password' => [
|
|
420
|
-
'required',
|
|
421
|
-
'confirmed',
|
|
422
|
-
Password::min(8)
|
|
423
|
-
->mixedCase()
|
|
424
|
-
->numbers()
|
|
425
|
-
->symbols(),
|
|
426
|
-
],
|
|
427
|
-
'role' => ['sometimes', 'string', 'in:admin,user,guest'],
|
|
428
|
-
];
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
public function messages(): array
|
|
432
|
-
{
|
|
433
|
-
return [
|
|
434
|
-
'email.unique' => 'This email is already registered.',
|
|
435
|
-
'password.confirmed' => 'Password confirmation does not match.',
|
|
436
|
-
];
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
// app/Http/Requests/User/UpdateUserRequest.php
|
|
442
|
-
namespace App\Http\Requests\User;
|
|
443
|
-
|
|
444
|
-
use Illuminate\Foundation\Http\FormRequest;
|
|
445
|
-
use Illuminate\Validation\Rule;
|
|
446
|
-
|
|
447
|
-
class UpdateUserRequest extends FormRequest
|
|
448
|
-
{
|
|
449
|
-
public function authorize(): bool
|
|
450
|
-
{
|
|
451
|
-
return $this->user()->isAdmin() || $this->user()->id === $this->route('user')->id;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
public function rules(): array
|
|
455
|
-
{
|
|
456
|
-
return [
|
|
457
|
-
'name' => ['sometimes', 'string', 'min:2', 'max:100'],
|
|
458
|
-
'email' => [
|
|
459
|
-
'sometimes',
|
|
460
|
-
'email',
|
|
461
|
-
Rule::unique('users')->ignore($this->route('user')),
|
|
462
|
-
],
|
|
463
|
-
'is_active' => ['sometimes', 'boolean'],
|
|
464
|
-
];
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
// app/Http/Requests/Organization/CreateOrganizationRequest.php
|
|
470
|
-
namespace App\Http\Requests\Organization;
|
|
471
|
-
|
|
472
|
-
use Illuminate\Foundation\Http\FormRequest;
|
|
473
|
-
|
|
474
|
-
class CreateOrganizationRequest extends FormRequest
|
|
475
|
-
{
|
|
476
|
-
public function authorize(): bool
|
|
477
|
-
{
|
|
478
|
-
return true;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
public function rules(): array
|
|
482
|
-
{
|
|
483
|
-
return [
|
|
484
|
-
'name' => ['required', 'string', 'min:2', 'max:255'],
|
|
485
|
-
'slug' => [
|
|
486
|
-
'required',
|
|
487
|
-
'string',
|
|
488
|
-
'min:2',
|
|
489
|
-
'max:100',
|
|
490
|
-
'regex:/^[a-z0-9-]+$/',
|
|
491
|
-
'unique:organizations,slug',
|
|
492
|
-
],
|
|
493
|
-
];
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
public function messages(): array
|
|
497
|
-
{
|
|
498
|
-
return [
|
|
499
|
-
'slug.regex' => 'Slug must contain only lowercase letters, numbers, and hyphens.',
|
|
500
|
-
'slug.unique' => 'This slug is already taken.',
|
|
501
|
-
];
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
// app/Http/Requests/Project/CreateProjectRequest.php
|
|
507
|
-
namespace App\Http\Requests\Project;
|
|
508
|
-
|
|
509
|
-
use App\Enums\ProjectStatus;
|
|
510
|
-
use Illuminate\Foundation\Http\FormRequest;
|
|
511
|
-
use Illuminate\Validation\Rule;
|
|
512
|
-
|
|
513
|
-
class CreateProjectRequest extends FormRequest
|
|
514
|
-
{
|
|
515
|
-
public function authorize(): bool
|
|
516
|
-
{
|
|
517
|
-
$organization = $this->route('organization');
|
|
518
|
-
return $this->user()->belongsToOrganization($organization);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
public function rules(): array
|
|
522
|
-
{
|
|
523
|
-
return [
|
|
524
|
-
'name' => [
|
|
525
|
-
'required',
|
|
526
|
-
'string',
|
|
527
|
-
'min:1',
|
|
528
|
-
'max:255',
|
|
529
|
-
Rule::unique('projects')
|
|
530
|
-
->where('organization_id', $this->route('organization')->id),
|
|
531
|
-
],
|
|
532
|
-
'description' => ['nullable', 'string', 'max:5000'],
|
|
533
|
-
'status' => ['sometimes', Rule::enum(ProjectStatus::class)],
|
|
534
|
-
];
|
|
57
|
+
return $search
|
|
58
|
+
? $query->where('name', 'like', "%{$search}%")
|
|
59
|
+
->orWhere('email', 'like', "%{$search}%")
|
|
60
|
+
: $query;
|
|
535
61
|
}
|
|
536
62
|
}
|
|
537
63
|
```
|
|
538
64
|
|
|
539
|
-
###
|
|
65
|
+
### Controller with Service
|
|
540
66
|
|
|
541
67
|
```php
|
|
542
|
-
<?php
|
|
543
|
-
// app/Http/Controllers/Api/UserController.php
|
|
544
|
-
namespace App\Http\Controllers\Api;
|
|
545
|
-
|
|
546
|
-
use App\Http\Controllers\Controller;
|
|
547
|
-
use App\Http\Requests\User\CreateUserRequest;
|
|
548
|
-
use App\Http\Requests\User\UpdateUserRequest;
|
|
549
|
-
use App\Http\Resources\PaginatedCollection;
|
|
550
|
-
use App\Http\Resources\UserResource;
|
|
551
|
-
use App\Models\User;
|
|
552
|
-
use App\Services\UserService;
|
|
553
|
-
use Illuminate\Http\JsonResponse;
|
|
554
|
-
use Illuminate\Http\Request;
|
|
555
|
-
use Illuminate\Http\Response;
|
|
556
|
-
|
|
557
68
|
class UserController extends Controller
|
|
558
69
|
{
|
|
559
|
-
public function __construct(
|
|
560
|
-
private readonly UserService $userService
|
|
561
|
-
) {}
|
|
70
|
+
public function __construct(private UserService $userService) {}
|
|
562
71
|
|
|
563
72
|
public function index(Request $request): PaginatedCollection
|
|
564
73
|
{
|
|
565
74
|
$users = $this->userService->list(
|
|
566
75
|
search: $request->input('search'),
|
|
567
|
-
role: $request->input('role'),
|
|
568
76
|
perPage: $request->input('per_page', 20)
|
|
569
77
|
);
|
|
570
|
-
|
|
571
78
|
return new PaginatedCollection($users, UserResource::class);
|
|
572
79
|
}
|
|
573
80
|
|
|
574
|
-
public function show(User $user): UserResource
|
|
575
|
-
{
|
|
576
|
-
return new UserResource(
|
|
577
|
-
$user->load('organizations')
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
81
|
public function store(CreateUserRequest $request): JsonResponse
|
|
582
82
|
{
|
|
583
83
|
$user = $this->userService->create($request->validated());
|
|
584
|
-
|
|
585
84
|
return (new UserResource($user))
|
|
586
85
|
->response()
|
|
587
|
-
->setStatusCode(
|
|
86
|
+
->setStatusCode(201);
|
|
588
87
|
}
|
|
589
88
|
|
|
590
|
-
public function
|
|
591
|
-
{
|
|
592
|
-
$user = $this->userService->update($user, $request->validated());
|
|
593
|
-
|
|
594
|
-
return new UserResource($user);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
public function destroy(User $user): Response
|
|
598
|
-
{
|
|
599
|
-
$this->userService->delete($user);
|
|
600
|
-
|
|
601
|
-
return response()->noContent();
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
public function me(Request $request): UserResource
|
|
605
|
-
{
|
|
606
|
-
return new UserResource(
|
|
607
|
-
$request->user()->load('organizations')
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
// app/Http/Controllers/Api/AuthController.php
|
|
614
|
-
namespace App\Http\Controllers\Api;
|
|
615
|
-
|
|
616
|
-
use App\Http\Controllers\Controller;
|
|
617
|
-
use App\Http\Requests\Auth\LoginRequest;
|
|
618
|
-
use App\Http\Requests\Auth\RegisterRequest;
|
|
619
|
-
use App\Http\Resources\UserResource;
|
|
620
|
-
use App\Services\AuthService;
|
|
621
|
-
use Illuminate\Http\JsonResponse;
|
|
622
|
-
use Illuminate\Http\Request;
|
|
623
|
-
use Illuminate\Http\Response;
|
|
624
|
-
|
|
625
|
-
class AuthController extends Controller
|
|
626
|
-
{
|
|
627
|
-
public function __construct(
|
|
628
|
-
private readonly AuthService $authService
|
|
629
|
-
) {}
|
|
630
|
-
|
|
631
|
-
public function register(RegisterRequest $request): JsonResponse
|
|
632
|
-
{
|
|
633
|
-
$result = $this->authService->register($request->validated());
|
|
634
|
-
|
|
635
|
-
return response()->json([
|
|
636
|
-
'user' => new UserResource($result['user']),
|
|
637
|
-
'token' => $result['token'],
|
|
638
|
-
], Response::HTTP_CREATED);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
public function login(LoginRequest $request): JsonResponse
|
|
642
|
-
{
|
|
643
|
-
$result = $this->authService->login(
|
|
644
|
-
$request->input('email'),
|
|
645
|
-
$request->input('password')
|
|
646
|
-
);
|
|
647
|
-
|
|
648
|
-
if (!$result) {
|
|
649
|
-
return response()->json([
|
|
650
|
-
'message' => 'Invalid credentials',
|
|
651
|
-
], Response::HTTP_UNAUTHORIZED);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
return response()->json([
|
|
655
|
-
'user' => new UserResource($result['user']),
|
|
656
|
-
'token' => $result['token'],
|
|
657
|
-
]);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
public function logout(Request $request): Response
|
|
661
|
-
{
|
|
662
|
-
$request->user()->currentAccessToken()->delete();
|
|
663
|
-
|
|
664
|
-
return response()->noContent();
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
public function refresh(Request $request): JsonResponse
|
|
668
|
-
{
|
|
669
|
-
$token = $this->authService->refreshToken($request->user());
|
|
670
|
-
|
|
671
|
-
return response()->json([
|
|
672
|
-
'token' => $token,
|
|
673
|
-
]);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
// app/Http/Controllers/Api/ProjectController.php
|
|
679
|
-
namespace App\Http\Controllers\Api;
|
|
680
|
-
|
|
681
|
-
use App\Http\Controllers\Controller;
|
|
682
|
-
use App\Http\Requests\Project\CreateProjectRequest;
|
|
683
|
-
use App\Http\Requests\Project\UpdateProjectRequest;
|
|
684
|
-
use App\Http\Resources\PaginatedCollection;
|
|
685
|
-
use App\Http\Resources\ProjectResource;
|
|
686
|
-
use App\Models\Organization;
|
|
687
|
-
use App\Models\Project;
|
|
688
|
-
use App\Services\ProjectService;
|
|
689
|
-
use Illuminate\Http\JsonResponse;
|
|
690
|
-
use Illuminate\Http\Request;
|
|
691
|
-
use Illuminate\Http\Response;
|
|
692
|
-
|
|
693
|
-
class ProjectController extends Controller
|
|
694
|
-
{
|
|
695
|
-
public function __construct(
|
|
696
|
-
private readonly ProjectService $projectService
|
|
697
|
-
) {}
|
|
698
|
-
|
|
699
|
-
public function index(Request $request, Organization $organization): PaginatedCollection
|
|
700
|
-
{
|
|
701
|
-
$projects = $this->projectService->listForOrganization(
|
|
702
|
-
$organization,
|
|
703
|
-
status: $request->input('status'),
|
|
704
|
-
perPage: $request->input('per_page', 20)
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
return new PaginatedCollection($projects, ProjectResource::class);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
public function store(CreateProjectRequest $request, Organization $organization): JsonResponse
|
|
711
|
-
{
|
|
712
|
-
$project = $this->projectService->create(
|
|
713
|
-
$organization,
|
|
714
|
-
$request->user(),
|
|
715
|
-
$request->validated()
|
|
716
|
-
);
|
|
717
|
-
|
|
718
|
-
return (new ProjectResource($project))
|
|
719
|
-
->response()
|
|
720
|
-
->setStatusCode(Response::HTTP_CREATED);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
public function show(Organization $organization, Project $project): ProjectResource
|
|
724
|
-
{
|
|
725
|
-
return new ProjectResource(
|
|
726
|
-
$project->load(['organization', 'creator'])
|
|
727
|
-
);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
public function update(
|
|
731
|
-
UpdateProjectRequest $request,
|
|
732
|
-
Organization $organization,
|
|
733
|
-
Project $project
|
|
734
|
-
): ProjectResource {
|
|
735
|
-
$project = $this->projectService->update($project, $request->validated());
|
|
736
|
-
|
|
737
|
-
return new ProjectResource($project);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
public function destroy(Organization $organization, Project $project): Response
|
|
89
|
+
public function show(User $user): UserResource
|
|
741
90
|
{
|
|
742
|
-
$
|
|
743
|
-
|
|
744
|
-
return response()->noContent();
|
|
91
|
+
return new UserResource($user->load('organizations'));
|
|
745
92
|
}
|
|
746
93
|
}
|
|
747
94
|
```
|
|
748
95
|
|
|
749
|
-
###
|
|
96
|
+
### Form Request Validation
|
|
750
97
|
|
|
751
98
|
```php
|
|
752
|
-
|
|
753
|
-
// app/Services/UserService.php
|
|
754
|
-
namespace App\Services;
|
|
755
|
-
|
|
756
|
-
use App\Models\User;
|
|
757
|
-
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
758
|
-
use Illuminate\Support\Facades\Hash;
|
|
759
|
-
|
|
760
|
-
class UserService
|
|
761
|
-
{
|
|
762
|
-
public function list(
|
|
763
|
-
?string $search = null,
|
|
764
|
-
?string $role = null,
|
|
765
|
-
int $perPage = 20
|
|
766
|
-
): LengthAwarePaginator {
|
|
767
|
-
return User::query()
|
|
768
|
-
->active()
|
|
769
|
-
->search($search)
|
|
770
|
-
->when($role, fn ($q) => $q->role($role))
|
|
771
|
-
->withCount('organizations')
|
|
772
|
-
->orderByDesc('created_at')
|
|
773
|
-
->paginate($perPage);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
public function create(array $data): User
|
|
777
|
-
{
|
|
778
|
-
return User::create([
|
|
779
|
-
'name' => $data['name'],
|
|
780
|
-
'email' => $data['email'],
|
|
781
|
-
'password' => Hash::make($data['password']),
|
|
782
|
-
'role' => $data['role'] ?? 'user',
|
|
783
|
-
]);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
public function update(User $user, array $data): User
|
|
787
|
-
{
|
|
788
|
-
$user->update($data);
|
|
789
|
-
|
|
790
|
-
return $user->fresh();
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
public function delete(User $user): void
|
|
794
|
-
{
|
|
795
|
-
$user->delete(); // Soft delete
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
public function findByEmail(string $email): ?User
|
|
799
|
-
{
|
|
800
|
-
return User::where('email', $email)->first();
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
// app/Services/AuthService.php
|
|
806
|
-
namespace App\Services;
|
|
807
|
-
|
|
808
|
-
use App\Models\User;
|
|
809
|
-
use Illuminate\Support\Facades\Hash;
|
|
810
|
-
|
|
811
|
-
class AuthService
|
|
99
|
+
class CreateUserRequest extends FormRequest
|
|
812
100
|
{
|
|
813
|
-
public function
|
|
814
|
-
private readonly UserService $userService
|
|
815
|
-
) {}
|
|
816
|
-
|
|
817
|
-
public function register(array $data): array
|
|
101
|
+
public function authorize(): bool
|
|
818
102
|
{
|
|
819
|
-
|
|
820
|
-
$token = $user->createToken('auth-token')->plainTextToken;
|
|
821
|
-
|
|
822
|
-
return [
|
|
823
|
-
'user' => $user,
|
|
824
|
-
'token' => $token,
|
|
825
|
-
];
|
|
103
|
+
return $this->user()->isAdmin();
|
|
826
104
|
}
|
|
827
105
|
|
|
828
|
-
public function
|
|
106
|
+
public function rules(): array
|
|
829
107
|
{
|
|
830
|
-
$user = $this->userService->findByEmail($email);
|
|
831
|
-
|
|
832
|
-
if (!$user || !Hash::check($password, $user->password)) {
|
|
833
|
-
return null;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
if (!$user->is_active) {
|
|
837
|
-
return null;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// Revoke existing tokens
|
|
841
|
-
$user->tokens()->delete();
|
|
842
|
-
|
|
843
|
-
$token = $user->createToken('auth-token')->plainTextToken;
|
|
844
|
-
|
|
845
108
|
return [
|
|
846
|
-
'
|
|
847
|
-
'
|
|
109
|
+
'name' => ['required', 'string', 'min:2', 'max:100'],
|
|
110
|
+
'email' => ['required', 'email', 'unique:users,email'],
|
|
111
|
+
'password' => ['required', 'confirmed', Password::min(8)->mixedCase()->numbers()],
|
|
112
|
+
'role' => ['sometimes', 'in:admin,user,guest'],
|
|
848
113
|
];
|
|
849
114
|
}
|
|
850
|
-
|
|
851
|
-
public function refreshToken(User $user): string
|
|
852
|
-
{
|
|
853
|
-
$user->currentAccessToken()->delete();
|
|
854
|
-
|
|
855
|
-
return $user->createToken('auth-token')->plainTextToken;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
// app/Services/ProjectService.php
|
|
861
|
-
namespace App\Services;
|
|
862
|
-
|
|
863
|
-
use App\Enums\ProjectStatus;
|
|
864
|
-
use App\Models\Organization;
|
|
865
|
-
use App\Models\Project;
|
|
866
|
-
use App\Models\User;
|
|
867
|
-
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
868
|
-
|
|
869
|
-
class ProjectService
|
|
870
|
-
{
|
|
871
|
-
public function listForOrganization(
|
|
872
|
-
Organization $organization,
|
|
873
|
-
?string $status = null,
|
|
874
|
-
int $perPage = 20
|
|
875
|
-
): LengthAwarePaginator {
|
|
876
|
-
return Project::query()
|
|
877
|
-
->forOrganization($organization->id)
|
|
878
|
-
->when($status, fn ($q) => $q->where('status', $status))
|
|
879
|
-
->with(['creator'])
|
|
880
|
-
->withCount('tasks')
|
|
881
|
-
->orderByDesc('created_at')
|
|
882
|
-
->paginate($perPage);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
public function create(
|
|
886
|
-
Organization $organization,
|
|
887
|
-
User $creator,
|
|
888
|
-
array $data
|
|
889
|
-
): Project {
|
|
890
|
-
return Project::create([
|
|
891
|
-
'organization_id' => $organization->id,
|
|
892
|
-
'created_by' => $creator->id,
|
|
893
|
-
'name' => $data['name'],
|
|
894
|
-
'description' => $data['description'] ?? null,
|
|
895
|
-
'status' => $data['status'] ?? ProjectStatus::DRAFT,
|
|
896
|
-
]);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
public function update(Project $project, array $data): Project
|
|
900
|
-
{
|
|
901
|
-
$project->update($data);
|
|
902
|
-
|
|
903
|
-
return $project->fresh();
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
public function delete(Project $project): void
|
|
907
|
-
{
|
|
908
|
-
$project->delete(); // Soft delete
|
|
909
|
-
}
|
|
910
115
|
}
|
|
911
116
|
```
|
|
912
117
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
```php
|
|
916
|
-
<?php
|
|
917
|
-
// app/Http/Middleware/EnsureOrganizationMember.php
|
|
918
|
-
namespace App\Http\Middleware;
|
|
919
|
-
|
|
920
|
-
use App\Models\Organization;
|
|
921
|
-
use Closure;
|
|
922
|
-
use Illuminate\Http\Request;
|
|
923
|
-
use Symfony\Component\HttpFoundation\Response;
|
|
118
|
+
## Workflows
|
|
924
119
|
|
|
925
|
-
|
|
926
|
-
{
|
|
927
|
-
public function handle(Request $request, Closure $next): Response
|
|
928
|
-
{
|
|
929
|
-
$organization = $request->route('organization');
|
|
930
|
-
|
|
931
|
-
if (!$organization instanceof Organization) {
|
|
932
|
-
abort(404, 'Organization not found');
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
if (!$request->user()->belongsToOrganization($organization)) {
|
|
936
|
-
abort(403, 'You are not a member of this organization');
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
return $next($request);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
120
|
+
### API Development
|
|
942
121
|
|
|
122
|
+
1. Create model and migration
|
|
123
|
+
2. Create controller with `php artisan make:controller --api`
|
|
124
|
+
3. Add Form Request for validation
|
|
125
|
+
4. Create API Resource for responses
|
|
126
|
+
5. Write feature tests
|
|
943
127
|
|
|
944
|
-
|
|
945
|
-
namespace App\Policies;
|
|
946
|
-
|
|
947
|
-
use App\Models\Project;
|
|
948
|
-
use App\Models\User;
|
|
949
|
-
|
|
950
|
-
class ProjectPolicy
|
|
951
|
-
{
|
|
952
|
-
public function viewAny(User $user): bool
|
|
953
|
-
{
|
|
954
|
-
return true;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
public function view(User $user, Project $project): bool
|
|
958
|
-
{
|
|
959
|
-
return $user->belongsToOrganization($project->organization);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
public function create(User $user): bool
|
|
963
|
-
{
|
|
964
|
-
return true;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
public function update(User $user, Project $project): bool
|
|
968
|
-
{
|
|
969
|
-
if ($user->isAdmin()) {
|
|
970
|
-
return true;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
return $user->id === $project->created_by;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
public function delete(User $user, Project $project): bool
|
|
977
|
-
{
|
|
978
|
-
if ($user->isAdmin()) {
|
|
979
|
-
return true;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
return $user->id === $project->created_by;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
```
|
|
986
|
-
|
|
987
|
-
### 7. Testing Patterns
|
|
128
|
+
### Resource Response
|
|
988
129
|
|
|
989
130
|
```php
|
|
990
|
-
|
|
991
|
-
// tests/Feature/UserTest.php
|
|
992
|
-
namespace Tests\Feature;
|
|
993
|
-
|
|
994
|
-
use App\Models\User;
|
|
995
|
-
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
996
|
-
use Laravel\Sanctum\Sanctum;
|
|
997
|
-
use Tests\TestCase;
|
|
998
|
-
|
|
999
|
-
class UserTest extends TestCase
|
|
1000
|
-
{
|
|
1001
|
-
use RefreshDatabase;
|
|
1002
|
-
|
|
1003
|
-
public function test_admin_can_list_users(): void
|
|
1004
|
-
{
|
|
1005
|
-
$admin = User::factory()->create(['role' => 'admin']);
|
|
1006
|
-
User::factory()->count(5)->create();
|
|
1007
|
-
|
|
1008
|
-
Sanctum::actingAs($admin);
|
|
1009
|
-
|
|
1010
|
-
$response = $this->getJson('/api/users');
|
|
1011
|
-
|
|
1012
|
-
$response->assertOk()
|
|
1013
|
-
->assertJsonStructure([
|
|
1014
|
-
'data' => [
|
|
1015
|
-
'*' => ['id', 'name', 'email', 'role', 'created_at'],
|
|
1016
|
-
],
|
|
1017
|
-
'pagination' => ['current_page', 'total', 'per_page'],
|
|
1018
|
-
]);
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
public function test_non_admin_cannot_list_users(): void
|
|
1022
|
-
{
|
|
1023
|
-
$user = User::factory()->create(['role' => 'user']);
|
|
1024
|
-
|
|
1025
|
-
Sanctum::actingAs($user);
|
|
1026
|
-
|
|
1027
|
-
$response = $this->getJson('/api/users');
|
|
1028
|
-
|
|
1029
|
-
$response->assertForbidden();
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
public function test_user_can_get_own_profile(): void
|
|
1033
|
-
{
|
|
1034
|
-
$user = User::factory()->create();
|
|
1035
|
-
|
|
1036
|
-
Sanctum::actingAs($user);
|
|
1037
|
-
|
|
1038
|
-
$response = $this->getJson('/api/users/me');
|
|
1039
|
-
|
|
1040
|
-
$response->assertOk()
|
|
1041
|
-
->assertJson([
|
|
1042
|
-
'data' => [
|
|
1043
|
-
'id' => $user->id,
|
|
1044
|
-
'email' => $user->email,
|
|
1045
|
-
],
|
|
1046
|
-
]);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
public function test_admin_can_create_user(): void
|
|
1050
|
-
{
|
|
1051
|
-
$admin = User::factory()->create(['role' => 'admin']);
|
|
1052
|
-
|
|
1053
|
-
Sanctum::actingAs($admin);
|
|
1054
|
-
|
|
1055
|
-
$response = $this->postJson('/api/users', [
|
|
1056
|
-
'name' => 'New User',
|
|
1057
|
-
'email' => 'new@example.com',
|
|
1058
|
-
'password' => 'SecurePass123!',
|
|
1059
|
-
'password_confirmation' => 'SecurePass123!',
|
|
1060
|
-
]);
|
|
1061
|
-
|
|
1062
|
-
$response->assertCreated()
|
|
1063
|
-
->assertJson([
|
|
1064
|
-
'data' => [
|
|
1065
|
-
'email' => 'new@example.com',
|
|
1066
|
-
],
|
|
1067
|
-
]);
|
|
1068
|
-
|
|
1069
|
-
$this->assertDatabaseHas('users', [
|
|
1070
|
-
'email' => 'new@example.com',
|
|
1071
|
-
]);
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
public function test_cannot_create_user_with_duplicate_email(): void
|
|
1075
|
-
{
|
|
1076
|
-
$admin = User::factory()->create(['role' => 'admin']);
|
|
1077
|
-
$existing = User::factory()->create();
|
|
1078
|
-
|
|
1079
|
-
Sanctum::actingAs($admin);
|
|
1080
|
-
|
|
1081
|
-
$response = $this->postJson('/api/users', [
|
|
1082
|
-
'name' => 'New User',
|
|
1083
|
-
'email' => $existing->email,
|
|
1084
|
-
'password' => 'SecurePass123!',
|
|
1085
|
-
'password_confirmation' => 'SecurePass123!',
|
|
1086
|
-
]);
|
|
1087
|
-
|
|
1088
|
-
$response->assertUnprocessable()
|
|
1089
|
-
->assertJsonValidationErrors(['email']);
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
// tests/Feature/AuthTest.php
|
|
1095
|
-
namespace Tests\Feature;
|
|
1096
|
-
|
|
1097
|
-
use App\Models\User;
|
|
1098
|
-
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
1099
|
-
use Tests\TestCase;
|
|
1100
|
-
|
|
1101
|
-
class AuthTest extends TestCase
|
|
131
|
+
class UserResource extends JsonResource
|
|
1102
132
|
{
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
public function test_user_can_register(): void
|
|
1106
|
-
{
|
|
1107
|
-
$response = $this->postJson('/api/auth/register', [
|
|
1108
|
-
'name' => 'Test User',
|
|
1109
|
-
'email' => 'test@example.com',
|
|
1110
|
-
'password' => 'SecurePass123!',
|
|
1111
|
-
'password_confirmation' => 'SecurePass123!',
|
|
1112
|
-
]);
|
|
1113
|
-
|
|
1114
|
-
$response->assertCreated()
|
|
1115
|
-
->assertJsonStructure([
|
|
1116
|
-
'user' => ['id', 'email', 'name'],
|
|
1117
|
-
'token',
|
|
1118
|
-
]);
|
|
1119
|
-
|
|
1120
|
-
$this->assertDatabaseHas('users', [
|
|
1121
|
-
'email' => 'test@example.com',
|
|
1122
|
-
]);
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
public function test_user_can_login(): void
|
|
1126
|
-
{
|
|
1127
|
-
$user = User::factory()->create([
|
|
1128
|
-
'password' => bcrypt('password123'),
|
|
1129
|
-
]);
|
|
1130
|
-
|
|
1131
|
-
$response = $this->postJson('/api/auth/login', [
|
|
1132
|
-
'email' => $user->email,
|
|
1133
|
-
'password' => 'password123',
|
|
1134
|
-
]);
|
|
1135
|
-
|
|
1136
|
-
$response->assertOk()
|
|
1137
|
-
->assertJsonStructure([
|
|
1138
|
-
'user' => ['id', 'email'],
|
|
1139
|
-
'token',
|
|
1140
|
-
]);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
public function test_login_fails_with_wrong_password(): void
|
|
133
|
+
public function toArray(Request $request): array
|
|
1144
134
|
{
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
'email' => $
|
|
1149
|
-
'
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
$response->assertUnauthorized();
|
|
135
|
+
return [
|
|
136
|
+
'id' => $this->id,
|
|
137
|
+
'name' => $this->name,
|
|
138
|
+
'email' => $this->email,
|
|
139
|
+
'organizations' => OrganizationResource::collection($this->whenLoaded('organizations')),
|
|
140
|
+
'created_at' => $this->created_at->toIso8601String(),
|
|
141
|
+
];
|
|
1153
142
|
}
|
|
1154
143
|
}
|
|
1155
144
|
```
|
|
1156
145
|
|
|
1157
|
-
##
|
|
1158
|
-
|
|
1159
|
-
### Queue Jobs for Background Processing
|
|
1160
|
-
|
|
1161
|
-
```php
|
|
1162
|
-
<?php
|
|
1163
|
-
// app/Jobs/SendWelcomeEmail.php
|
|
1164
|
-
namespace App\Jobs;
|
|
1165
|
-
|
|
1166
|
-
use App\Mail\WelcomeEmail;
|
|
1167
|
-
use App\Models\User;
|
|
1168
|
-
use Illuminate\Bus\Queueable;
|
|
1169
|
-
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
1170
|
-
use Illuminate\Foundation\Bus\Dispatchable;
|
|
1171
|
-
use Illuminate\Queue\InteractsWithQueue;
|
|
1172
|
-
use Illuminate\Queue\SerializesModels;
|
|
1173
|
-
use Illuminate\Support\Facades\Mail;
|
|
1174
|
-
|
|
1175
|
-
class SendWelcomeEmail implements ShouldQueue
|
|
1176
|
-
{
|
|
1177
|
-
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
1178
|
-
|
|
1179
|
-
public int $tries = 3;
|
|
1180
|
-
public int $backoff = 60;
|
|
1181
|
-
|
|
1182
|
-
public function __construct(
|
|
1183
|
-
public readonly User $user
|
|
1184
|
-
) {}
|
|
146
|
+
## Best Practices
|
|
1185
147
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
148
|
+
| Do | Avoid |
|
|
149
|
+
|----|-------|
|
|
150
|
+
| Use Form Requests for validation | Validating in controllers |
|
|
151
|
+
| Use API Resources for responses | Returning models directly |
|
|
152
|
+
| Use service classes for logic | Fat controllers |
|
|
153
|
+
| Use eager loading | N+1 queries |
|
|
154
|
+
| Use soft deletes | Hard deletes for important data |
|
|
1191
155
|
|
|
1192
|
-
|
|
1193
|
-
{
|
|
1194
|
-
// Log or notify about the failure
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
156
|
+
## Project Structure
|
|
1197
157
|
|
|
1198
|
-
// Usage
|
|
1199
|
-
SendWelcomeEmail::dispatch($user);
|
|
1200
158
|
```
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
public readonly User $user
|
|
1219
|
-
) {}
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// app/Listeners/SendWelcomeNotification.php
|
|
1223
|
-
namespace App\Listeners;
|
|
1224
|
-
|
|
1225
|
-
use App\Events\UserCreated;
|
|
1226
|
-
use App\Jobs\SendWelcomeEmail;
|
|
1227
|
-
|
|
1228
|
-
class SendWelcomeNotification
|
|
1229
|
-
{
|
|
1230
|
-
public function handle(UserCreated $event): void
|
|
1231
|
-
{
|
|
1232
|
-
SendWelcomeEmail::dispatch($event->user);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// app/Providers/EventServiceProvider.php
|
|
1237
|
-
protected $listen = [
|
|
1238
|
-
UserCreated::class => [
|
|
1239
|
-
SendWelcomeNotification::class,
|
|
1240
|
-
],
|
|
1241
|
-
];
|
|
159
|
+
app/
|
|
160
|
+
├── Http/
|
|
161
|
+
│ ├── Controllers/Api/
|
|
162
|
+
│ ├── Requests/
|
|
163
|
+
│ ├── Resources/
|
|
164
|
+
│ └── Middleware/
|
|
165
|
+
├── Models/
|
|
166
|
+
├── Services/
|
|
167
|
+
├── Policies/
|
|
168
|
+
├── Jobs/
|
|
169
|
+
└── Events/
|
|
170
|
+
routes/
|
|
171
|
+
├── api.php
|
|
172
|
+
└── web.php
|
|
173
|
+
tests/
|
|
174
|
+
├── Feature/
|
|
175
|
+
└── Unit/
|
|
1242
176
|
```
|
|
1243
177
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
### Do's
|
|
1247
|
-
|
|
1248
|
-
- Use UUID primary keys for public APIs
|
|
1249
|
-
- Use Form Requests for validation
|
|
1250
|
-
- Use API Resources for response formatting
|
|
1251
|
-
- Use service classes for business logic
|
|
1252
|
-
- Use policies for authorization
|
|
1253
|
-
- Use eager loading to prevent N+1
|
|
1254
|
-
- Use database transactions for writes
|
|
1255
|
-
- Write feature and unit tests
|
|
1256
|
-
- Use queues for heavy operations
|
|
1257
|
-
- Use soft deletes for important data
|
|
1258
|
-
|
|
1259
|
-
### Don'ts
|
|
1260
|
-
|
|
1261
|
-
- Don't put business logic in controllers
|
|
1262
|
-
- Don't use raw queries without bindings
|
|
1263
|
-
- Don't ignore validation
|
|
1264
|
-
- Don't skip authorization checks
|
|
1265
|
-
- Don't expose internal IDs
|
|
1266
|
-
- Don't use mutable defaults
|
|
1267
|
-
- Don't ignore exceptions
|
|
1268
|
-
- Don't skip testing
|
|
1269
|
-
- Don't use sync for heavy tasks
|
|
1270
|
-
- Don't forget rate limiting
|
|
1271
|
-
|
|
1272
|
-
## References
|
|
1273
|
-
|
|
1274
|
-
- [Laravel Documentation](https://laravel.com/docs)
|
|
1275
|
-
- [Laravel Best Practices](https://github.com/alexeymezenin/laravel-best-practices)
|
|
1276
|
-
- [Laravel API Tutorial](https://laravel.com/docs/eloquent-resources)
|
|
1277
|
-
- [Laravel Testing](https://laravel.com/docs/testing)
|
|
1278
|
-
- [Spatie Guidelines](https://spatie.be/guidelines/laravel-php)
|
|
178
|
+
For detailed examples and patterns, see reference files above.
|