Baris new PenggunaController(new LayananPendaftaran(new RepositoriPenggunaSqlite())) yang menutup chapter Controller-Service-Repository adalah dependency injection dalam bentuknya yang paling murni: object dibuat di luar, lalu diserahterimakan kepada object yang membutuhkannya. Dependency injection (DI) adalah teknik menyuplai dependency — object yang dibutuhkan class lain untuk bekerja — dari luar, bukan dibuat sendiri di dalam class. Teknik ini terdengar sederhana, tetapi dampaknya besar: class menjadi longgar (loose coupling), implementasinya mudah ditukar, dan yang paling terasa, mudah diuji tanpa menyentuh infrastruktur nyata seperti SMTP atau database. Pada chapter ini kita bedah teknik tersebut dari masalah asalnya sampai pola penyambungan manual tanpa framework sekalipun.

Apa Itu Dependency Injection #

Dalam istilah arsitektur, dependency adalah object yang dibutuhkan class lain untuk menyelesaikan pekerjaannya. Service pendaftaran membutuhkan repository untuk menyimpan data, layanan pesanan membutuhkan mailer untuk mengirim konfirmasi, dan controller membutuhkan service untuk menjalankan use case. Dependency injection adalah teknik menyuplai kebutuhan tersebut dari luar: pemanggil yang membuat object dependency, lalu menyerahkannya lewat constructor atau method. Class penerima tidak pernah tahu dan tidak pernah perlu peduli bagaimana dependency itu dibuat.

DI memiliki tiga bentuk klasik: constructor injection (lewat constructor), setter injection (lewat method setter), dan interface injection (dependency diberi tahu siapa penyuntiknya — bentuk yang hampir tidak pernah dipakai di ekosistem PHP). Constructor injection adalah bentuk dominan dan menjadi fokus chapter ini. Alasannya sederhana: object yang seluruh kebutuhannya terpenuhi sejak baris new tidak pernah berada dalam keadaan setengah jadi.

Masalah: Dependency yang Dikunci di Dalam Class #

Mari kita mulai dari versi yang bermasalah. Class berikut menangani notifikasi pesanan, tetapi membuat mailer-nya sendiri di dalam method:

php
<?php
final class LayananPesananKaku
{
    public function proses(string $email, int $totalTagihan): void
    {
        // Dependency dibuat di DALAM method: pilihan implementasi dikunci di sini.
        $mailer = new MailerSmtp("smtp.example.com", 587);
        $mailer->kirim($email, "Pesanan Anda diterima, total Rp$totalTagihan");
    }
}

Kode ini dapat berjalan, tetapi tiga keputusan besar telah dikunci di dalam method proses(). Pertama, implementasi notifikasi pasti MailerSmtp — tidak ada cara memakai server sandbox saat pengembangan tanpa mengedit class. Kedua, alamat smtp.example.com:587 tertanam keras, padahal nilai seperti itu layak berubah antar lingkungan. Ketiga, pengujian otomatis menjadi mahal: setiap kali aturan pesanan diuji, email sungguhan ikut terkirim. Masalahnya bukan pada MailerSmtp sebagai class, melainkan pada siapa yang memegang keputusan memilihnya.

Constructor Injection dengan Constructor Promotion #

Versi yang diperbaiki memindahkan keputusan itu ke luar. LayananPesanan tidak lagi membuat mailer; class ini hanya menyatakan kebutuhannya lewat parameter constructor, lalu bekerja dengan implementasi apa pun yang dipasokkan pemanggil:

php
<?php
interface PengirimNotifikasi
{
    public function kirim(string $tujuan, string $isi): string;
}

final class MailerSmtp implements PengirimNotifikasi
{
    public function __construct(
        private readonly string $host = "smtp.example.com",
        private readonly int $port = 587,
    ) {
    }

    public function kirim(string $tujuan, string $isi): string
    {
        // Dunia nyata: buka koneksi SMTP ke $this->host:$this->port,
        // lalu kirim $isi sebagai email ke $tujuan.
        return sprintf("[SMTP %s:%d] terkirim ke %s", $this->host, $this->port, $tujuan);
    }
}

final class LayananPesanan
{
    public function __construct(private readonly PengirimNotifikasi $notifikasi)
    {
    }

    public function proses(string $email, int $totalTagihan): string
    {
        $isi = "Pesanan Anda diterima, total Rp"
            . number_format($totalTagihan, 0, ",", ".");
        $buktiKirim = $this->notifikasi->kirim($email, $isi);

        return "pesanan diproses; bukti kirim: " . $buktiKirim;
    }
}

