feat: update CourseService and CourseController to accept User parameter for improved scoping

This commit is contained in:
Yoga Pangestu 2026-09-03 13:45:27 +07:00
parent 2b458e26fa
commit 1c901e9103
3 changed files with 50 additions and 36 deletions

View File

@ -161,6 +161,21 @@ ## 4. Controller, Request, dan Service
- **Controller** hanya mengatur alur request → response (validasi input dipanggil, service dipanggil, redirect/Inertia render dikembalikan). **Tidak boleh** ada query Eloquent kompleks atau business logic langsung di controller. - **Controller** hanya mengatur alur request → response (validasi input dipanggil, service dipanggil, redirect/Inertia render dikembalikan). **Tidak boleh** ada query Eloquent kompleks atau business logic langsung di controller.
- **Selalu gunakan FormRequest**, sekecil apapun validasinya — jangan validasi inline di controller dengan `$request->validate()`. - **Selalu gunakan FormRequest**, sekecil apapun validasinya — jangan validasi inline di controller dengan `$request->validate()`.
- Business logic (kalkulasi, orkestrasi antar model, side effect seperti notifikasi/log) **disimpan di Service**, bukan di controller atau model. - Business logic (kalkulasi, orkestrasi antar model, side effect seperti notifikasi/log) **disimpan di Service**, bukan di controller atau model.
- **Kalau method Service butuh user yang sedang login untuk scoping/filtering** (mis. dosen cuma lihat kelasnya sendiri, mahasiswa cuma lihat jurusannya sendiri), terima sebagai parameter eksplisit `User $user` (non-nullable, tanpa default) di **posisi pertama** — jangan panggil `auth()->user()` langsung di dalam Service. Controller yang menyuplainya lewat `$request->user()`. Ini soal testability (Service tidak bergantung diam-diam ke global state) dan konsistensi lintas Service.
```php
// Bad
public function paginated(int $perPage = 25): LengthAwarePaginator
{
$user = auth()->user();
// ...
}
// Good
public function paginated(User $user, int $perPage = 25): LengthAwarePaginator
{
// ...
}
```
- Otorisasi berbasis permission (Spatie) dicek di dua tempat: - Otorisasi berbasis permission (Spatie) dicek di dua tempat:
- Route-level: middleware `permission:create-xxx` dipasang per-route/per-group. - Route-level: middleware `permission:create-xxx` dipasang per-route/per-group.
- Object-level (mis. user hanya boleh mengubah record miliknya sendiri): di `authorize()` milik FormRequest, kombinasikan `$this->user()->can('permission-name')` dengan pengecekan kepemilikan record. - Object-level (mis. user hanya boleh mengubah record miliknya sendiri): di `authorize()` milik FormRequest, kombinasikan `$this->user()->can('permission-name')` dengan pengecekan kepemilikan record.

View File

