Service Container & Dependency Injection
Pahami IoC container: Laravel menyuntik dependensi otomatis.
Container: Otak di Balik "Magic" Laravel
Pernah heran kenapa kamu bisa type-hint Request $request di controller dan Laravel "tahu" harus memberi apa? Atau kenapa Cache::get() bekerja tanpa kamu pernah new apapun? Jawabannya satu: service container, sebuah IoC (Inversion of Control) container raksasa yang tahu cara membangun hampir semua object di aplikasimu. Memahami container berarti memahami kenapa kode Laravel mudah di-test, kenapa facade bukan static sungguhan, dan kenapa mengganti implementasi semudah mengubah satu baris.
Cara Kerja: Dari Type-Hint ke Object
Saat Laravel melihat type-hint di constructor atau method controller, container memakai reflection untuk membaca class apa yang dibutuhkan, membangun dependensinya secara rekursif, lalu menyuntikkannya:
// app/Services/StrukService.php
namespace App\Services;
class StrukService
{
// container otomatis membangun PrinterService + dependensinya
public function __construct(
private PrinterService $printer,
private LogoService $logo,
) {}
}
// app/Http/Controllers/KasirController.php
class KasirController extends Controller
{
// method injection: tanpa constructor pun bisa
public function cetak(StrukService $struk)
{
return $struk->cetak();
}
}Kamu tidak pernah menulis new StrukService(new PrinterService(...)). Container mengurus pohon dependensi sepanjang apapun. Inilah dependency injection: class tidak mencari dependensinya sendiri, dependensi disuntikkan dari luar.
Binding: Janji Interface ke Implementasi
Kekuatan sebenarnya muncul saat kamu type-hint interface, bukan class konkret:
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use App\Services\MidtransGateway;
use App\Services\PaymentGateway;
use App\Services\XenditGateway;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// bind: setiap butuh PaymentGateway, beri MidtransGateway
$this->app->bind(PaymentGateway::class, MidtransGateway::class);
// singleton: satu instance dipakai ulang sepanjang request
$this->app->singleton(\App\Services\KeranjangService::class);
// contextual: controller A dapat implementasi beda dari controller B
$this->app->when(LaporanController::class)
->needs(Exporter::class)
->give(CsvExporter::class);
$this->app->when(DashboardController::class)
->needs(Exporter::class)
->give(ExcelExporter::class);
}
}bind() membuat instance baru setiap kali diminta (cocok untuk service stateless). singleton() memakai ulang satu instance (cocok untuk service yang mahal dibuat atau menyimpan state request, seperti keranjang belanja). Contextual binding menyelesaikan kasus "dua controller butuh exporter berbeda" tanpa if-else di controller.
Facade: Bukan Static, Tapi Proxy Container
Ini fakta yang mengejutkan banyak orang: Cache::get('x') BUKAN static method. Facade adalah proxy cerdas ke object yang hidup di container:
use Illuminate\Support\Facades\Cache;
// sebenarnya sama dengan:
app('cache')->get('x');Kenapa ini penting? Karena di test, kamu bisa menukar object di belakang facade: Cache::fake(), Mail::fake(), Queue::fake(). Kalau Cache:: benar-benar static, fake tidak mungkin dilakukan. Memahami ini menjelaskan kenapa testing Laravel terasa mulus.
Jebakan Umum
Service location di mana-mana. Memanggil app(PaymentGateway::class) atau resolve() di tengah method adalah service location, bukan injection. Kode jadi sulit di-test dan dependensi tersembunyi. Aturan: app() hanya di provider, route closure, atau tempat yang memang tidak mendukung injection. Di class biasa, selalu constructor injection.
Singleton menyimpan state user. Singleton hidup sepanjang request. Menyimpan $this->userId di singleton lalu dipakai request berikutnya? Tidak masalah di PHP tradisional (request = proses baru). Tapi di Octane/Swoole (long-lived process), state bocor antar request! Jangan simpan state request di singleton kalau berencana pakai Octane.
Bind class konkret, bukan interface. $this->app->bind(MidtransGateway::class, ...) tidak memberi fleksibilitas apa-apa. Selalu bind interface ke implementasi agar ganti vendor tinggal ubah satu baris tanpa menyentuh controller.
Circular dependency. A butuh B, B butuh A. Container akan meledak dengan error membingungkan. Solusinya: pecah salah satu dependensi (event, atau inject salah satunya secara lazy via closure).
Catatan teknis: Butuh melihat isi container saat debug?
php artisan tinkerlaluapp()->bindingsmenampilkan semua binding terdaftar, atauresolve(PaymentGateway::class)untuk melihat object apa yang benar-benar dibangun. Sangat berguna saat binding "tidak jalan" dan kamu curiga salah tulis namespace.
Tantangan
Ganti Implementasi
Buat interface Notifier + 2 implementasi (EmailNotifier, LogNotifier). Bind interface ke LogNotifier di provider. Buat route yang type-hint Notifier dan buktikan yang dipakai LogNotifier. Lalu ganti binding ke EmailNotifier.