Mari kita bedah peran tiap bagian di atas:

  1. interface PengirimNotifikasi mendefinisikan kontrak “sesuatu yang bisa mengirim notifikasi”. LayananPesanan menulis type hint ke interface ini, bukan ke class konkret — keputusan arsitektural yang dibedah tuntas pada chapter Dependency Inversion .
  2. MailerSmtp::__construct() memakai constructor property promotion: parameter constructor sekaligus menjadi property (private readonly string $host). Fitur ini telah kita pelajari pada chapter Constructor Property Promotion .
  3. LayananPesanan::__construct(private readonly PengirimNotifikasi $notifikasi) adalah satu baris yang mendefinisikan seluruh hubungan: dependency disuntikkan dari luar, disimpan sebagai property readonly, dan siap dipakai oleh semua method.
  4. proses() hanya memanggil $this->notifikasi->kirim() tanpa pernah memeriksa implementasi aslinya — SMTP, log, atau object tiruan sama-sama diperlakukan.

Seluruh definisi di atas tersimpan sebagai berkas notifikasi.php pada bundle chapter ini. Pemakaiannya tampak seperti ini:

php
<?php
require __DIR__ . "/notifikasi.php";

$layanan = new LayananPesanan(new MailerSmtp());
echo $layanan->proses("[email protected]", 250000), "\n";

Output yang Dihasilkan:

text
pesanan diproses; bukti kirim: [SMTP smtp.example.com:587] terkirim ke [email protected]

Baris new LayananPesanan(new MailerSmtp()) melakukan seluruh penyuntikan: mailer dibuat lebih dahulu, lalu diserahterimakan ke layanan. Ketika proses() berjalan, MailerSmtp::kirim() menghasilkan string bukti kirim yang kemudian ditampilkan layanan. Pada aplikasi nyata string bukti itu tidak dicetak, melainkan ditulis ke log — di sini kita menampilkannya agar hasil eksekusi dapat dilihat langsung.

Manfaat Nyata: Menukar Dependency saat Menguji #

Manfaat DI paling terasa pada saat menguji. Karena constructor hanya menuntut “sesuatu yang mengimplementasikan PengirimNotifikasi”, kita bebas memasang versi palsu yang menangkap pesan ke dalam array alih-alih mengirimkannya:

php
<?php
require __DIR__ . "/notifikasi.php";

final class MailerTiruan implements PengirimNotifikasi
{
    /** @var list<string> */
    public array $kotakKeluar = [];

    public function kirim(string $tujuan, string $isi): string
    {
        $this->kotakKeluar[] = "$tujuan <= $isi";

        return "[TIRUAN] tidak benar-benar terkirim";
    }
}

$fake = new MailerTiruan();
$layanan = new LayananPesanan($fake);

$layanan->proses("[email protected]", 175000);

printf("pesan tertangkap: %d\n", count($fake->kotakKeluar));
echo $fake->kotakKeluar[0], "\n";

Output yang Dihasilkan:

text
pesan tertangkap: 1
[email protected] <= Pesanan Anda diterima, total Rp175.000

MailerTiruan adalah contoh test double — object yang sengaja dibuat sederhana untuk keperluan pengujian, seperti RepositoriPenggunaArray pada chapter sebelumnya. Object palsu ini tidak membuka koneksi, tidak membutuhkan berkas, dan berjalan dalam hitungan mikrodetik. Setelah proses() dipanggil, isi $fake->kotakKeluar dapat diperiksa untuk memastikan notifikasi memang ditujukan ke Siti dengan total yang benar — pengujian berbasis pemeriksaan seperti ini akan kita perdalam bersama PHPUnit pada section Testing dan Quality Assurance . Perhatikan pula bahwa LayananPesanan tidak diubah satu baris pun; kemampuan bertukar implementasi inilah yang membuat service mudah diuji.

DI Manual Tanpa Framework #

DI sering diidentikkan dengan framework, padahal tekniknya murni bahasa PHP. Yang diberikan framework hanyalah otomatisasi berupa dependency injection container yang membaca type hint constructor lalu menyusun object graph secara otomatis. Sebelum object graph membesar, penyambungan manual di titik masuk aplikasi sudah lebih dari cukup:

php
<?php
require __DIR__ . "/notifikasi.php";

// Semua keputusan "implementasi mana yang dipakai" terkumpul di satu tempat.
$mode = "produksi"; // di dunia nyata: dibaca dari environment atau file konfigurasi