@ -23,6 +23,7 @@ public function index(PaginatedRequest $request): Response
{ {
return Inertia::render('admin/master/courses/index', [ return Inertia::render('admin/master/courses/index', [
'courses' => $this->service->paginated( 'courses' => $this->service->paginated(
$request->user(),
...$request->validatedWithDefaults(), ...$request->validatedWithDefaults(),
departmentId: $request->validated('department_id'), departmentId: $request->validated('department_id'),
semesterNumber: $request->validated('semester_number'), semesterNumber: $request->validated('semester_number'),

View File

@ -2,52 +2,28 @@
namespace App\Services\Admin\Master; namespace App\Services\Admin\Master;
use App\Enums\UserRole;
use App\Models\Course; use App\Models\Course;
use App\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
class CourseService class CourseService
{ {
public function getAllForSelect(): Collection public function paginated(User $user, int $perPage = 25, string $search = '', ?int $departmentId = null, ?int $semesterNumber = null): LengthAwarePaginator
{ {
return Course::query() return Course::query()
->select(['courses.id', 'courses.code', 'courses.name', 'courses.department_id', 'courses.semester_number'])
->join('departments', 'departments.id', '=', 'courses.department_id')
->with('department:id,name')
->orderBy('departments.name')
->orderBy('courses.semester_number')
->orderBy('courses.name')
->get();
}
public function getSemesterNumbers(): array
{
return Course::query()
->select('semester_number')
->whereNotNull('semester_number')
->distinct()
->orderBy('semester_number')
->pluck('semester_number')
->all();
}
public function paginated(int $perPage = 25, string $search = '', ?int $departmentId = null, ?int $semesterNumber = null): LengthAwarePaginator
{
$user = auth()->user();
return Course::query()
->select(['courses.id', 'courses.code', 'courses.name', 'courses.credits', 'courses.department_id', 'courses.semester_number'])
->join('departments', 'departments.id', '=', 'courses.department_id') ->join('departments', 'departments.id', '=', 'courses.department_id')
->with('department:id,name') ->with('department:id,name')
->when($search, fn ($q) => $q->where('courses.name', 'like', "%{$search}%")->orWhere('courses.code', 'like', "%{$search}%")) ->when($search, fn ($q) => $q->where('courses.name', 'like', "%{$search}%")->orWhere('courses.code', 'like', "%{$search}%"))
->when($departmentId, fn ($q) => $q->where('courses.department_id', $departmentId)) ->when($departmentId, fn ($q) => $q->where('courses.department_id', $departmentId))
->when($semesterNumber, fn ($q) => $q->where('courses.semester_number', $semesterNumber)) ->when($semesterNumber, fn ($q) => $q->where('courses.semester_number', $semesterNumber))
->when($user->hasRole('mahasiswa'), fn ($q) => $q->where('courses.department_id', $user->student?->department_id)) ->when($user->hasRole(UserRole::Mahasiswa->value), fn ($q) => $q->where('courses.department_id', $user->student?->department_id))
->when($user->hasRole('dosen'), fn ($q) => $q->whereIn('courses.department_id', $user->lecturer?->departments()->pluck('departments.id') ?? [])) ->when($user->hasRole(UserRole::Dosen->value), fn ($q) => $q->whereIn('courses.department_id', $user->lecturer?->departments()->pluck('departments.id') ?? []))
->orderBy('departments.name') ->orderBy('departments.name')
->orderBy('courses.semester_number') ->orderBy('courses.semester_number')
->orderBy('courses.name') ->orderBy('courses.name')
->paginate($perPage); ->paginate($perPage, ['courses.id', 'courses.code', 'courses.name', 'courses.credits', 'courses.department_id', 'courses.semester_number']);
} }
public function create(array $data): Course public function create(array $data): Course
@ -57,12 +33,13 @@ public function create(array $data): Course
public function update(Course $course, array $data): Course public function update(Course $course, array $data): Course
{ {
$course->code = $data['code']; $course->update([
$course->name = $data['name']; 'code' => $data['code'],
$course->credits = $data['credits']; 'name' => $data['name'],
$course->department_id = $data['department_id']; 'credits' => $data['credits'],
$course->semester_number = $data['semester_number'] ?? null; 'department_id' => $data['department_id'],
$course->update(); 'semester_number' => $data['semester_number'] ?? null,
]);
return $course; return $course;
} }
@ -71,4 +48,25 @@ public function delete(Course $course): bool
{ {
return $course->delete(); return $course->delete();
} }
public function getAllForSelect(): Collection
{
return Course::query()
->join('departments', 'departments.id', '=', 'courses.department_id')
->with('department:id,name')
->orderBy('departments.name')
->orderBy('courses.semester_number')
->orderBy('courses.name')
->get(['courses.id', 'courses.code', 'courses.name', 'courses.department_id', 'courses.semester_number']);
}
public function getSemesterNumbers(): array
{
return Course::query()
->whereNotNull('semester_number')
->distinct()
->orderBy('semester_number')
->pluck('semester_number')
->all();
}
} }