Filosofi thin controller pada chapter pembuka Section ini menuturkan janji: controller adalah koordinator, bukan pelaksana. Kita kini punya bekal cukup untuk menepati janji tersebut secara nyata, karena dua fasilitas Laravel yang dibutuhkan sudah berada dalam jangkauan — FormRequest untuk memindahkan validasi dan action class untuk memindahkan logika bisnis. Chapter ini membangun alur pendaftaran pengguna yang lengkap, lalu membandingkan hasilnya dengan versi controller yang menumpuk semua pekerjaan di satu method.

Masalah Controller Gemuk #

Alur pendaftaran yang sehat: validasi input, buat pengguna dengan password ter-hash, kembalikan redirect. Bayangkan seluruh langkah itu ditulis langsung di method controller:

php
// Contoh tampilan — pola yang perlu kita hindari
public function simpan(Request $request): RedirectResponse
{
    $data = $request->all();

    if (empty($data['nama']) || strlen($data['nama']) > 100) {
        return back()->withErrors(['nama' => 'Nama wajib diisi maksimal 100 karakter.']);
    }

    if (! filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
        return back()->withErrors(['email' => 'Format email tidak valid.']);
    }

    if (strlen($data['password']) < 8) {
        return back()->withErrors(['password' => 'Password minimal 8 karakter.']);
    }

    if (User::where('email', $data['email'])->exists()) {
        return back()->withErrors(['email' => 'Email sudah terdaftar.']);
    }

    $pengguna = User::create([
        'name' => $data['nama'],
        'email' => $data['email'],
        'password' => Hash::make($data['password']),
    ]);

    // ... kirim email selamat datang, catat log, dst.

    return redirect()->route('pendaftaran.sukses')
        ->with('status', "Selamat bergabung, {$pengguna->name}!");
}

Method di atas bekerja, tetapi setiap tambahan aturan menambah baris di satu tempat yang sama, dan logika “cara mendaftarkan pengguna” terkunci di balik request HTTP — queue job atau Artisan command tidak bisa memakainya tanpa meniru seluruh method. Ulangi pola ini pada sepuluh controller lain dan aplikasi kita memperoleh sepuluh salinan aturan validasi email yang harus dirawat satu per satu. Inilah fat controller: banyak alasan berubah, satu berkas menampung semuanya.

Validasi Pindah ke FormRequest #

FormRequest adalah kelas request yang membawa aturan validasinya sendiri. Generator make:request menyediakan kerangkanya:

bash
php artisan make:request SimpanPendaftaranRequest

Output yang Dihasilkan:

text
  INFO  Request [app/Http/Requests/SimpanPendaftaranRequest.php] created successfully.  

Kita isi method rules() dengan aturan yang tadi ditulis manual, dan ubah authorize() menjadi true karena pendaftaran memang terbuka:

php
<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;

class SimpanPendaftaranRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    /**
     * @return array<string, ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'nama' => 'required|string|max:100',
            'email' => 'required|email|unique:users,email',
            'password' => 'required|string|min:8',
        ];
    }
}

Aturan berpindah dari tubuh controller menjadi deklarasi di kelas request. Ketika method controller men-type-hint SimpanPendaftaranRequest, container meng-instansiasi-nya, menjalankan validasi, dan me-redirect kembali dengan error — semua sebelum baris pertama method controller berjalan. Gagal validasi berarti method controller tidak pernah dieksekusi.

Logika Bisnis Pindah ke Action Class #

Pekerjaan “mendaftarkan pengguna” — bukan urusannya memvalidasi request atau membangun redirect — pindah ke kelas kecil yang berdiri sendiri:

php
<?php

namespace App\Actions;

use App\Models\User;
use Illuminate\Support\Facades\Hash;

class DaftarkanPengguna
{
    public function eksekusi(string $nama, string $email, string $password): User
    {
        return User::create([
            'name' => $nama,
            'email' => $email,
            'password' => Hash::make($password),
        ]);
    }
}

Kelas DaftarkanPengguna tidak mengenal HTTP sama sekali: tidak ada Request, tidak ada redirect. Karena itu pula kelas ini bisa dipanggil dari queue job, Artisan command, atau seeder dengan cara yang persis sama — inilah alasan utama pemisahan. Konvensi penempatannya sederhana: folder app/Actions untuk kelas berisi satu aksi bernama kata kerja (DaftarkanPengguna, KirimInvoice), satu berkas satu kelas. Bila nanti alur pendaftaran bertambah — mengirim email selamat datang, mencatat event analytics — semua perubahan terjadi di dalam kelas ini tanpa menyentuh controller, form request, maupun view.

Controller sebagai Koordinator #

Hasil akhirnya adalah controller yang hanya menempelkan bagian-bagian tersebut:

php
<?php

namespace App\Http\Controllers;

use App\Actions\DaftarkanPengguna;
use App\Http\Requests\SimpanPendaftaranRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;

class PendaftaranController extends Controller
{
    public function form(): View
    {
        return view('pendaftaran');
    }

    public function simpan(SimpanPendaftaranRequest $request, DaftarkanPengguna $daftarkan): RedirectResponse
    {
        $pengguna = $daftarkan->eksekusi(
            nama: $request->input('nama'),
            email: $request->input('email'),
            password: $request->input('password'),
        );

        return redirect()
            ->route('pendaftaran.sukses')
            ->with('status', "Selamat bergabung, {$pengguna->name}!");
    }

    public function sukses(): string
    {
        return 'Pendaftaran sukses — '.session('status', 'belum ada status');
    }
}

Signature simpan() sekaligus merangkum arsitektur alur ini: SimpanPendaftaranRequest menjamin input bersih, DaftarkanPengguna menjalankan logika, dan controller tinggal menyusun response. Method form() dan sukses() menempatkan tugas presentasi pada view plus satu pembacaan flash message — tetap koordinasi, bukan logika. Pembagian tanggung jawabnya menjadi:

Tanggung jawabDipegang oleh
Aturan validasi inputSimpanPendaftaranRequest
Logika bisnis pendaftaranDaftarkanPengguna (action class)
Penyimpanan dataModel User + Eloquent
Koordinasi dan responsePendaftaranController
Tampilan formView pendaftaran.blade.php

Feature test memastikan seluruh rangkaian bekerja — termasuk bahwa password tersimpan dalam bentuk hash dan input tidak valid tidak pernah menyentuh database:

php
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;

class PendaftaranTest extends TestCase
{
    use RefreshDatabase;

    public function test_pendaftaran_valid_menyimpan_pengguna(): void
    {
        $response = $this->post('/pendaftaran', [
            'nama' => 'Budi',
            'email' => '[email protected]',
            'password' => 'rahasia-kuat-123',
        ]);

        $response->assertRedirect('/pendaftaran/sukses');
        $response->assertSessionHas('status', 'Selamat bergabung, Budi!');

        $pengguna = User::sole();

        $this->assertSame('Budi', $pengguna->name);
        $this->assertTrue(Hash::check('rahasia-kuat-123', $pengguna->password));
    }

    public function test_pendaftaran_tidak_valid_kembali_dengan_error(): void
    {
        $response = $this->from('/pendaftaran')->post('/pendaftaran', [
            'nama' => 'Siti',
            'email' => 'bukan-email',
            'password' => 'pendek',
        ]);

        $response->assertRedirect('/pendaftaran');
        $response->assertSessionHasErrors(['email', 'password']);
        $this->assertDatabaseCount('users', 0);
    }
}

Output yang Dihasilkan:

text
  PASS  Tests\Feature\PendaftaranTest
  ✓  pendaftaran valid menyimpan pengguna                                0.16s  
  ✓  pendaftaran tidak valid kembali dengan error                        0.01s  

  Tests:    2 passed (11 assertions)
  Duration: 0.25s

Pemisahan pada chapter ini adalah pintu masuk ke diskusi arsitektur yang lebih dalam — action class versus service class, kapan memakai keduanya, dan bagaimana melapisinya pada aplikasi besar. Kita lanjutkan pembahasan tersebut di dokumentasi validation untuk FormRequest, lalu mendalami pola-pola tersebut pada Section Arsitektur dan Best Practice.