Membuat Controller di Laravel 12: Resource, API, dan Invokable

laravel

Cara membuat controller di Laravel 12 adalah menjalankan php artisan make:controller NamaController. File-nya akan dibuat di app/Http/Controllers, lalu kamu menghubungkannya ke URL lewat routes/web.php atau routes/api.php. Panduan ini membahas empat jenis controller yang paling sering dipakai: controller biasa, resource controller untuk CRUD, API controller, dan invokable controller. Semua contoh kode di sini sudah kami jalankan dan uji dengan feature test di Laravel 12.70 (PHP 8.5), jadi bisa langsung kamu pakai.

Belum punya project? Ikuti dulu cara install Laravel 12, lalu kembali ke sini.

Apa Itu Controller di Laravel?

Controller adalah class yang mengumpulkan logika penanganan request. Daripada menulis semua logika di dalam closure di file route, kamu memindahkannya ke method di controller. Hasilnya, file route tetap pendek dan logika untuk satu fitur (misalnya produk) ada di satu tempat.

Di Laravel 12, base class App\Http\Controllers\Controller sengaja dibuat kosong. Berbeda dengan Laravel 10 ke bawah, base controller tidak lagi membawa trait AuthorizesRequests dan ValidatesRequests. Akibatnya, kode lama yang memanggil $this->authorize() atau $this->validate() di controller akan error. Gantinya, pakai Gate::authorize() dan $request->validate(), atau Form Request seperti contoh di bawah.

Jenis Controller dan Perintah Artisan-nya

Jenis Perintah Method yang dibuat Dipakai untuk
Biasa php artisan make:controller ProductController Kosong Halaman atau aksi bebas
Resource php artisan make:controller ProductController --resource index, create, store, show, edit, update, destroy CRUD dengan halaman form (Blade)
API php artisan make:controller ProductController --api index, store, show, update, destroy REST API (tanpa create dan edit)
Invokable php artisan make:controller ExportProductController --invokable __invoke Satu aksi saja, misalnya ekspor

Opsi tambahan yang berguna:

  • --model=Product membuat type-hint model di parameter method, sehingga route model binding langsung aktif.
  • --requests (dipakai bersama --resource atau --api) membuat class StoreProductRequest dan UpdateProductRequest untuk validasi.
  • Nama dengan garis miring, misalnya Api/ProductController, membuat controller di subfolder dengan namespace App\Http\Controllers\Api.

Mau tahu perintah Artisan lain? Lihat juga panduan membuat custom Artisan command di Laravel 12.

Contoh Kasus: Data Produk

Semua contoh di artikel ini memakai satu model, Product, dengan kolom nama, harga, dan stok. Buat model sekaligus migration-nya:

php artisan make:model Product -m

Isi migration di database/migrations/xxxx_create_products_table.php:

public function up(): void
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->unsignedInteger('price');
        $table->unsignedInteger('stock')->default(0);
        $table->timestamps();
    });
}

Lalu izinkan mass assignment di app/Models/Product.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = ['name', 'price', 'stock'];
}

Jalankan php artisan migrate. Penjelasan lengkap soal tipe kolom, foreign key, dan perintah rollback ada di panduan migration Laravel.

Membuat Controller Biasa

Controller biasa cocok untuk halaman yang tidak mengikuti pola CRUD.

php artisan make:controller PageController
<?php

namespace App\Http\Controllers;

use Illuminate\View\View;

class PageController extends Controller
{
    public function about(): View
    {
        return view('about');
    }
}

Daftarkan di routes/web.php (dasar-dasar route ada di artikel routing Laravel). Perhatikan baris use di atas, karena ini sumber error paling umum:

use App\Http\Controllers\PageController;
use Illuminate\Support\Facades\Route;

Route::get('/tentang', [PageController::class, 'about'])->name('about');

Resource Controller untuk CRUD

Resource controller membuat tujuh method CRUD sekaligus. Dengan opsi --model dan --requests, Laravel juga menyiapkan type-hint model dan class validasi:

php artisan make:controller ProductController --model=Product --resource --requests

Perintah itu membuat tiga file: ProductController, StoreProductRequest, dan UpdateProductRequest. Isi aturan validasinya dulu. Perhatikan method authorize(): stub bawaan mengembalikan false, sehingga semua request akan ditolak dengan 403 kalau tidak kamu ubah.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreProductRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // stub bawaan bernilai false, sesuaikan dengan aturan akses
    }

    public function rules(): array
    {
        return [
            'name'  => ['required', 'string', 'max:255'],
            'price' => ['required', 'integer', 'min:0'],
            'stock' => ['nullable', 'integer', 'min:0'],
        ];
    }
}

Isi UpdateProductRequest dengan aturan yang sama. Sekarang controller-nya:

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Models\Product;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;

