Mengapa Memahami Arsitektur Ini Membuat Anda Siap Bekerja di Proyek Mana Pun?
Baik Anda bekerja menggunakan PHP murni, Slim, Symfony, maupun Laravel, seluruh aplikasi web PHP modern di industri dibangun di atas tiga pilar arsitektur yang distandarkan oleh PHP-FIG:
- Representasi Pesan HTTP (PSR-7) & Pipa Middleware (PSR-15): Permintaan HTTP mengalir melewati lapisan-lapisan pemeriksaan (seperti autentikasi, proteksi CSRF, pembatasan laju, dan pencatatan log) sebelum tiba di Controller.
- Routing: Memetakan pasangan metode HTTP + jalur URI (
GET /kelas/php) ke penangan (handler/controller) yang tepat. - Dependency Injection (DI) & Container (PSR-11): Menyuntikkan objek yang dibutuhkan oleh sebuah kelas melalui konstruktornya, bukan membuat koneksi database atau layanan sendiri di dalam kelas tersebut!
1. Apa Itu Dependency Injection (DI) dan Mengapa new di Dalam Layanan Berbahaya?
Perhatikan perbedaan antara kelas yang **terikat kaku (tightly coupled) dan kelas yang menggunakan Dependency Injection**:
<?php
declare(strict_types=1);
// 🚫 BURUK (Tightly Coupled):
// PendaftaranService membuat sendiri koneksi PDO dan pengirim email di dalamnya.
// Akibatnya: Anda TIDAK BISA menguji PendaftaranService tanpa database produksi dan server SMTP nyata!
class PendaftaranServiceBuruk
{
public function daftar(string $surel): void
{
$pdo = new PDO('pgsql:host=db.prod;dbname=utama', 'admin', 'rahasia'); // Terkunci mati!
// ...
}
}
// ✅ BENAR (Dependency Injection melalui Constructor + Interface!):
interface PengirimSurelInterface
{
public function kirim(string $tujuan, string $subjek, string $isi): void;
}
final readonly class PendaftaranService
{
// Kelas ini TIDAK PEDULI apakah $pdo terhubung ke PostgreSQL produksi atau SQLite in-memory,
// dan TIDAK PEDULI apakah $pengirimSurel benar-benar mengirim lewat SMTP atau hanya mock pengujian!
public function __construct(
private PDO $pdo,
private PengirimSurelInterface $pengirimSurel,
) {}
public function daftar(string $surel): void
{
$stmt = $this->pdo->prepare('INSERT INTO peserta (surel) VALUES (:surel)');
$stmt->execute(['surel' => $surel]);
$this->pengirimSurel->kirim($surel, 'Selamat Datang di Kodeloka', 'Pendaftaran berhasil!');
}
}
Keuntungan Besar Dependency Injection:
- **Mudah Diuji (Testable):** Saat menjalankan PHPUnit, Anda cukup menyuntikkan
new PDO('sqlite::memory:')dan sebuah kelasPengirimSurelPalsu implements PengirimSurelInterfaceyang mencatat surel ke dalam array memori tanpa mengirim surel sungguhan! - **Mudah Diganti (Swappable):** Jika bulan depan perusahaan berpindah penyedia layanan surel, Anda cukup membuat kelas baru yang mengimplementasikan
PengirimSurelInterfacetanpa mengubah satu huruf pun di dalamPendaftaranService!
2. Pola Pipa Middleware (Konsep PSR-15)
Bayangkan Middleware seperti lapisan-lapisan kulit bawang yang mengelilingi Controller inti Anda:
Permintaan HTTP Masuk │ ▼ [ Middleware 1: Pencatat Log & Waktu Eksekusi ] │ ▼ [ Middleware 2: Pemeriksa Sesi & CSRF ] ──(Jika tidak sah)──> Langsung balas 403 Forbidden! │ ▼ [ Controller Inti: Mengambil Data & Merender Respons ] │ ▼ [ Kembali melewati Middleware 2 -> Middleware 1 (Menambahkan header X-Response-Time) ] │ ▼ Respons HTTP Keluar ke Peramban
Mari kita lihat betapa sederhananya mengimplementasikan arsitektur Request, Response, dan Middleware Pipeline dalam PHP modern:
<?php
declare(strict_types=1);
namespace Kodeloka\Http;
readonly class Request
{
public function __construct(
public string $metode,
public string $jalur,
public array $query = [],
public array $body = [],
public array $header = [],
) {}
}
readonly class Response
{
public function __construct(
public string $isi,
public int $status = 200,
public array $header = ['Content-Type' => 'text/html; charset=utf-8'],
) {}
}
interface MiddlewareInterface
{
/**
* @param callable(Request): Response $lanjut
*/
public function proses(Request $request, callable $lanjut): Response;
}
// Contoh Middleware Pencatat Waktu Eksekusi:
final class WaktuEksekusiMiddleware implements MiddlewareInterface
{
public function proses(Request $request, callable $lanjut): Response
{
$mulai = hrtime(true);
// Teruskan request ke lapisan berikutnya di dalam pipa
$respons = $lanjut($request);
$durasiMs = round((hrtime(true) - $mulai) / 1_000_000, 2);
$headerBaru = [...$respons->header, 'X-Waktu-Proses-Ms' => (string) $durasiMs];
// Properti readonly publik hanya boleh diubah dari dalam class Response sendiri
// (clone($respons, [...]) dari luar class akan memicu Error), sehingga kita membuat instans baru:
return new Response(
isi: $respons->isi,
status: $respons->status,
header: $headerBaru,
);
}
}
3. Merakit Dependency Injection Container (PSR-11) Sederhana
Agar Anda tidak perlu merangkai new PendaftaranService(new PDO(...), new SmtpMailer(...)) berulang kali di setiap rute, kita menggunakan Service Container yang menyimpan resep pembuatan objek (factories) dan instans shared/singleton:
<?php
declare(strict_types=1);
final class Container
{
/** @var array<string, callable(self): object> */
private array $resep = [];
/** @var array<string, object> */
private array $instansTunggal = [];
public function daftarkan(string $id, callable $pembuat): void
{
$this->resep[$id] = $pembuat;
}
public function ambil(string $id): object
{
// Jika sudah pernah dibuat sebelumnya, kembalikan instans yang sama
if (isset($this->instansTunggal[$id])) {
return $this->instansTunggal[$id];
}
if (!isset($this->resep[$id])) {
throw new RuntimeException("Layanan tidak terdaftar di Container: {$id}");
}
$this->instansTunggal[$id] = ($this->resep[$id])($this);
return $this->instansTunggal[$id];
}
}
Catatan: Container di atas hanyalah ilustrasi. Standar PSR-11 mensyaratkan class yang mengimplementasikan Psr\Container\ContainerInterface dengan metode get(string $id) dan has(string $id), serta melempar exception yang mengimplementasikan NotFoundExceptionInterface untuk id yang tidak ditemukan. Demikian pula, antarmuka middleware pada contoh sebelumnya adalah penyederhanaan konsep; PSR-15 memakai Psr\Http\Server\MiddlewareInterface::process(ServerRequestInterface, RequestHandlerInterface).
Latihan Praktik: Membangun Router & Pipa Middleware Mini
Buatlah berkas pipa-middleware.php dan jalankan dengan php pipa-middleware.php:
- Buat
OtorisasiMiddleware implements MiddlewareInterfaceyang memeriksa apakah$request->header['X-Api-Key'] === 'rahasia-kodeloka'(jika salah, langsung kembalikannew Response('Akses Ditolak', 401)tanpa memanggil$lanjut($request)!). - Rangkai
WaktuEksekusiMiddleware->OtorisasiMiddleware->Controllerdan uji dengan request yang memiliki kunci API benar maupun salah.