--- name: laravel-patterns description: Laravel 12 best practices, design patterns, and coding standards. Use when creating controllers, models, services, middleware, or any PHP backend code in Laravel projects. allowed-tools: Read, Grep, Glob, Edit, Write --- # Laravel Best Practices Skill This skill provides guidance for writing clean, maintainable Laravel 12 code following modern PHP and Laravel conventions. ## Project Structure ### Service Layer Pattern ``` app/ ├── Http/ │ ├── Controllers/ # Thin controllers, delegate to services │ ├── Requests/ # Form request validation │ ├── Resources/ # API resources │ └── Middleware/ # Request/response middleware ├── Models/ # Eloquent models ├── Services/ # Business logic ├── Repositories/ # Data access (optional) ├── Actions/ # Single-purpose action classes ├── DTOs/ # Data transfer objects ├── Enums/ # PHP 8.1+ enums └── Exceptions/ # Custom exceptions ``` ## Controllers ### Thin Controllers Controllers should only handle HTTP concerns. Delegate business logic to services. ```php employeeService->create($request->validated()); return EmployeeResource::make($employee) ->response() ->setStatusCode(201); } public function index(): JsonResponse { $employees = $this->employeeService->paginate(); return EmployeeResource::collection($employees)->response(); } } ``` ### Resource Controllers Use resource controllers for CRUD operations: ```php Route::resource('employees', EmployeeController::class); Route::apiResource('api/employees', Api\EmployeeController::class); ``` ## Form Requests ### Validation Logic ```php user()->can('create', Employee::class); } public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'unique:employees,email'], 'employee_id' => ['required', 'string', 'unique:employees'], 'department' => ['required', Rule::in(['IT', 'HR', 'Finance'])], 'salary' => ['required', 'numeric', 'min:0'], 'hire_date' => ['required', 'date', 'before_or_equal:today'], ]; } public function messages(): array { return [ 'email.unique' => 'Email sudah terdaftar.', 'hire_date.before_or_equal' => 'Tanggal tidak boleh di masa depan.', ]; } } ``` ## Services ### Service Class Pattern ```php attendanceService->initializeForEmployee($employee); return $employee->fresh(['department', 'schedules']); }); } public function paginate(int $perPage = 15): LengthAwarePaginator { return Employee::query() ->with(['department', 'latestAttendance']) ->withCount('attendances') ->latest() ->paginate($perPage); } public function findOrFail(string $id): Employee { return Employee::with(['department', 'schedules', 'attendances']) ->findOrFail($id); } } ``` ## Models ### Model Best Practices ```php 'date', 'salary' => 'decimal:2', 'status' => EmployeeStatus::class, 'type' => EmployeeType::class, 'metadata' => 'array', ]; } // Relationships public function department(): BelongsTo { return $this->belongsTo(Department::class); } public function attendances(): HasMany { return $this->hasMany(Attendance::class); } public function latestAttendance(): HasOne { return $this->hasOne(Attendance::class)->latestOfMany(); } // Scopes public function scopeActive(Builder $query): Builder { return $query->where('status', EmployeeStatus::Active); } public function scopeByDepartment(Builder $query, string $departmentId): Builder { return $query->where('department_id', $departmentId); } // Accessors protected function fullName(): Attribute { return Attribute::get(fn () => "{$this->first_name} {$this->last_name}"); } } ``` ## Enums (PHP 8.1+) ```php 'Aktif', self::Inactive => 'Tidak Aktif', self::OnLeave => 'Cuti', self::Terminated => 'Diberhentikan', }; } public function color(): string { return match($this) { self::Active => 'green', self::Inactive => 'gray', self::OnLeave => 'yellow', self::Terminated => 'red', }; } } ``` ## Query Optimization ### Eager Loading ```php // BAD - N+1 problem $employees = Employee::all(); foreach ($employees as $employee) { echo $employee->department->name; // N queries } // GOOD - Eager load $employees = Employee::with(['department', 'schedules'])->get(); ``` ### Chunking Large Datasets ```php Employee::query() ->where('status', 'active') ->chunk(100, function ($employees) { foreach ($employees as $employee) { // Process each employee } }); // Or with lazy loading for memory efficiency Employee::query() ->where('status', 'active') ->lazy() ->each(function ($employee) { // Process }); ``` ### Query Scopes ```php // In Model public function scopeAttendedToday(Builder $query): Builder { return $query->whereHas('attendances', function ($q) { $q->whereDate('date', today()); }); } // Usage $presentEmployees = Employee::active()->attendedToday()->get(); ``` ## API Resources ```php $this->id, 'name' => $this->name, 'email' => $this->email, 'employee_id' => $this->employee_id, 'position' => $this->position, 'status' => [ 'value' => $this->status->value, 'label' => $this->status->label(), 'color' => $this->status->color(), ], 'department' => DepartmentResource::make($this->whenLoaded('department')), 'attendances_count' => $this->whenCounted('attendances'), 'latest_attendance' => AttendanceResource::make($this->whenLoaded('latestAttendance')), 'created_at' => $this->created_at->toISOString(), 'updated_at' => $this->updated_at->toISOString(), ]; } } ``` ## Exception Handling ### Custom Exceptions ```php json([ 'error' => 'employee_not_found', 'message' => $this->getMessage(), ], 404); } } ``` ## Middleware ```php user()->employee; if (!$employee || !$employee->status->isActive()) { abort(403, 'Employee account is not active.'); } return $next($request); } } ``` ## Testing ### Feature Tests ```php admin()->create(); Employee::factory()->count(5)->create(); $response = $this->actingAs($user) ->getJson('/api/employees'); $response->assertOk() ->assertJsonCount(5, 'data') ->assertJsonStructure([ 'data' => [ '*' => ['id', 'name', 'email', 'status'] ], 'meta' => ['current_page', 'total'] ]); } public function test_can_create_employee(): void { $user = User::factory()->admin()->create(); $response = $this->actingAs($user) ->postJson('/api/employees', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'employee_id' => 'EMP001', ]); $response->assertCreated() ->assertJsonPath('data.name', 'John Doe'); $this->assertDatabaseHas('employees', [ 'email' => 'john@example.com' ]); } } ``` ## Security Best Practices ### Mass Assignment Protection ```php // Always use $fillable, never use $guarded = [] protected $fillable = ['name', 'email', 'position']; ``` ### Authorization with Policies ```php // Policy public function update(User $user, Employee $employee): bool { return $user->hasRole('admin') || $user->employee_id === $employee->id; } // Controller $this->authorize('update', $employee); ``` ### Sensitive Data ```php // Hide sensitive attributes protected $hidden = ['password', 'salary', 'remember_token']; // Or explicitly select Employee::select(['id', 'name', 'email'])->get(); ``` ## Performance Tips 1. **Cache expensive queries** ```php Cache::remember('dashboard.stats', 3600, fn() => $this->calculateStats()); ``` 2. **Use database transactions** ```php DB::transaction(function () { // Multiple related operations }); ``` 3. **Index frequently queried columns** ```php $table->index(['department_id', 'status']); ``` 4. **Use queue for heavy operations** ```php ProcessPayroll::dispatch($employee)->onQueue('payroll'); ```