Service Container & IoC di Laravel — Cara Laravel Resolve Dependency

Di artikel-artikel sebelumnya, kita sudah beberapa kali menyebut “IoC Container” dan “Service Container” — biasanya dengan kalimat seperti “Laravel akan resolve ini otomatis” atau “Container yang menyediakan object-nya.”

Tapi kita belum pernah buka: bagaimana tepatnya Laravel tahu harus buat object apa, dengan dependensi apa, dalam urutan apa?

Itu yang akan kita bahas di artikel ini. Bukan hanya cara pakainya — tapi cara berpikirnya. Karena begitu kamu paham cara Container bekerja di dalam, semua hal yang terasa “ajaib” di Laravel akan jadi masuk akal.


🤔 Masalah yang Container Selesaikan

Kita mulai dari kenyataan yang tidak nyaman dulu.

Bayangin kamu punya ArticleController yang butuh ArticleService. ArticleService butuh ArticleRepository. ArticleRepository butuh koneksi database. Koneksi database butuh konfigurasi dari .env.

Kalau harus rakit manual:

// Tanpa container — kamu merakit semua sendiri
$config     = new DatabaseConfig(env('DB_HOST'), env('DB_NAME'));
$connection = new DatabaseConnection($config);
$repository = new EloquentArticleRepository($connection);
$service    = new ArticleService($repository);
$controller = new ArticleController($service);

$controller->index();

Ini baru empat lapis. Aplikasi nyata bisa sepuluh lapis, dengan cabang di tiap lapisnya. Kalau ada yang berubah di tengah rantai — misalnya ArticleRepository butuh parameter baru — kamu harus update semua tempat yang merakit rantai itu.

Service Container adalah solusinya. Kamu cukup daftarkan aturan perakitan sekali, dan setiap kali butuh object, Container yang merakit seluruh rantainya — otomatis.


🏭 Analogi Besar — Pabrik dengan Buku Resep

Bayangin sebuah pabrik makanan. Di pabrik ini ada gudang bahan baku, buku resep, dan mesin produksi.

Kalau kamu pesan “Nasi Goreng Spesial”:

  1. Mesin cek buku resep: “Nasi Goreng Spesial butuh nasi, telur, dan bumbu rahasia.”
  2. Mesin ambil semua bahan dari gudang.
  3. Kalau ada bahan yang juga punya resepnya sendiri (bumbu rahasia = bawang + cabai + terasi), mesin selesaikan itu dulu.
  4. Baru setelah semua bahan siap, produk jadi dibuat dan diserahkan.

Service Container di Laravel persis seperti itu:

  • Gudang = IoC Container (menyimpan binding dan instance)
  • Buku resep = binding yang kamu tulis di Service Provider
  • Mesin produksi = proses resolve yang berjalan rekursif
  • Pesanan = type-hint di constructor atau app()->make(...)

🔍 Auto-resolve — Resolve Tanpa Instruksi

Kemampuan paling mendasar Container adalah autowiring — kemampuan membuat object kelas konkret tanpa kamu mendaftarkan apapun.

Cara kerjanya: Container membaca constructor sebuah kelas menggunakan PHP Reflection API, melihat type-hint tiap parameternya, lalu membuat dependensi itu satu per satu secara rekursif.

// Tidak perlu daftarkan apapun — Container resolve sendiri
class ArticleRepository
{
    // Tidak ada constructor — tidak ada dependensi
}

class ArticleService
{
    public function __construct(
        private ArticleRepository $repository  // ← Container baca ini
    ) {}
}

class ArticleController extends Controller
{
    public function __construct(
        private ArticleService $service  // ← Container baca ini
    ) {}
}

Yang terjadi di balik layar ketika request masuk ke ArticleController:

Container dapat permintaan: "buat ArticleController"
  ↓
Baca constructor ArticleController → butuh ArticleService
  ↓
Baca constructor ArticleService → butuh ArticleRepository
  ↓
