Chapter Dependency Injection di Controller memperkenalkan suntikan lewat type hint pada method controller. Setelah memahami container dan provider, kita siap melihat pola tersebut sebagai satu mekanisme yang berlaku di seluruh aplikasi — bukan fitur khusus controller. Inilah dependency injection (DI): kelas menyatakan apa yang dibutuhkan lewat constructor atau parameter method, dan container yang mengisi kebutuhan tersebut. Chapter ini memetakan di titik mana Laravel melakukan resolusi, kapan memilih constructor atau method, serta bagaimana DI membuat kode kita bisa diuji lewat penggantian dependency di test.
Suntikan di Mana-mana #
Setiap kelas yang di-resolve container mendapat perlakuan yang sama. Pemetaannya:
| Konteks | Bentuk suntikan yang didukung |
|---|---|
| Method controller | Constructor dan method |
handle() queue job | Method |
handle() Artisan command | Method |
| Listener event | Constructor |
| Middleware | Constructor |
| Form request | Di-instansiasi container secara penuh |
Kita buktikan tiga baris pertama dengan satu dependency yang sama — interface PusherNotifikasi yang terikat ke PusherLog di AppServiceProvider (chapter sebelumnya). Sebuah queue job menerima dependency-nya di method handle():
<?php
namespace App\Jobs;
use App\Contracts\PusherNotifikasi;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
class KirimPesanJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public string $penerima,
public string $pesan,
) {}
public function handle(PusherNotifikasi $pusher): void
{
Cache::put('hasil_notifikasi', $pusher->kirim($this->penerima, $this->pesan));
}
}Sebuah Artisan command memakai bentuk yang sama persis — dependency tinggal diletakkan di signature handle():
<?php
namespace App\Console\Commands;
use App\Contracts\PusherNotifikasi;
use Illuminate\Console\Command;
class NotifikasiCommand extends Command
{
protected $signature = 'notifikasi:kirim {penerima} {pesan}';
protected $description = 'Kirim notifikasi lewat gateway yang terikat di container';
public function handle(PusherNotifikasi $pusher): int
{
$this->info($pusher->kirim($this->argument('penerima'), $this->argument('pesan')));
return self::SUCCESS;
}
}Listener event, sebaliknya, harus memakai constructor — kode sumber dispatcher memanggil handle() listener sinkron hanya dengan event-nya, tanpa resolusi parameter tambahan:
<?php
namespace App\Listeners;
use App\Contracts\PusherNotifikasi;
use App\Events\PostDiterbitkan;
use Illuminate\Support\Facades\Cache;
class KirimNotifikasiPost
{
public function __construct(
protected PusherNotifikasi $pusher,
) {}
public function handle(PostDiterbitkan $event): void
{
Cache::put('notifikasi_post', $this->pusher->kirim(
$event->post->user->email,
'Post baru diterbitkan',
));
}
}Satu feature test menguji ketiganya sekaligus — job di-dispatch sinkron, event ditembak langsung, command dijalankan via artisan:
<?php
namespace Tests\Feature;
use App\Events\PostDiterbitkan;
use App\Jobs\KirimPesanJob;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class DiEverywhereTest extends TestCase
{
use RefreshDatabase;
public function test_job_menerima_dependency_di_method_handle(): void
{
dispatch_sync(new KirimPesanJob('[email protected]', 'Pesanan dikirim'));
$this->assertSame(
'[log] notifikasi untuk [email protected]: Pesanan dikirim',
Cache::get('hasil_notifikasi'),
);
}
public function test_command_menerima_dependency_di_method_handle(): void
{
$this->artisan('notifikasi:kirim', [
'penerima' => '[email protected]',
'pesan' => 'Pesanan dikirim',
])
->expectsOutputToContain('[log] notifikasi untuk [email protected]: Pesanan dikirim')
->assertSuccessful();
}
// test listener + test container swap menyusul di bawah
}Output yang Dihasilkan:
PASS Tests\Feature\DiEverywhereTest
✓ job menerima dependency di method handle 0.22s
✓ command menerima dependency di method handle 0.01s
✓ listener menerima dependency di method handle 0.04s
✓ container swap menukar gateway dengan test double 0.02s
Tests: 4 passed (6 assertions)
Duration: 0.41sPilihan antara constructor dan method mengikuti cakupan pemakaian: dependency yang dipakai banyak method milik constructor, dependency yang hanya relevan untuk satu operasi lebih jelas dinyatakan sebagai parameter method itu sendiri. Keduanya boleh hadir bersamaan dalam satu kelas.
Bau Konstruksi Manual #
Sebelum DI, cara klasik mendapat dependency adalah new:
// Contoh tampilan — pola yang perlu kita hindari
public function simpan(Request $request)
{
$gateway = new GatewaySandbox(); // dependensi dikunci di dalam method
$hasil = $gateway->charge($request->input('kartu'), 150000);
// ...
}Kode tersebut bekerja, tetapi simpan() memilih implementasinya sendiri — test tidak punya jalan masuk untuk mengganti $gateway tanpa memonitor eksternal. Dependency yang dinyatakan lewat signature, sebaliknya, adalah seam: satu titik resmi tempat container (dan test) menyuntik objek. Aturan praktisnya sederhana: kelas Laravel yang di-resolve container tidak perlu new untuk dependency-nya sendiri.
Anti-Pola Service Locator #
Kalau suntikan bagus, memanggil container dari mana saja harus lebih fleksibel, kan? Tidak — pola app()->make(PusherNotifikasi::class) di dalam method justru menyembunyikan dependency: signature berbohong tentang kebutuhan kelas, ketergantungan pada container menyebar ke seluruh kode, dan tanpa framework kelas tersebut tidak bisa dites. Perbandingannya:
| Aspek | Dependency injection | Service locator (app()->make) |
|---|---|---|
| Kebutuhan kelas terlihat di | Signature (constructor/method) | Tubuh method, tersembunyi |
| Bisa diganti di test | Ya, lewat container swap | Sulit tanpa mock global |
| Ketergantungan pada container | Tidak ada | Di setiap pemanggilan |
app() tetap sah sebagai alat framework — di provider, file routes/web.php, atau skrip runner seperti yang kita pakai di chapter container. Batasnya: jangan menjadikannya cara kelas bisnis mengambil dependency-nya sendiri.
Menukar Dependency di Test #
Test double masuk lewat seam yang sama. $this->instance() pada test mengganti binding container — route /pembayaran/charge dari chapter provider mendadak memakai gateway palsu:
public function test_container_swap_menukar_gateway_dengan_test_double(): void
{
$budi = User::factory()->create();
$this->instance(PaymentGateway::class, new GatewayPalsu);
$response = $this->actingAs($budi)->post('/pembayaran/charge', [
'kartu' => '4242424242424242',
'jumlah' => 150000,
]);
$response->assertOk();
$response->assertSee('PALSU: charge tidak benar-benar dieksekusi');
}GatewayPalsu hanyalah kelas kecil yang mengimplementasikan PaymentGateway dan mengembalikan string tetap. Test ini adalah bukti paling kuat dari nilai DI: satu baris $this->instance() (atau $this->swap() yang setara) mengubah perilaku seluruh request tanpa menyentuh kode produksi. Lebih jauh tentang test double ada di dokumentasi mocking
.
Kita kini bisa menyuntik dependency ke kelas mana pun yang di-resolve Laravel. Pertanyaan berikutnya sudah bukan bagaimana menyuntik, tetapi ke kelas apa — dan jawabannya adalah service class, wadah logika bisnis yang menjadi tulang punggung aplikasi.