Kalau kamu sudah mulai ngoding Laravel dan sering lihat kelas yang constructornya diisi parameter aneh-aneh — “ini dari mana datangnya? Kan tidak pernah di-new manual?” — itu adalah Dependency Injection yang sedang bekerja.
Topik ini sering terasa misterius karena Laravel melakukan banyak hal “otomatis” di balik layar. Artikel ini akan buka tutupnya satu per satu: mulai dari kenapa DI ada, lalu tiga cara pakainya, sampai bagaimana Laravel tahu harus inject apa.
Kita mulai dari masalahnya dulu.
🤔 Masalah yang DI Selesaikan
Bayangin kamu lagi bikin fitur artikel di blog. Ada ArticleController yang butuh ArticleService untuk logika bisnisnya.
Cara paling naif yang langsung terpikirkan:
class ArticleController extends Controller
{
public function index()
{
// Langsung bikin object di dalam controller
$service = new ArticleService();
return $service->getAll();
}
}
Kelihatannya tidak ada masalah. Tapi coba bayangkan tiga skenario ini:
Skenario 1: ArticleService butuh ArticleRepository untuk akses database. Jadi kamu harus new ArticleRepository() di dalam ArticleService. Dan ArticleRepository butuh koneksi database. Dan seterusnya — kamu tiba-tiba sedang merakit rantai panjang object secara manual.
Skenario 2: Kamu mau test ArticleController. Tapi new ArticleService() di dalamnya langsung memanggil database sungguhan — test-mu tidak bisa jalan tanpa database aktif.
Skenario 3: Kamu mau ganti ArticleService dengan versi berbeda (misalnya yang pakai cache). Kamu harus cari semua tempat yang nulis new ArticleService() dan ganti satu per satu.
Dependency Injection adalah solusinya. Alih-alih kelas yang membuat dependensinya sendiri, dependensi itu diberikan dari luar. Kelas cukup bilang “aku butuh ini” — urusan siapa yang menyediakan, bukan urusannya lagi.
🔧 Constructor Injection — “Kasih Sebelum Mulai”
Analoginya
Bayangin kamu seorang chef baru yang mulai kerja di restoran. Sebelum kamu mulai masak apapun, manajernya sudah menyiapkan semua yang kamu butuhkan di meja dapur: pisau, talenan, dan bumbu dasar.
Kamu tidak perlu keluar dan beli sendiri. Kamu tidak perlu tahu bumbu itu dari warung mana. Kamu tinggal masak.
Constructor Injection persis seperti itu — semua yang dibutuhkan kelas sudah tersedia sejak object pertama kali dibuat, sebelum method apapun dipanggil.
Kapan Cocok?
Kalau dependensi dipakai di banyak method dalam kelas yang sama. Inject sekali di constructor, pakai di mana saja.
Kodenya
Studi kasus: ArticleController butuh ArticleService untuk semua operasinya.
📁 app/Services/ArticleService.php
<?php
namespace App\Services;
use App\Models\Article;
use Illuminate\Support\Collection;
class ArticleService
{
public function getAll(): Collection
{
return Article::with('user')->latest()->get();
}
public function getPublished(): Collection
{
return Article::with('user')
->where('status', 'published')
->latest('published_at')
->get();
}
public function findOrFail(int $id): Article
{
return Article::findOrFail($id);
}
}
📁 app/Http/Controllers/ArticleController.php
<?php
namespace App\Http\Controllers;
use App\Services\ArticleService;
use Illuminate\Http\JsonResponse;
class ArticleController extends Controller
{
// $articleService tersimpan sebagai property —
// bisa dipakai oleh index(), show(), store(), dan method lain
public function __construct(
private ArticleService $articleService
) {}
public function index(): JsonResponse
{
$articles = $this->articleService->getPublished();
return response()->json($articles);
}
public function show(int $id): JsonResponse
{
$article = $this->articleService->findOrFail($id);
return response()->json($article);
}
}
📌 Perhatikan — ArticleController tidak pernah nulis new ArticleService(). Dia hanya mendeklarasikan bahwa dia butuh ArticleService. Laravel yang akan menyediakannya secara otomatis saat controller dipanggil.
Cara Kerja di Baliknya
Ketika ada request masuk ke route yang menuju ArticleController, Laravel membaca constructor-nya: “Oh, butuh ArticleService. Oke, aku buatkan.” Lalu object ArticleService dibuat dan dimasukkan ke constructor secara otomatis. Ini yang disebut auto-resolve — dan kita akan bahas lebih dalam di bagian IoC Container.
💉 Method Injection — “Kasih Saat Dibutuhkan”
Analoginya
Masih di restoran yang sama. Kali ini ada tamu yang pesan menu spesial yang butuh peralatan khusus — blow torch untuk membakar crème brûlée.
Blow torch itu tidak perlu ada di meja kamu sepanjang waktu. Kamu tidak pakai setiap hari. Kamu minta ke dapur hanya ketika ada pesanan menu itu saja.
Method Injection seperti itu — dependensi hanya disediakan ketika method tertentu dipanggil, tidak perlu ada sepanjang hidup object.
Kapan Cocok?
Kalau dependensi hanya dipakai di satu method tertentu — tidak perlu dibawa ke seluruh kelas.
Kodenya
Studi kasus: ada CommentController yang butuh ArticleService hanya di satu method — untuk validasi apakah artikel yang mau dikomentari itu ada.
📁 app/Http/Controllers/CommentController.php
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreCommentRequest;
use App\Services\ArticleService;
use App\Services\CommentService;
use Illuminate\Http\JsonResponse;
class CommentController extends Controller
{
// CommentService dipakai di banyak method → layak di constructor
public function __construct(
private CommentService $commentService
) {}
// ArticleService hanya dibutuhkan di method ini saja → method injection
public function store(StoreCommentRequest $request, ArticleService $articleService): JsonResponse
{
// Pastikan artikel yang dituju benar-benar ada
$article = $articleService->findOrFail($request->article_id);
$comment = $this->commentService->create(
$request->validated(),
$article,
$request->user()
);
return response()->json($comment, 201);
}
}
📌 ArticleService hanya ada di parameter method store(). Method lain di controller ini tidak akan mendapat — dan tidak butuh — ArticleService sama sekali. Laravel tetap auto-resolve dengan cara yang sama.
Method Injection di Route Closure
Ada satu tempat lagi yang sering dipakai untuk method injection: langsung di route closure. Cocok untuk endpoint ringan yang tidak butuh controller tersendiri.
📁 routes/api.php
use App\Services\ArticleService;
use Illuminate\Http\Request;
// Laravel inject ArticleService langsung ke closure ini
Route::get('/articles/published', function (Request $request, ArticleService $service) {
return $service->getPublished();
});
📌 Persis seperti di controller — tulis type-hint di parameter, Laravel yang sediakan. Tidak perlu new ArticleService() di mana pun.
📦 IoC Container — “Otak di Balik Semuanya”
Analoginya
Sekarang kamu mungkin bertanya: “Oke, tapi Laravel tahu dari mana harus inject apa?”
Bayangin ada gudang besar di restoran itu. Gudang ini menyimpan daftar semua peralatan yang tersedia, stoknya, dan aturan khusus kalau ada peralatan yang perlu disiapkan dengan cara tertentu sebelum diberikan.
Ketika chef butuh sesuatu, chef tidak pergi ke gudang sendiri. Ada petugas gudang yang bertugas: membaca kebutuhan chef, mengambil dari gudang, menyiapkan sesuai aturan, lalu mengantarkan.
IoC Container (Inversion of Control Container) adalah petugas gudang itu. Di Laravel, dia diakses lewat app() atau App::make(). Dan aturan-aturan khusus tadi? Itu yang kita tulis di Service Provider.
Auto-resolve — Saat Kelas Tidak Butuh Konfigurasi
Untuk kelas biasa seperti ArticleService yang tidak punya dependensi kompleks, IoC Container bisa langsung membuatnya tanpa perlu instruksi apapun. Cukup type-hint, Laravel resolve sendiri.
// Laravel resolve ArticleService otomatis — kamu tidak perlu daftarkan apapun
$service = app(ArticleService::class);
// Atau lewat facade
$service = App::make(ArticleService::class);
Ini jalan karena Laravel membaca constructor ArticleService, melihat dependensinya, membuat dependensi itu dulu, lalu membuat ArticleService-nya. Rekursif sampai semua dependensi terpenuhi.
Binding — Saat Interface Perlu Dipetakan ke Implementasi
Auto-resolve tidak bisa jalan untuk interface. Kalau kamu type-hint dengan ArticleRepositoryInterface, Laravel tidak tahu implementasi mana yang harus dipakai — ada banyak kemungkinan.
Di sinilah binding dibutuhkan.
Studi kasus: CommentService bergantung pada CommentRepositoryInterface, bukan langsung ke Eloquent.
📁 app/Contracts/CommentRepositoryInterface.php
<?php
namespace App\Contracts;
use App\Models\Comment;
use Illuminate\Support\Collection;
interface CommentRepositoryInterface
{
public function getUnapproved(): Collection;
public function approve(int $commentId): void;
}
📁 app/Repositories/EloquentCommentRepository.php
<?php
namespace App\Repositories;
use App\Contracts\CommentRepositoryInterface;
use App\Models\Comment;
use Illuminate\Support\Collection;
class EloquentCommentRepository implements CommentRepositoryInterface
{
public function getUnapproved(): Collection
{
return Comment::where('is_approved', false)->latest()->get();
}
public function approve(int $commentId): void
{
Comment::findOrFail($commentId)->update(['is_approved' => true]);
}
}
📁 app/Providers/AppServiceProvider.php
Di sinilah kita “mendaftarkan aturan” ke IoC Container — interface ini dipasangkan ke implementasi ini.
<?php
namespace App\Providers;
use App\Contracts\CommentRepositoryInterface;
use App\Repositories\EloquentCommentRepository;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// "Kalau ada yang minta CommentRepositoryInterface,
// berikan EloquentCommentRepository."
$this->app->bind(
CommentRepositoryInterface::class,
EloquentCommentRepository::class
);
}
}
📁 app/Services/CommentService.php
Service ini tidak tahu apakah yang dipakai Eloquent, cache, atau API — dia hanya tahu kontraknya.
<?php
namespace App\Services;
use App\Contracts\CommentRepositoryInterface;
use Illuminate\Support\Collection;
class CommentService
{
public function __construct(
private CommentRepositoryInterface $commentRepository
) {}
public function getUnapproved(): Collection
{
return $this->commentRepository->getUnapproved();
}
public function approve(int $commentId): void
{
$this->commentRepository->approve($commentId);
}
}
📌 Kalau besok kamu mau ganti implementasi repository — misalnya pakai cache untuk performa lebih cepat — kamu hanya ubah satu baris di AppServiceProvider. CommentService, controller, dan semua kode lain tidak perlu disentuh sama sekali.
bind vs singleton — Kapan Pakai Yang Mana?
Ada dua cara binding yang paling sering dipakai, dan perbedaannya penting:
// bind → buat object BARU setiap kali diminta
$this->app->bind(
CommentRepositoryInterface::class,
EloquentCommentRepository::class
);
// singleton → buat object SEKALI, lalu pakai ulang object yang sama
$this->app->singleton(
ArticleService::class,
ArticleService::class
);
bind | singleton | |
|---|---|---|
| Jumlah object yang dibuat | Baru setiap request ke container | Sekali, lalu di-reuse |
| Cocok untuk | Repository, kelas yang butuh state bersih | Service stateless, koneksi, konfigurasi |
| Contoh di Laravel | Form Request, Action kecil | Cache, Queue, Auth Guard |
📌 Untuk kebanyakan service di blog kita, singleton sudah cukup — tidak ada state yang perlu di-reset antar penggunaan. Pakai bind kalau kamu butuh object yang benar-benar fresh setiap kali.
🗺️ Gambaran Besar — Alur Lengkap
Ini alur yang terjadi dari request masuk sampai response keluar, dikaitkan dengan DI:
Request masuk
↓
Router menemukan ArticleController@store
↓
IoC Container membaca constructor ArticleController
→ Butuh ArticleService
↓
IoC Container membaca constructor ArticleService
→ Tidak ada dependensi → langsung dibuat
↓
ArticleService selesai dibuat
↓
ArticleController dibuat, ArticleService dimasukkan ke constructor
↓
Method store() dipanggil
→ Butuh StoreArticleRequest (method injection) → dibuat
↓
Response dikembalikan
Dan struktur file-nya:
app/
├── Contracts/
│ └── CommentRepositoryInterface.php ← kontrak repository
│
├── Http/
│ ├── Controllers/
│ │ ├── ArticleController.php ← constructor injection
│ │ └── CommentController.php ← constructor + method injection
│ └── Requests/
│ └── StoreCommentRequest.php
│
├── Services/
│ ├── ArticleService.php ← di-inject lewat constructor
│ └── CommentService.php ← bergantung pada interface
│
├── Repositories/
│ └── EloquentCommentRepository.php ← implementasi konkret
│
└── Providers/
└── AppServiceProvider.php ← tempat binding interface → implementasi
🧭 Kapan Pakai Yang Mana?
Constructor Injection ketika dependensi dipakai di banyak method dalam satu kelas. Ini pilihan default — kalau ragu, pakai ini.
Method Injection ketika dependensi hanya dipakai di satu method tertentu, atau di route closure yang terlalu kecil untuk dibuatkan controller sendiri.
IoC Container + Binding ketika kamu type-hint dengan interface, atau butuh kontrol lebih atas cara Laravel membuat suatu object (misalnya mau inject konfigurasi tertentu).
📊 Perbandingan Singkat
| Constructor | Method | IoC Binding | |
|---|---|---|---|
| Dependensi tersedia di | Semua method kelas | Hanya method itu | Di mana saja |
| Cocok untuk | Dependensi utama kelas | Dependensi sesekali | Interface ke implementasi |
| Ditulis di mana | __construct() | Parameter method | AppServiceProvider |
| Perlu didaftarkan? | Tidak (auto-resolve) | Tidak (auto-resolve) | Ya, kalau pakai interface |
💡 Ingat!Waktu pertama kali lihat constructor yang isinya penuh type-hint, wajar kalau rasanya “ini ngapain? Langsungnewaja kan lebih simpel.”
Tapi begitu kamu mau nulis test, atau mau ganti implementasi tanpa meledakkan separuh aplikasi — kamu akan ngerti kenapa DI ada. Kelas yang dependensinya disuntikkan dari luar itu jauh lebih mudah ditukar, dites, dan dirawat.
Laravel sudah menyediakan semua infrastrukturnya. Kamu tinggal type-hint dengan benar, dan biarkan IoC Container bekerja.
Kalau kamu baru belajar OOP, dua hal ini pasti bakal bikin kepala pusing duluan — bukan karena susah, tapi karena hampir semua penjelasan di internet langsung loncat ke kode tanpa ngasih gambaran besarnya dulu.
Jadi di artikel ini, kita mulai dari analogi dulu. Baru setelah ngerti konsepnya, kita masuk ke kode.
Masalah yang Mereka Selesaikan
Bayangin kamu lagi bangun aplikasi manajemen data. Di aplikasi ini ada fitur ekspor laporan, dan kamu mau support tiga format: PDF, Excel, dan CSV.
Masing-masing format cara pembuatannya beda. Tapi semua harus bisa melakukan dua hal yang sama:
generate()— untuk membuat file-nyadownload()— untuk mengirim file ke browser
Nah, di sinilah masalahnya muncul.
Kalau kamu bikin 3 kelas terpisah tanpa aturan apapun, bisa-bisa PdfExporter punya method bernama generate(), tapi ExcelExporter malah namanya build(), dan CsvExporter namanya create(). Semua bikin hal yang sama tapi dengan nama berbeda.
Ini yang disebut kode tidak konsisten — dan ini mimpi buruk kalau aplikasi makin besar.
Interface dan Abstract Class adalah solusinya. Keduanya adalah cara untuk bilang ke semua kelas: “Hei, kamu harus punya method ini. Titik.”
Interface — “Daftar Aturan Kosong”
Analoginya
Bayangin Interface seperti formulir kontrak kerja.
Di kontrak itu tertulis: “Kamu wajib bisa menyetir mobil, wajib bisa berbahasa Inggris, dan wajib punya SIM A.”
Kontrak tidak peduli kamu belajar nyetir dari siapa, les bahasa Inggris di mana, atau ujian SIM-nya di kota mana. Yang penting: hasilnya ada, dan bisa dibuktikan.
Interface di PHP persis seperti itu — dia cuma mendaftarkan apa yang harus bisa dilakukan, tanpa peduli bagaimana caranya.
Aturan Interface
- Semua method di dalam interface tidak boleh punya isi (hanya nama dan parameternya saja).
- Kelas yang pakai interface (
implements) wajib mengisi semua method yang didaftarkan. - Satu kelas boleh
implementslebih dari satu interface sekaligus.
Kodenya
<?php
namespace App\Contracts;
interface ExporterInterface
{
// Hanya daftar method — tidak ada isi sama sekali
public function generate(array $data): string;
public function download(string $filename): void;
}
📌 Perhatikan — di dalam interface tidak ada kurung kurawal { } berisi logika. Hanya ada nama method, parameter, dan tipe return-nya. Itu saja. Ini namanya method signature.
Abstract Class — “Fondasi Setengah Jadi”
Analoginya
Sekarang bayangin Abstract Class seperti rumah yang sudah dibangun setengah jadi oleh developer perumahan.
Fondasi sudah ada. Dinding sudah ada. Atap sudah ada. Listrik dan air sudah nyambung.
Tapi beberapa hal sengaja dibiarkan kosong — desain interior, warna cat, jenis lantai. Kenapa? Karena itu terserah pembelinya masing-masing.
Developer bilang: “Bagian ini kamu yang harus isi sendiri. Bagian lainnya sudah aku siapkan, tinggal pakai.”
Abstract Class di PHP persis seperti itu.
Aturan Abstract Class
- Boleh punya method yang sudah ada isinya (siap pakai oleh subclass).
- Boleh punya method
abstract— yang wajib diisi oleh subclass. - Boleh punya property (variabel seperti
$disk). - Tidak bisa di-
newlangsung — hanya bisa dipakai lewat subclass. - Satu kelas hanya boleh
extendssatu abstract class.
Kodenya
<?php
namespace App\Services;
use App\Contracts\ExporterInterface;
use Illuminate\Support\Facades\Storage;
abstract class BaseExporter implements ExporterInterface
{
// Property yang bisa dipakai oleh semua subclass
protected string $disk = 'local';
// Method ini SUDAH ADA ISINYA — semua format pakai logika download yang sama:
// simpan file sementara → kirim ke browser → hapus file
public function download(string $filename): void
{
$path = "exports/{$filename}";
Storage::disk($this->disk)->put($path, $this->tempFile);
response()->download(
Storage::disk($this->disk)->path($path)
)->deleteFileAfterSend()->send();
}
// Method ini SENGAJA DIKOSONGKAN — tiap format cara generate-nya beda:
// PDF pakai library berbeda dengan Excel, Excel berbeda dengan CSV
abstract public function generate(array $data): string;
}
📌 Perhatikan keyword abstract di depan method generate. Itu artinya: “Method ini wajib diisi oleh siapapun yang extends class ini. Kalau tidak diisi, PHP akan langsung error.”
Concrete Class — “Produk Jadinya”
Ini adalah kelas yang benar-benar dipakai di aplikasi. Dia extends abstract class dan mengisi semua method yang masih kosong.
📁 app/Services/PdfExporter.php
<?php
namespace App\Services;
use Barryvdh\DomPDF\Facade\Pdf;
class PdfExporter extends BaseExporter
{
// Wajib diisi karena BaseExporter mendeklarasikannya sebagai abstract
public function generate(array $data): string
{
// Logika khusus PDF — pakai library DomPDF
$pdf = Pdf::loadView('exports.report', ['data' => $data]);
$this->tempFile = $pdf->output();
return $this->tempFile;
}
// Method download() tidak perlu ditulis ulang —
// sudah diwarisi dari BaseExporter
}
📁 app/Services/ExcelExporter.php
<?php
namespace App\Services;
use Maatwebsite\Excel\Facades\Excel;
class ExcelExporter extends BaseExporter
{
public function generate(array $data): string
{
// Logika khusus Excel — pakai library Maatwebsite
// Cara generate-nya sama sekali beda dengan PDF, tapi nama method-nya sama
$this->tempFile = Excel::raw(new ReportExport($data), \Maatwebsite\Excel\Excel::XLSX);
return $this->tempFile;
}
}
📌 ExcelExporter tidak perlu nulis ulang download() karena sudah diwarisi dari BaseExporter. Yang berbeda hanya cara generate()-nya — dan memang itulah satu-satunya yang berbeda antar format.
Gambaran Besar — Posisi Tiap File
Ini gambaran keseluruhan struktur dan hubungannya:
app/
├── Contracts/
│ └── ExporterInterface.php ← "Daftar aturan" — interface
│
├── Services/
│ ├── BaseExporter.php ← "Fondasi" — abstract class
│ ├── PdfExporter.php ← "Produk jadi" — concrete class
│ ├── ExcelExporter.php ← concrete class lain
│ └── CsvExporter.php ← concrete class lain
Alur warisannya:
ExporterInterface
↓ (implements)
BaseExporter
↓ (extends)
PdfExporter / ExcelExporter / CsvExporter
Jadi, Kapan Pakai Yang Mana?
Pakai Interface ketika: Kamu cuma mau bilang “kelas ini harus bisa melakukan X dan Y” — tanpa peduli bagaimana caranya dan tanpa ada logika bersama sama sekali.
Pakai Abstract Class ketika: Ada logika yang sama persis di beberapa kelas, dan kamu malas nulis ulang di tiap kelas. Tapi beberapa bagian tetap harus diisi sendiri.
Pakai keduanya (paling umum di Laravel) ketika: Interface sebagai kontrak publik, Abstract Class sebagai fondasi bersama, Concrete Class sebagai implementasi spesifik.
📊 Perbandingan Singkat
| Interface | Abstract Class | |
|---|---|---|
| Bisa punya isi method | ❌ | ✅ |
| Bisa punya property | ❌ | ✅ |
| Satu kelas bisa pakai lebih dari satu | ✅ | ❌ |
Bisa di-new langsung | ❌ | ❌ |
| Tujuan utama | Kontrak/aturan | Fondasi bersama |
💡 Ingat!Waktu pertama kali belajar ini, wajar kalau masih bingung “kenapa tidak bikin class biasa saja?”— Ini bukan tentang benar atau salah. Ini tentang kode yang mudah dirawat jangka panjang.
Jawabannya: boleh saja. Tapi begitu aplikasi makin besar dan kamu punya banyak format ekspor — atau besok diminta tambah format baru — kamu akan sangat bersyukur ada interface yang memastikan semuanya konsisten, dan ada abstract class yang memastikan kamu tidak copy-paste logikadownload()yang sama ke mana-mana.