LaravelArsitekturDIMahir4 mnt baca

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:

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

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

php
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 tinker lalu app()->bindings menampilkan semua binding terdaftar, atau resolve(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.