Chapter sebelumnya berakhir dengan satu pertanyaan yang belum terjawab: siapa yang membuat instance PostPublisher lalu menyusunnya ke constructor controller tanpa pernah kita menulis new PostPublisher()? Pelakunya adalah service container — sebuah registry terpusat yang tugasnya membuat dan menyediakan objek. Container Laravel mengikuti standar PSR-11 untuk interface container PHP, dan hampir semua fitur yang kita pakai sejauh ini — route model binding, validasi form request, sampai penyuntikan dependency — berdiri di atasnya. Chapter ini membedah cara container menyelesaikan dependency, dari resolusi otomatis tanpa konfigurasi sampai berbagai bentuk binding.
Resolusi Tanpa Konfigurasi #
Fakta pertama yang membuat container terasa ajaib: kelas konkret bisa diselesaikan tanpa kita mendaftarkan apa pun. Container memakai reflection untuk membaca constructor kelas, lalu menyediakan setiap parameter yang dibutuhkan — dan resolusinya berlapis: dependency dari dependency juga diselesaikan. Perhatikan dua kelas berikut:
<?php
namespace App\Services;
class Penyapa
{
public function __construct(
protected string $sapaan = 'Halo',
) {}
public function sapa(string $nama): string
{
return "{$this->sapaan}, {$nama}!";
}
}<?php
namespace App\Services;
class Sambutan
{
public function __construct(
protected Penyapa $penyapa,
) {}
public function sambut(string $nama): string
{
return 'Selamat datang, '.$nama.' — dari '.get_class($this->penyapa);
}
}Kelas Sambutan butuh Penyapa, dan kita tidak pernah mengikat keduanya ke mana pun. Kita uji resolusinya lewat skrip runner:
<?php
// uji-container.php — jalankan dari root proyek Laravel
$dir = $argv[1];
require $dir.'/vendor/autoload.php';
$app = require $dir.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
use App\Contracts\PusherNotifikasi;
use App\Services\Penyapa;
use App\Services\PusherLog;
use App\Services\Sambutan;
echo 'PSR-11: '.($app instanceof Psr\Container\ContainerInterface ? 'ya' : 'bukan').PHP_EOL;
echo 'zero-config: '.$app->make(Penyapa::class)->sapa('Budi').PHP_EOL;
echo 'rekursif : '.$app->make(Sambutan::class)->sambut('Siti').PHP_EOL;
$app->bind(PusherNotifikasi::class, PusherLog::class);
echo 'bind : '.$app->make(PusherNotifikasi::class)
->kirim('[email protected]', 'Tagihan siap').PHP_EOL;Output yang Dihasilkan:
PSR-11: ya
zero-config: Halo, Budi!
rekursif : Selamat datang, Siti — dari App\Services\Penyapa
bind : [log] notifikasi untuk [email protected]: Tagihan siapBaris zero-config membuktikan container meng-instansiasi Penyapa begitu diminta lewat make() atau helper app(). Baris rekursif menunjukkan container membaca constructor Sambutan, menemukan parameter bertipe Penyapa, lalu menyelesaikannya lebih dulu — dependency injection terjadi otomatis di dalam container, dan baris PSR-11 memastikan container Laravel mengimplementasi interface standar tersebut.
Mengikat Interface ke Implementasi #
Interface tidak bisa diinstansiasi — new PusherNotifikasi() jelas mustahil. Binding menghubungkan abstraction tersebut dengan kelas konkret yang mengimplementasikannya:
<?php
namespace App\Contracts;
interface PusherNotifikasi
{
public function kirim(string $penerima, string $pesan): string;
}<?php
namespace App\Services;
use App\Contracts\PusherNotifikasi;
class PusherLog implements PusherNotifikasi
{
public function kirim(string $penerima, string $pesan): string
{
return "[log] notifikasi untuk {$penerima}: {$pesan}";
}
}Setelah $app->bind(PusherNotifikasi::class, PusherLog::class), setiap permintaan terhadap interface tersebut menghasilkan instance PusherLog — persis seperti baris keempat output di atas. Kode pemanggil cukup bergantung pada interface; implementasi bisa ditukar di satu titik binding tanpa menyentuh pemanggilnya.
bind, singleton, dan scoped #
Tiga bentuk binding yang paling sering dipakai dibedakan oleh cara mereka mengelola instance:
| Binding | Instance yang dikembalikan |
|---|---|
bind() | Baru setiap kali di-resolve |
singleton() | Satu instance dibagikan sepanjang hidup aplikasi |
scoped() | Seperti singleton, tetapi di-reset tiap siklus request |
Perbedaannya bisa dibuktikan dengan kelas penghitung sederhana yang menyimpan status internal:
<?php
namespace App\Services;
class Penghitung
{
public int $hitungan = 0;
public function naik(): int
{
return ++$this->hitungan;
}
}Lanjutan skrip runner di atas menguji ketiganya dengan status yang sama — dibuat naik, lalu di-resolve ulang:
$a = $app->make(Penghitung::class);
$a->naik();
$b = $app->make(Penghitung::class);
echo 'bind : hitungan instance kedua = '.$b->hitungan.PHP_EOL;
$app->singleton(Penghitung::class);
$c = $app->make(Penghitung::class);
$c->naik();
$d = $app->make(Penghitung::class);
echo 'singleton : objek sama = '.(spl_object_id($c) === spl_object_id($d) ? 'ya' : 'bukan')
.', hitungan = '.$d->hitungan.PHP_EOL;
$app->scoped(Penghitung::class);
$e = $app->make(Penghitung::class);
$e->naik();
$app->forgetScopedInstances();
$f = $app->make(Penghitung::class);
echo 'scoped : setelah reset = '.$f->hitungan.PHP_EOL;Output yang Dihasilkan:
bind : hitungan instance kedua = 0
singleton : objek sama = ya, hitungan = 1
scoped : setelah reset = 0Ketiga baris tersebut bercerita: bind() menyerahkan instance baru ($b tidak mewarisi hitungan $a), singleton() menyerahkan objek yang sama persis, dan scoped() mengembalikan instance segar begitu container me-reset instance ber-scope — pada aplikasi web, reset ini terjadi otomatis di awal setiap request.
Binding Primitif dan Kontekstual #
Container juga mengisi parameter scalar constructor melalui contextual binding — binding yang berlaku hanya untuk satu kelas pemakai. Lanjutan skrip runner di atas, dengan RiwayatNotifikasi ikut di-import:
$app->when(RiwayatNotifikasi::class)->needs('$batas')->give(25);
echo 'kontekstual: batas = '.$app->make(RiwayatNotifikasi::class)->batas().PHP_EOL;Output yang Dihasilkan:
kontekstual: batas = 25Constructor RiwayatNotifikasi memiliki default protected int $batas = 10, tetapi binding di atas menimpanya menjadi 25 khusus saat kelas tersebut di-resolve oleh container. Bentuk when()->needs()->give() yang sama juga bisa mengarahkan interface ke implementasi berbeda per pemakai — berguna bila dua service membutuhkan “notifier” dengan perilaku berbeda.
Saat Container Menyerah #
Container hanya bisa mengisi parameter yang jelas sumbernya: kelas konkret lewat reflection, atau interface lewat binding. Interface tanpa binding memancing exception yang jujur:
use App\Contracts\PengirimPesan;
try {
$app->make(PengirimPesan::class);
} catch (Illuminate\Contracts\Container\BindingResolutionException $e) {
echo 'gagal : '.$e->getMessage().PHP_EOL;
}Output yang Dihasilkan:
gagal : Target [App\Contracts\PengirimPesan] is not instantiable.Interface PengirimPesan memang sengaja tidak kita ikat. Pesan Target … is not instantiable adalah jawaban container atas type hint yang tidak bisa dipenuhi — solusinya selalu salah satu dari dua: daftarkan binding-nya, atau ganti type hint ke kelas konkret.
Semua binding pada chapter ini kita tulis langsung ke variabel $app demi eksperimen. Aplikasi nyata menyimpannya di satu tempat yang dijalankan setiap kali framework booting: service provider — topik chapter berikutnya. Daftar lengkap fitur container tersedia di dokumentasi resmi
.