$pengirim = match ($mode) {
    "produksi" => new MailerSmtp("smtp.example.com", 587),
    "pengembangan" => new MailerSmtp("localhost", 1025),
};

$layanan = new LayananPesanan($pengirim);
echo $layanan->proses("[email protected]", 90000), "\n";

Output yang Dihasilkan:

text
pesanan diproses; bukti kirim: [SMTP smtp.example.com:587] terkirim ke [email protected]

Seluruh keputusan pemilihan implementasi kini hidup pada satu tempat: variabel $mode dibaca, ekspresi match memilih mailer produksi atau pengembangan, dan LayananPesanan menerima object yang sudah jadi. Menambah lingkungan testing cukup dengan menambah satu arm pada match — service tetap tak tersentuh. Cara membaca konfigurasi lingkungan seperti $mode dibahas tuntas pada chapter Configuration Management , dan otomatisasi penyambungannya pada chapter Dependency Container di section Framework Development.

Setter Injection, Pilihan yang Jarang #

Bentuk kedua DI menyuntikkan dependency lewat method setter setelah object lahir. PHP memperbolehkannya, tetapi wilayah pakainya sempit:

php
<?php
final class PelacakAcara
{
    private ?PengirimNotifikasi $notifikasi = null;

    public function setNotifikasi(PengirimNotifikasi $notifikasi): void
    {
        $this->notifikasi = $notifikasi;
    }

    public function catat(string $acara): void
    {
        // $this->notifikasi bisa saja null: wajib dicek setiap kali dipakai
        $this->notifikasi?->kirim("[email protected]", $acara);
    }
}

Perbandingan kedua bentuk tersebut dapat dirangkum begini:

AspekConstructor injectionSetter injection
Kesiapan objectsiap pakai sejak newbisa setengah jadi sampai setter dipanggil
Dependency wajibtempat alamimemaksa pemeriksaan null di banyak tempat
Dependency opsionalmemaksa parameter bernilai nulltempat alami (plugin, fitur tambahan)
Penggunaan umumservice, repository, controllerkomponen dengan fitur sukarela

DI, DIP, dan Container: Tiga Istilah yang Sering Tertukar #

Tiga istilah ini sering dipakai bergantian padahal menjawab pertanyaan yang berbeda. Pemetaannya begini:

IstilahJenisPertanyaan yang dijawab
Dependency Injectionteknikbagaimana object mendapatkan dependency-nya?
Dependency Inversionprinsip arsitekturdependency seharusnya menunjuk ke mana?
Dependency Injection Containeralatsiapa yang menulis kode penyambungannya?

Ringkasan #

Dependency injection memindahkan keputusan memilih dependency dari dalam class ke tangan pemanggil: constructor menyatakan kebutuhan, constructor property promotion membuat deklarasinya ringkas, dan test double membuktikan manfaatnya tanpa infrastruktur apa pun. DI manual di titik masuk aplikasi memadai untuk sebagian besar proyek, sementara setter injection cukup disimpan untuk dependency yang benar-benar opsional. Satu pertanyaan belum terjawab: ke arah apa seharusnya dependency itu menunjuk — class konkret atau abstraksi? Chapter Dependency Inversion membalik panah itu dan menunjukkan mengapa aturan bisnis tidak seharusnya menunggu pada detail SQL.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

notifikasi.php

Unduh berkas
php
<?php

// Bundle chapter Dependency Injection: definisi layanan pesanan dan mailer.

interface PengirimNotifikasi
{
    public function kirim(string $tujuan, string $isi): string;
}

final class MailerSmtp implements PengirimNotifikasi
{
    public function __construct(
        private readonly string $host = "smtp.example.com",
        private readonly int $port = 587,
    ) {
    }

    public function kirim(string $tujuan, string $isi): string
    {
        // Dunia nyata: buka koneksi SMTP ke $this->host:$this->port,
        // lalu kirim $isi sebagai email ke $tujuan.
        return sprintf("[SMTP %s:%d] terkirim ke %s", $this->host, $this->port, $tujuan);
    }
}

final class LayananPesanan
{
    public function __construct(private readonly PengirimNotifikasi $notifikasi)
    {
    }

    public function proses(string $email, int $totalTagihan): string
    {
        $isi = "Pesanan Anda diterima, total Rp"
            . number_format($totalTagihan, 0, ",", ".");
        $buktiKirim = $this->notifikasi->kirim($email, $isi);

        return "pesanan diproses; bukti kirim: " . $buktiKirim;
    }
}