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
<?php

namespace App\Services;

class Penyapa
{
    public function __construct(
        protected string $sapaan = 'Halo',
    ) {}

    public function sapa(string $nama): string
    {
        return "{$this->sapaan}, {$nama}!";
    }
}
php
<?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
<?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:

text
PSR-11: ya
zero-config: Halo, Budi!
rekursif   : Selamat datang, Siti — dari App\Services\Penyapa
bind       : [log] notifikasi untuk [email protected]: Tagihan siap

Baris 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
<?php

namespace App\Contracts;

interface PusherNotifikasi
{
    public function kirim(string $penerima, string $pesan): string;
}
php
<?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:

BindingInstance 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
<?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:

php
$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:

text
bind       : hitungan instance kedua = 0
singleton  : objek sama = ya, hitungan = 1
scoped     : setelah reset = 0

Ketiga 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:

php
$app->when(RiwayatNotifikasi::class)->needs('$batas')->give(25);

echo 'kontekstual: batas = '.$app->make(RiwayatNotifikasi::class)->batas().PHP_EOL;

Output yang Dihasilkan:

text
kontekstual: batas = 25

Constructor 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:

php
use App\Contracts\PengirimPesan;

try {
    $app->make(PengirimPesan::class);
} catch (Illuminate\Contracts\Container\BindingResolutionException $e) {
    echo 'gagal      : '.$e->getMessage().PHP_EOL;
}

Output yang Dihasilkan:

text
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 .