Baca constructor ArticleRepository → tidak butuh apa-apa
  ↓
Buat ArticleRepository ✓
  ↓
Buat ArticleService(repository: ArticleRepository) ✓
  ↓
Buat ArticleController(service: ArticleService) ✓

📌 Proses ini rekursif — Container menyelesaikan dependensi dari dalam ke luar, seperti mengupas bawang dari tengah. Dan semua ini terjadi hanya karena kamu nulis type-hint yang benar. Tidak ada konfigurasi tambahan.

PHP Reflection API — Alat yang Dipakai

Container menggunakan ReflectionClass bawaan PHP untuk “membaca” struktur kelas tanpa menjalankannya:

// Ini yang dilakukan Container di balik layar (disederhanakan)
$reflection  = new ReflectionClass(ArticleService::class);
$constructor = $reflection->getConstructor();
$parameters  = $constructor->getParameters();

foreach ($parameters as $param) {
    $type = $param->getType()->getName(); // → "App\Repositories\ArticleRepository"
    $dependencies[] = $this->make($type); // → resolve rekursif
}

return $reflection->newInstanceArgs($dependencies);

📌 Itulah kenapa type-hint wajib ditulis dengan nama kelas/interface yang benar. Kalau parameternya $repo tanpa type-hint, Container tidak tahu harus inject apa — dan akan lempar error.


📋 Binding — Resolve dengan Instruksi

Auto-resolve tidak bisa bekerja untuk interface — karena interface tidak bisa di-instansiasi. Container butuh instruksi eksplisit: “kalau diminta interface ini, buat implementasi itu.”

Ini yang disebut binding.

bind — Baru Tiap Kali

// Setiap kali ada yang minta ArticleRepositoryInterface,
// Container buat EloquentArticleRepository yang baru
$this->app->bind(
    ArticleRepositoryInterface::class,
    EloquentArticleRepository::class
);

singleton — Buat Sekali, Pakai Terus

// Container buat ArticleService sekali saja.
// Semua yang minta ArticleService dapat object yang sama persis.
$this->app->singleton(ArticleService::class);

// Untuk interface → implementasi, sintaksnya sama:
$this->app->singleton(
    CacheRepositoryInterface::class,
    RedisCacheRepository::class
);

bind dengan Closure — Kontrol Penuh

Kadang kamu butuh kontrol lebih atas bagaimana object dibuat — misalnya perlu inject konfigurasi atau kondisi tertentu:

$this->app->bind(ArticleRepositoryInterface::class, function ($app) {
    // $app adalah Container itu sendiri — bisa resolve dependensi lain
    return new EloquentArticleRepository(
        $app->make(DatabaseManager::class),
        config('blog.per_page')  // inject nilai dari konfigurasi
    );
});

instance — Daftarkan Object yang Sudah Jadi

// Kalau object sudah kamu buat sebelumnya dan mau didaftarkan
$config = new BlogConfig(['per_page' => 10, 'allow_guest_comments' => false]);
$this->app->instance('blog.config', $config);

// Nanti bisa diambil dengan:
$config = app('blog.config');

📌 Perbedaan bind dan singleton yang paling mudah diingat: kalau kelas menyimpan state yang berubah antar penggunaan, pakai bind — supaya tiap caller dapat object yang bersih. Kalau tidak ada state yang perlu di-reset, pakai singleton — lebih hemat memori karena object tidak dibuat ulang.


🎯 Contextual Binding — Inject Berbeda untuk Kelas Berbeda

Ini fitur yang jarang dibahas tapi sangat berguna: Container bisa inject implementasi berbeda ke kelas yang berbeda, meskipun keduanya minta interface yang sama.

Studi kasus: ArticleController butuh FileStorageInterface yang simpan ke disk lokal. ImageUploadController butuh FileStorageInterface yang sama, tapi harus simpan ke S3.

// Di AppServiceProvider::register()
$this->app
    ->when(ArticleController::class)          // ← khusus untuk kelas ini
    ->needs(FileStorageInterface::class)      // ← ketika butuh interface ini
    ->give(LocalFileStorage::class);          // ← berikan implementasi ini