class ProductController extends Controller
{
    public function index(): View
    {
        $products = Product::latest()->paginate(10);

        return view('products.index', compact('products'));
    }

    public function create(): View
    {
        return view('products.create');
    }

    public function store(StoreProductRequest $request): RedirectResponse
    {
        Product::create($request->validated());

        return redirect()->route('products.index')
            ->with('status', 'Produk berhasil ditambahkan.');
    }

    public function show(Product $product): View
    {
        return view('products.show', compact('product'));
    }

    public function edit(Product $product): View
    {
        return view('products.edit', compact('product'));
    }

    public function update(UpdateProductRequest $request, Product $product): RedirectResponse
    {
        $product->update($request->validated());

        return redirect()->route('products.show', $product)
            ->with('status', 'Produk berhasil diperbarui.');
    }

    public function destroy(Product $product): RedirectResponse
    {
        $product->delete();

        return redirect()->route('products.index')
            ->with('status', 'Produk berhasil dihapus.');
    }
}

Kalau validasi gagal, Laravel otomatis mengembalikan pengguna ke form sebelumnya beserta pesan error. Kalau ID produk tidak ada, route model binding otomatis menghasilkan 404. Kamu tidak perlu menulis findOrFail sendiri.

Cukup satu baris di routes/web.php:

use App\Http\Controllers\ProductController;

Route::resource('products', ProductController::class);

Baris itu menghasilkan tujuh route berikut. Cek dengan php artisan route:list --except-vendor.

Method HTTP URI Method controller Nama route
GET /products index products.index
GET /products/create create products.create
POST /products store products.store
GET /products/{product} show products.show
GET /products/{product}/edit edit products.edit
PUT/PATCH /products/{product} update products.update
DELETE /products/{product} destroy products.destroy

Butuh sebagian saja? Pakai Route::resource('products', ProductController::class)->only(['index', 'show']); atau ->except(['destroy']).

Form HTML hanya mengenal GET dan POST. Untuk update dan hapus, tambahkan directive @method di form Blade:

<form method="POST" action="{{ route('products.update', $product) }}">
    @csrf
    @method('PUT')
    <input name="name" value="{{ old('name', $product->name) }}">
    <input name="price" type="number" value="{{ old('price', $product->price) }}">
    <button type="submit">Simpan</button>
</form>

API Controller di Laravel 12

Di Laravel 12, file routes/api.php tidak ada secara bawaan. Buat dulu dengan:

php artisan install:api

Perintah ini memasang Laravel Sanctum, membuat routes/api.php, dan mendaftarkannya di bootstrap/app.php. Semua route di file ini otomatis berawalan /api. Setelah itu buat API controller di subfolder agar tidak bentrok dengan controller web:

php artisan make:controller Api/ProductController --api --model=Product
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Models\Product;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;

class ProductController extends Controller
{
    public function index(): JsonResponse
    {
        return response()->json(Product::latest()->paginate(10));
    }

    public function store(StoreProductRequest $request): JsonResponse
    {
        $product = Product::create($request->validated());

        return response()->json($product, 201);
    }

    public function show(Product $product): JsonResponse
    {
        return response()->json($product);
    }

    public function update(UpdateProductRequest $request, Product $product): JsonResponse
    {
        $product->update($request->validated());

        return response()->json($product);
    }

    public function destroy(Product $product): Response
    {
        $product->delete();

        return response()->noContent();
    }
}

Daftarkan di routes/api.php:

use App\Http\Controllers\Api\ProductController;
use Illuminate\Support\Facades\Route;

Route::name('api.')->group(function () {
    Route::apiResource('products', ProductController::class);
});

Kenapa dibungkus Route::name('api.')? Saat kami menguji, Route::apiResource('products', ...) di routes/api.php menghasilkan nama route yang sama persis dengan resource web: products.index, products.store, dan seterusnya. Awalan URL /api tidak ikut mengubah nama route. Akibatnya route('products.index') di controller web bisa mengarah ke URL API. Dengan awalan nama api., nama route API menjadi api.products.index dan tidak bentrok lagi.

Perilaku yang kamu dapat tanpa kode tambahan: request POST /api/products dengan data tidak valid mendapat respons 422 berisi pesan error dalam JSON, ID yang tidak ada mendapat 404, dan hapus yang berhasil mendapat 204 No Content. Untuk mengatur bentuk JSON lebih rapi (menyembunyikan kolom tertentu atau mengganti nama field), lanjutkan dengan API Resource (php artisan make:resource ProductResource).

Invokable Controller (Controller Satu Aksi)

Kalau sebuah aksi tidak masuk pola CRUD dan cukup kompleks, beri ia controller sendiri dengan satu method __invoke. Contohnya ekspor produk ke CSV:

