Service container adalah pusat pembentuk objek pada Laravel yang menyimpan definisi (binding) dan memproduksi setiap objek yang dibutuhkan aplikasi. Topik ini membahas cara mendaftarkan binding, cara mengambil (resolve) objek dari container melalui Tinker, serta cara injeksi dependensi (dependency injection) menghubungkan kelas-kelas yang saling bergantung secara otomatis. Seluruh contoh dijalankan pada aplikasi catatan, sehingga perilaku container dapat diamati langsung dari keluaran aslinya.
Peran service container#
Service container pada dokumentasi Laravel adalah instance Illuminate\Foundation\Application yang mewarisi seluruh kemampuan kelas Illuminate\Container\Container. Container menyimpan dua hal. Pertama, daftar binding, yaitu aturan “kelas atau interface tertentu dibuat dengan cara tertentu”. Kedua, mekanisme resolusi yang membaca aturan tersebut lalu menghasilkan objek yang sudah lengkap dengan dependensinya.
Perilaku tersebut mudah dibayangkan sebagai pabrik yang menyimpan resep: perintah App::bind() mencatat resepnya, sedangkan App::make() menjalankan resep dan menyerahkan objek jadinya. Router, dispatcher, dan koneksi database yang digunakan framework juga diambil dari container yang sama. Karena semua objek dibentuk melalui satu titik yang sama, implementasi sebuah layanan dapat diganti di satu tempat tanpa menyentuh kode pemanggilnya.
Binding dan resolusi manual#
Metode bind() mendaftarkan pasangan interface dan implementasinya. Setiap pemanggilan make() terhadap interface tersebut menghasilkan objek baru yang mengikuti aturannya. Contoh berikut mendaftarkan interface Sapaan ke kelas SapaanFormal, lalu mengambil objeknya dari container.
# mendaftarkan binding interface lalu mengambil objeknya
cd ~/aplikasi-catatan
php artisan tinker --execute='
use Illuminate\Support\Facades\App;
interface Sapaan { public function ucapkan(): string; }
class SapaanFormal implements Sapaan {
public function ucapkan(): string { return "Selamat datang di aplikasi catatan."; }
}
App::bind(Sapaan::class, SapaanFormal::class);
$sapaan = App::make(Sapaan::class);
echo get_class($sapaan) . PHP_EOL;
echo $sapaan->ucapkan() . PHP_EOL;
'SapaanFormal
Selamat datang di aplikasi catatan.Alur dari pencatatan binding hingga objek yang siap digunakan dirangkum pada diagram berikut.
bind(), container menyediakan singleton() untuk aturan yang hanya membuat satu instance, kemudian instance yang sama digunakan ulang pada setiap resolusi. Perbedaannya mudah dibuktikan dengan kelas penghitung sederhana.
# membandingkan singleton dengan bind biasa
cd ~/aplikasi-catatan
php artisan tinker --execute='
use Illuminate\Support\Facades\App;
class PenghitungKategori {
public int $jumlah = 0;
public function tambah(): void { $this->jumlah++; }
}
App::singleton(PenghitungKategori::class);
App::make(PenghitungKategori::class)->tambah();
App::make(PenghitungKategori::class)->tambah();
echo App::make(PenghitungKategori::class)->jumlah . PHP_EOL;
App::bind(PenghitungKategori::class);
App::make(PenghitungKategori::class)->tambah();
App::make(PenghitungKategori::class)->tambah();
echo App::make(PenghitungKategori::class)->jumlah . PHP_EOL;
'2
0Setelah singleton() dipasang, ketiga pemanggilan make() mengembalikan objek yang sama sehingga nilai hitungannya mencapai 2. Setelah binding diganti kembali dengan bind(), setiap make() menghasilkan objek baru yang hitungannya dimulai dari nol. Ada satu varian lagi, yaitu instance(), yang memasang objek yang sudah jadi ke dalam container sehingga resolusi tidak membentuk objek baru.
Injeksi dependensi otomatis#
Container tidak memerlukan binding untuk membentuk kelas konkret. Saat make() diminta membuat sebuah kelas, container membaca constructor-nya, memeriksa tipe setiap parameter, lalu mengulangi proses yang sama untuk setiap dependensi sampai seluruh rantai terisi. Mekanisme ini disebut resolusi rekursif, sehingga tidak ada satu pun pemanggilan new yang perlu dituliskan pada kode aplikasi.
# resolusi otomatis tanpa binding apa pun
cd ~/aplikasi-catatan
php artisan tinker --execute='
use Illuminate\Support\Facades\App;
class RepositoriKategoriMemori {
public function semua(): array { return ["Kerja", "Belajar", "Pribadi"]; }
}
class LaporanKategori {
public function __construct(private RepositoriKategoriMemori $repositori) {}
public function jumlahKategori(): int { return count($this->repositori->semua()); }
}
$laporan = App::make(LaporanKategori::class);
echo $laporan->jumlahKategori() . PHP_EOL;
'3Keluaran 3 dihasilkan tanpa satu baris konfigurasi apa pun. App::make(LaporanKategori::class) membaca parameter RepositoriKategoriMemori pada constructor, membentuk objeknya lebih dahulu, kemudian menyuntikkannya. Mekanisme yang sama bekerja ketika framework membangun controller, yaitu parameter pada constructor maupun parameter metode aksi diisi oleh container melalui router.
// tidak-dijalankan
<?php
namespace App\Http\Controllers;
use App\Services\RingkasanCatatan;
class RingkasanController extends Controller
{
public function index(RingkasanCatatan $ringkasan): string
{
return $ringkasan->teks();
}
}Kapan binding perlu dituliskan#
Resolusi otomatis hanya berlaku untuk kelas konkret. Interface tidak dapat di-instantiate, sehingga setiap interface yang di-type hint wajib memiliki binding. Tanpa binding, resolusinya gagal dengan error yang jelas.
# mengambil interface yang belum punya binding
cd ~/aplikasi-catatan
php artisan tinker --execute='
interface PenyediaRingkasan {}
$ringkasan = app()->make(PenyediaRingkasan::class);
' Illuminate\Contracts\Container\BindingResolutionException Target [PenyediaRingkasan] is not instantiable while building [Laravel\Tinker\Console\TinkerCommand].Pesan error menyebut TinkerCommand karena resolusi berjalan di dalam perintah Tinker yang mengeksekusi kode tersebut. Pada aplikasi sungguhan, konteksnya menunjuk kelas yang sedang dibangun, misalnya controller pemanggilnya. Perbaikannya tetap sama, yaitu mendaftarkan App::bind(PenyediaRingkasan::class, Implementasinya::class) sebelum resolusi dijalankan. Binding aplikasi ditempatkan pada metode register() service provider, sebagaimana dibahas pada topik Service provider.
| Metode | Perilaku saat resolusi |
|---|---|
bind() | Objek baru dibuat pada setiap pemanggilan make(). |
singleton() | Satu objek dibuat sekali, lalu digunakan ulang. |
instance() | Objek yang sudah jadi dipasang dan dikembalikan apa adanya. |
Kesalahan umum#
Kesalahan pertama adalah menggunakan singleton() untuk kelas yang menyimpan keadaan per permintaan, misalnya objek yang menampung data pengguna yang sedang login. Instance tersebut bertahan sepanjang proses dan digunakan ulang, sehingga pada aplikasi berbasis queue atau long-running process, data satu pengguna dapat terbawa ke permintaan lain. Gunakan bind() untuk kelas semacam itu.
Kesalahan kedua adalah membuat objek dengan new secara manual, misalnya new LaporanKategori(new RepositoriKategoriMemori()). Pemanggilan seperti ini melewati container sepenuhnya, sehingga perubahan implementasi pada binding tidak berpengaruh apa pun pada objek tersebut. Serahkan pembentukan objek kepada make() atau cukup tulis type hint-nya, lalu biarkan container mengisinya.
Kesalahan ketiga adalah menempatkan binding di tempat yang dieksekusi terlambat, misalnya di dalam metode aksi controller. Binding harus terpasang sebelum objek pertama diminta, dan tempat yang ditetapkan untuknya adalah register() pada service provider yang dimuat framework saat booting.
Kesalahan keempat adalah mengikat kelas konkret yang sudah dapat diresolusi otomatis tanpa kebutuhan khusus, misalnya App::bind(LayananArsip::class, LayananArsip::class). Binding semacam ini tidak menambah perilaku apa pun karena container memang mampu membangun kelas tersebut, tetapi binding itu menambah satu tempat yang harus dibaca ulang setiap kali implementasinya diperiksa. Binding bermakna ketika mengganti implementasi, membungkus constructor dengan argumen tertentu, atau mengubah masa hidup objek menjadi singleton.
Ringkasan#
- Service container menyimpan binding dan memproduksi objek untuk seluruh aplikasi, termasuk objek internal framework.
bind()membuat objek baru pada setiap resolusi,singleton()menggunakan ulang satu instance, daninstance()memasang objek yang sudah jadi.- Container membaca constructor secara rekursif, sehingga kelas konkret dapat diambil tanpa konfigurasi, sedangkan interface wajib memiliki binding agar tidak gagal dengan
BindingResolutionException. - Injeksi dependensi pada controller bekerja melalui mekanisme yang sama, baik pada constructor maupun pada parameter metode aksi.
Referensi#
- Service Container — Laravel — Dokumentasi resmi binding, resolusi, dan injeksi dependensi otomatis.
- Artisan Console — Laravel — Dokumentasi Tinker yang dipakai untuk menguji perilaku container pada topik ini.