$this->app
    ->when(ImageUploadController::class)      // ← kelas berbeda
    ->needs(FileStorageInterface::class)      // ← minta interface yang sama
    ->give(S3FileStorage::class);             // ← dapat implementasi berbeda

Kedua controller tetap type-hint dengan interface yang sama — tidak ada perubahan di sana:

class ArticleController extends Controller
{
    public function __construct(
        private FileStorageInterface $storage  // ← dapat LocalFileStorage
    ) {}
}

class ImageUploadController extends Controller
{
    public function __construct(
        private FileStorageInterface $storage  // ← dapat S3FileStorage
    ) {}
}

📌 Contextual binding sangat berguna ketika satu interface punya banyak implementasi dan kamu butuh kontrol tepat siapa dapat apa — tanpa harus bikin interface baru hanya untuk membedakan keduanya.

Contextual Binding untuk Nilai Primitif

Contextual binding juga bekerja untuk nilai non-object seperti string atau integer:

// ArticleService dapat 10 item per halaman
$this->app
    ->when(ArticleService::class)
    ->needs('$perPage')       // ← cocok dengan nama parameter di constructor
    ->give(10);

// AdminArticleService dapat 50 item per halaman
$this->app
    ->when(AdminArticleService::class)
    ->needs('$perPage')
    ->give(50);
class ArticleService
{
    public function __construct(
        private ArticleRepositoryInterface $repository,
        private int $perPage   // ← Container inject nilai 10 di sini
    ) {}
}

🏷️ Tagging — Resolve Banyak Sekaligus

Kalau kamu punya beberapa implementasi berbeda yang semua perlu dijalankan bersamaan, kamu bisa tag mereka dan resolve semuanya sekaligus.

Studi kasus: notifikasi artikel published dikirim ke email, Slack, dan database.

// Di Service Provider — daftarkan dan beri tag yang sama
$this->app->tag([
    EmailNotificationChannel::class,
    SlackNotificationChannel::class,
    DatabaseNotificationChannel::class,
], 'notification.channels');
// Inject semua yang punya tag itu sekaligus lewat Closure
$this->app->bind(ArticlePublishedNotifier::class, function ($app) {
    return new ArticlePublishedNotifier(
        $app->tagged('notification.channels')  // ← resolve semua sekaligus
    );
});
class ArticlePublishedNotifier
{
    public function __construct(
        private iterable $channels  // ← berisi semua tiga channel
    ) {}

    public function notify(Article $article): void
    {
        foreach ($this->channels as $channel) {
            $channel->send($article);  // kirim ke semua
        }
    }
}

📌 Tagging adalah pasangan alami dari prinsip Open/Closed di SOLID. Mau tambah WhatsAppNotificationChannel besok? Cukup daftarkan dan tag — ArticlePublishedNotifier tidak perlu disentuh sama sekali.


🔁 Resolving Callback — Kode yang Jalan Setelah Object Dibuat

Kadang kamu butuh melakukan sesuatu setiap kali Container selesai membuat object tertentu:

// Setiap kali ArticleRepository berhasil dibuat,
// jalankan kode ini sebelum object diserahkan ke caller
$this->app->resolving(ArticleRepository::class, function ($repo, $app) {
    $repo->setPerPage(config('blog.per_page'));
    $repo->setOrderBy('published_at', 'desc');
});

Atau untuk semua object apapun:

// Jalankan untuk SEMUA object yang di-resolve dari container
$this->app->resolving(function ($object, $app) {
    if (method_exists($object, 'setLocale')) {
        $object->setLocale(app()->getLocale());
    }
});

🛠️ Cara Mengambil dari Container

Ada beberapa cara resolve object secara manual — masing-masing punya tempat yang tepat:

// 1. Helper app() — paling ringkas
$service = app(ArticleService::class);
$config  = app('blog.config');

// 2. Facade App::make() — identik dengan app()
use Illuminate\Support\Facades\App;
$service = App::make(ArticleService::class);

// 3. Lewat $this->app di Service Provider
$service = $this->app->make(ArticleService::class);

// 4. Type-hint — cara yang paling dianjurkan
// Container inject otomatis, tidak perlu panggil app() sama sekali
public function __construct(private ArticleService $service) {}

📌 Di kode aplikasi sehari-hari, hindari app() dan App::make() sebisa mungkin — itu membuat dependensi tersembunyi dan sulit dites. Type-hint di constructor tetap cara terbaik. app() lebih cocok dipakai di Service Provider, global helper function, atau tempat di mana constructor injection tidak tersedia.


🗺️ Gambaran Besar — Semua Cara Binding

IoC Container
│
├── Auto-resolve
│   └── Kelas konkret + type-hint benar → rekursif otomatis, tanpa daftar
│
├── bind()
│   ├── Interface → Kelas konkret    → object baru tiap kali
│   └── Interface → Closure          → kontrol penuh cara pembuatan
│
├── singleton()
│   └── Sama seperti bind(), tapi object dibuat SEKALI lalu di-cache
│
├── instance()
│   └── Daftarkan object yang sudah jadi
│
├── Contextual Binding
│   └── Interface yang sama → implementasi berbeda per kelas penerima
│
├── tag() + tagged()
│   └── Kumpulkan banyak kelas dalam satu label → resolve sekaligus
│
└── resolving()
    └── Callback yang jalan setiap kali object berhasil dibuat

Dan posisi Container di antara komponen lain yang sudah kita pelajari:

Service Provider
  → register() mendaftarkan binding ke Container
  → boot() menjalankan setup setelah semua binding siap
        ↓
  IoC Container menyimpan semua aturan dan instance
        ↓
  DI (constructor/method) → Container resolve dan inject otomatis
  Facade → Container ambil object lewat getFacadeAccessor()
  app()  → Container resolve secara manual

🧭 Kapan Pakai Yang Mana?

Auto-resolve untuk kelas konkret yang semua dependensinya juga kelas konkret. Tidak perlu daftarkan apa-apa.

bind() ketika type-hint pakai interface, atau ketika cara membuat object butuh logika kustom lewat Closure.

singleton() untuk service stateless yang berat dibuat ulang — mayoritas service di aplikasi blog cocok di sini.

Contextual Binding ketika satu interface punya implementasi berbeda untuk konteks berbeda, tanpa mau bikin interface baru hanya untuk membedakan.

tag() ketika ada banyak implementasi yang harus dijalankan semua — notifikasi multi-channel, validator pipeline, atau sistem plugin.

resolving() ketika butuh setup tambahan setelah object dibuat, tanpa ubah konstruktornya.


📊 Perbandingan Semua Jenis Binding

JenisObject DibuatButuh Daftar?Cocok untuk
Auto-resolveTiap kaliTidakKelas konkret tanpa interface
bindTiap kaliYaInterface → implementasi
singletonSekaliYaService stateless
instanceSudah adaYaObject yang sudah dibuat manual
ContextualTiap kaliYaInterface beda implementasi per kelas
tagTiap kaliYaResolve banyak implementasi sekaligus

💡 Ingat!

Waktu pertama kali belajar ini, wajar kalau terasa “ini terlalu dalam — tahu bind() dan singleton() saja sudah cukup.”

Untuk kebanyakan proyek awal, memang iya. Tapi satu hal yang paling penting untuk dibawa pulang dari artikel ini: Container bukan sihir. Dia membaca type-hint lewat PHP Reflection, mencocokkan dengan binding yang didaftarkan, membuat object dari dalam ke luar secara rekursif, lalu menyerahkannya.

Itu saja. Setiap kali kamu type-hint ArticleService $service di constructor dan object-nya muncul entah dari mana — proses itulah yang terjadi.

Share Twitter/X LinkedIn