php artisan make:controller ExportProductController --invokable
<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Symfony\Component\HttpFoundation\StreamedResponse;

class ExportProductController extends Controller
{
    public function __invoke(): StreamedResponse
    {
        return response()->streamDownload(function () {
            $out = fopen('php://output', 'w');
            fputcsv($out, ['id', 'name', 'price', 'stock']);

            Product::orderBy('id')->each(function (Product $product) use ($out) {
                fputcsv($out, [$product->id, $product->name, $product->price, $product->stock]);
            });

            fclose($out);
        }, 'products.csv', ['Content-Type' => 'text/csv']);
    }
}

Route-nya tidak perlu menyebut nama method:

use App\Http\Controllers\ExportProductController;

Route::get('/products/export', ExportProductController::class)->name('products.export');
Route::resource('products', ProductController::class);

Urutan itu penting. Route /products/export harus didaftarkan sebelum Route::resource. Kalau terbalik, URL tersebut tertangkap route /products/{product} dan Laravel mencari produk dengan ID “export”, lalu mengembalikan 404. Penjelasan lebih dalam soal pola ini ada di artikel invokable controller di Laravel.

Middleware di Controller Laravel 12

Cara lama $this->middleware('auth') di constructor tidak berlaku lagi. Ada dua pilihan. Pertama, pasang di route:

Route::resource('products', ProductController::class)->middleware('auth');

Kedua, implementasikan interface HasMiddleware di controller:

<?php

namespace App\Http\Controllers;

use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;

class ReportController extends Controller implements HasMiddleware
{
    public static function middleware(): array
    {
        return [
            'auth',
            new Middleware('verified', only: ['download']),
        ];
    }

    public function index()
    {
        // semua pengguna yang sudah login
    }

    public function download()
    {
        // hanya pengguna dengan email terverifikasi
    }
}

Catatan: middleware verified hanya berpengaruh kalau model User mengimplementasikan interface MustVerifyEmail. Untuk aturan hak akses yang lebih detail, misalnya hanya pemilik data yang boleh mengedit, pakai Policy. Panduannya ada di Policy dan Gate di Laravel 12.

Error yang Sering Muncul Saat Membuat Controller

  • Target class [ProductController] does not exist. Baris use App\Http\Controllers\ProductController; lupa ditulis di file route, atau namespace di file controller salah. Format string lama 'ProductController@index' juga tidak bisa dipakai tanpa konfigurasi namespace, jadi gunakan sintaks array [ProductController::class, 'index'].
  • 403 This action is unauthorized padahal belum memasang hak akses. Method authorize() di Form Request masih return false.
  • Call to undefined method ... authorize(). Kode dari Laravel 10 ke bawah memakai $this->authorize(). Ganti dengan Gate::authorize('update', $product) (import Illuminate\Support\Facades\Gate).
  • 405 Method Not Allowed. Form update atau hapus tidak memakai @method('PUT') atau @method('DELETE').
  • 404 pada route tambahan seperti /products/export. Route tambahan didaftarkan setelah Route::resource.
  • Route model binding tidak jalan (model selalu kosong). Nama parameter di method harus sama dengan parameter di route. Untuk resource products, parameternya {product}, jadi method harus menerima Product $product.

Pertanyaan yang Sering Diajukan

Apa beda resource controller dan API controller?

Resource controller punya tujuh method, termasuk create dan edit yang menampilkan form HTML. API controller (opsi --api) hanya lima method karena API tidak butuh halaman form.

Apakah controller di Laravel 12 masih harus extends Controller?

Tidak wajib. Dokumentasi Laravel menyebut controller tidak harus meng-extend base class. Stub bawaan tetap meng-extend Controller supaya kamu punya satu tempat untuk menaruh method bersama.

Di mana file controller disimpan?

Di app/Http/Controllers. Kalau nama yang kamu berikan mengandung garis miring, misalnya Admin/ProductController, file disimpan di subfolder Admin dengan namespace yang sesuai.

Apakah cara membuat controller di Laravel 11 dan 12 sama?

Sama. Perintah make:controller, base controller kosong, HasMiddleware, dan install:api sudah ada sejak Laravel 11. Yang berubah ada pada perbandingan dengan Laravel 10 ke bawah.

Langkah berikutnya: kalau ingin halaman interaktif tanpa menulis JavaScript, pelajari Laravel Volt dan Livewire. Semua materi Laravel kami ada di halaman tutorial Laravel.

Butuh sistem berbasis Laravel untuk bisnis, misalnya aplikasi inventori, kasir, atau API untuk aplikasi mobile? Tim Arrazy Inovasi mengerjakan pembuatan sistem aplikasi custom dari perancangan sampai server production.

Konsultasi project Laravel →

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *