Interface #
Interface adalah salah satu alat paling penting dalam desain perangkat lunak PHP — bukan karena sintaksnya yang rumit, tapi karena apa yang direpresentasikannya: kontrak. Saat sebuah fungsi menerima parameter bertipe interface, fungsi tersebut tidak peduli dengan implementasi spesifiknya. Sebuah Logger bisa menulis ke file, ke database, ke Slack, atau tidak ke mana-mana — selama mengimplementasikan interface yang sama, fungsi pemanggilnya tidak perlu berubah. Ini adalah inti dari Dependency Inversion Principle, salah satu prinsip desain paling berpengaruh dalam rekayasa perangkat lunak. Artikel ini membahas interface PHP secara menyeluruh: sintaks dan aturannya, interface bawaan PHP yang sering digunakan, pewarisan interface, perbedaan dengan abstract class, dan bagaimana interface membuat kode lebih mudah di-test dan diubah.
Apa Itu Interface dan Mengapa Penting #
Interface mendefinisikan apa yang harus bisa dilakukan sebuah objek, tanpa mendefinisikan bagaimana ia melakukannya. Bayangkan stopkontak listrik — interface-nya adalah dua atau tiga lubang dengan spesifikasi tegangan tertentu. Apakah listriknya berasal dari PLN, panel surya, atau generator diesel tidak relevan bagi perangkat yang ditancapkan.
flowchart LR
A[Fungsi / Kelas Pemanggil] -- "tipe: LoggerInterface" --> B[LoggerInterface]
B --> C[FileLogger\nimplements LoggerInterface]
B --> D[DatabaseLogger\nimplements LoggerInterface]
B --> E[SlackLogger\nimplements LoggerInterface]
B --> F[NullLogger\nimplements LoggerInterface]
style B fill:#fef9c3,stroke:#ca8a04
style A fill:#dbeafePemanggil bergantung pada interface (abstraksi), bukan pada implementasi konkret. Ini memungkinkan kamu mengganti implementasi tanpa mengubah satu baris kode pemanggil.
Mendefinisikan Interface #
Interface didefinisikan dengan keyword interface. Semua method di dalamnya otomatis public dan abstract — tidak perlu (dan tidak boleh) ditulis eksplisit. Interface tidak bisa memiliki implementasi method, property instance, atau constructor.
<?php
interface LoggerInterface
{
// Semua method di interface otomatis public abstract
public function log(string $level, string $pesan, array $konteks = []): void;
public function info(string $pesan, array $konteks = []): void;
public function warning(string $pesan, array $konteks = []): void;
public function error(string $pesan, array $konteks = []): void;
}
interface StorageInterface
{
public function simpan(string $kunci, mixed $nilai, int $ttl = 0): bool;
public function ambil(string $kunci): mixed;
public function hapus(string $kunci): bool;
public function ada(string $kunci): bool;
}
interface SerializableInterface
{
public function toArray(): array;
public function toJson(): string;
}
Aturan Interface #
<?php
interface ContohInterface
{
// ✓ Konstanta — diperbolehkan
const VERSI = '1.0';
// ✓ Method signature — harus public
public function methodPublik(): void;
// ✗ Method private/protected — tidak diperbolehkan
// private function methodPrivat(): void;
// protected function methodProtected(): void;
// ✗ Property instance — tidak diperbolehkan
// public string $nama;
// ✗ Constructor — tidak diperbolehkan
// public function __construct();
// ✗ Implementasi method — tidak diperbolehkan
// public function methodDenganBody(): void { echo "ini tidak valid"; }
}
Mengimplementasikan Interface #
Kelas menggunakan keyword implements untuk menyatakan bahwa ia memenuhi kontrak interface. Kelas harus mengimplementasikan semua method yang didefinisikan interface, jika tidak PHP melempar fatal error.
<?php
interface LoggerInterface
{
public function log(string $level, string $pesan, array $konteks = []): void;
public function info(string $pesan, array $konteks = []): void;
public function warning(string $pesan, array $konteks = []): void;
public function error(string $pesan, array $konteks = []): void;
}
// Implementasi ke file
class FileLogger implements LoggerInterface
{
public function __construct(
private string $path,
private string $format = '[{waktu}][{level}] {pesan}',
) {}
public function log(string $level, string $pesan, array $konteks = []): void
{
$baris = strtr($this->format, [
'{waktu}' => date('Y-m-d H:i:s'),
'{level}' => strtoupper($level),
'{pesan}' => $pesan,
]) . "\n";
file_put_contents($this->path, $baris, FILE_APPEND | LOCK_EX);
}
public function info(string $pesan, array $konteks = []): void
{
$this->log('info', $pesan, $konteks);
}
public function warning(string $pesan, array $konteks = []): void
{
$this->log('warning', $pesan, $konteks);
}
public function error(string $pesan, array $konteks = []): void
{
$this->log('error', $pesan, $konteks);
}
}
// Implementasi ke database
class DatabaseLogger implements LoggerInterface
{
public function __construct(private \PDO $db) {}
public function log(string $level, string $pesan, array $konteks = []): void
{
$stmt = $this->db->prepare(
"INSERT INTO logs (level, pesan, konteks, created_at)
VALUES (?, ?, ?, NOW())"
);
$stmt->execute([$level, $pesan, json_encode($konteks)]);
}
public function info(string $pesan, array $konteks = []): void
{
$this->log('info', $pesan, $konteks);
}
public function warning(string $pesan, array $konteks = []): void
{
$this->log('warning', $pesan, $konteks);
}
public function error(string $pesan, array $konteks = []): void
{
$this->log('error', $pesan, $konteks);
}
}
// Implementasi kosong — untuk testing (tidak melakukan apa-apa)
class NullLogger implements LoggerInterface
{
public function log(string $level, string $pesan, array $konteks = []): void {}
public function info(string $pesan, array $konteks = []): void {}
public function warning(string $pesan, array $konteks = []): void {}
public function error(string $pesan, array $konteks = []): void {}
}
Interface sebagai Type Hint #
Kekuatan interface sebenarnya muncul saat digunakan sebagai type hint. Fungsi yang menerima LoggerInterface bekerja dengan semua implementasinya:
<?php
class OrderService
{
// Bergantung pada abstraksi (interface), bukan implementasi konkret
public function __construct(
private LoggerInterface $logger,
private \PDO $db,
) {}
public function buatOrder(array $data): int
{
$this->logger->info("Membuat order baru", ['data' => $data]);
try {
$stmt = $this->db->prepare(
"INSERT INTO orders (user_id, total) VALUES (?, ?)"
);
$stmt->execute([$data['user_id'], $data['total']]);
$orderId = (int) $this->db->lastInsertId();
$this->logger->info("Order berhasil dibuat", ['id' => $orderId]);
return $orderId;
} catch (\Exception $e) {
$this->logger->error("Gagal membuat order", ['error' => $e->getMessage()]);
throw $e;
}
}
}
// Di production — gunakan FileLogger
$service = new OrderService(
logger: new FileLogger('/var/log/orders.log'),
db: $pdo,
);
// Di testing — gunakan NullLogger atau mock
$serviceTest = new OrderService(
logger: new NullLogger(),
db: $mockPdo,
);
// Ganti implementasi logger ke database tanpa mengubah OrderService
$serviceProd = new OrderService(
logger: new DatabaseLogger($pdo),
db: $pdo,
);
Mengimplementasikan Beberapa Interface #
Sebuah kelas bisa mengimplementasikan lebih dari satu interface sekaligus — inilah keunggulan interface dibanding abstract class untuk mendefinisikan kemampuan (capability) yang ortogonal satu sama lain.
<?php
interface Cacheable
{
public function getCacheKey(): string;
public function getCacheTtl(): int;
}
interface Serializable
{
public function toArray(): array;
public function toJson(): string;
}
interface Validatable
{
public function isValid(): bool;
public function getErrors(): array;
}
// Satu kelas bisa implement banyak interface
class Produk implements Cacheable, Serializable, Validatable
{
private array $errors = [];
public function __construct(
private int $id,
private string $nama,
private float $harga,
private int $stok,
) {}
// Implementasi Cacheable
public function getCacheKey(): string
{
return "produk:{$this->id}";
}
public function getCacheTtl(): int
{
return 3600; // 1 jam
}
// Implementasi Serializable
public function toArray(): array
{
return [
'id' => $this->id,
'nama' => $this->nama,
'harga' => $this->harga,
'stok' => $this->stok,
];
}
public function toJson(): string
{
return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
}
// Implementasi Validatable
public function isValid(): bool
{
$this->errors = [];
if (empty(trim($this->nama))) {
$this->errors[] = "Nama produk tidak boleh kosong";
}
if ($this->harga < 0) {
$this->errors[] = "Harga tidak boleh negatif";
}
if ($this->stok < 0) {
$this->errors[] = "Stok tidak boleh negatif";
}
return empty($this->errors);
}
public function getErrors(): array
{
return $this->errors;
}
}
$produk = new Produk(1, 'Laptop', 15_000_000, 5);
// Bisa digunakan di semua konteks yang membutuhkan salah satu interface-nya
var_dump($produk instanceof Cacheable); // true
var_dump($produk instanceof Serializable); // true
var_dump($produk instanceof Validatable); // true
// Fungsi yang menerima Cacheable bekerja untuk semua objek Cacheable
function simpanKeCache(Cacheable $obj, string $data): void
{
$key = $obj->getCacheKey();
$ttl = $obj->getCacheTtl();
// simpan ke Redis/Memcached...
}
simpanKeCache($produk, $produk->toJson());
Pewarisan Interface #
Interface bisa meng-extend interface lain — bahkan lebih dari satu sekaligus. Ini memungkinkan membangun hierarki interface yang granular:
<?php
// Interface dasar — kemampuan paling minimum
interface ReadableInterface
{
public function find(int $id): ?array;
public function findAll(array $filter = []): array;
}
interface WritableInterface
{
public function create(array $data): int;
public function update(int $id, array $data): bool;
public function delete(int $id): bool;
}
// Interface yang lebih spesifik — extend dua interface sekaligus
interface RepositoryInterface extends ReadableInterface, WritableInterface
{
public function findByKriteria(array $kriteria, int $limit = 10): array;
public function count(array $filter = []): int;
}
// Interface read-only — hanya butuh ReadableInterface
interface ReadOnlyRepositoryInterface extends ReadableInterface
{
public function findPaginated(int $halaman, int $perHalaman): array;
}
// Implementasi lengkap
class ProdukRepository implements RepositoryInterface
{
public function __construct(private \PDO $db) {}
public function find(int $id): ?array
{
$stmt = $this->db->prepare("SELECT * FROM produk WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
}
public function findAll(array $filter = []): array
{
// implementasi query dengan filter dinamis
return [];
}
public function create(array $data): int
{
$stmt = $this->db->prepare(
"INSERT INTO produk (nama, harga, stok) VALUES (?, ?, ?)"
);
$stmt->execute([$data['nama'], $data['harga'], $data['stok']]);
return (int) $this->db->lastInsertId();
}
public function update(int $id, array $data): bool
{
$stmt = $this->db->prepare(
"UPDATE produk SET nama = ?, harga = ?, stok = ? WHERE id = ?"
);
return $stmt->execute([$data['nama'], $data['harga'], $data['stok'], $id]);
}
public function delete(int $id): bool
{
$stmt = $this->db->prepare("DELETE FROM produk WHERE id = ?");
return $stmt->execute([$id]);
}
public function findByKriteria(array $kriteria, int $limit = 10): array
{
return [];
}
public function count(array $filter = []): int
{
return 0;
}
}
Interface Bawaan PHP yang Penting #
PHP menyediakan banyak interface bawaan yang — jika diimplementasikan — memungkinkan objekmu berintegrasi langsung dengan fitur bahasa PHP seperti foreach, count(), casting ke string, dan lainnya.
Countable — Membuat Objek Bisa di-count()
#
<?php
class Keranjang implements \Countable
{
private array $items = [];
public function tambah(array $produk): void
{
$this->items[] = $produk;
}
// Wajib diimplementasikan: mengembalikan jumlah elemen
public function count(): int
{
return count($this->items);
}
}
$keranjang = new Keranjang();
$keranjang->tambah(['nama' => 'Laptop', 'harga' => 15_000_000]);
$keranjang->tambah(['nama' => 'Mouse', 'harga' => 250_000]);
echo count($keranjang); // 2 — fungsi count() bawaan PHP bekerja!
Iterator — Membuat Objek Bisa di-foreach
#
<?php
class ProdukKoleksi implements \Iterator
{
private int $posisi = 0;
public function __construct(private array $items = []) {}
// Elemen saat ini
public function current(): mixed
{
return $this->items[$this->posisi];
}
// Key/indeks saat ini
public function key(): int
{
return $this->posisi;
}
// Maju ke elemen berikutnya
public function next(): void
{
$this->posisi++;
}
// Kembali ke awal
public function rewind(): void
{
$this->posisi = 0;
}
// Apakah posisi saat ini valid?
public function valid(): bool
{
return isset($this->items[$this->posisi]);
}
}
$koleksi = new ProdukKoleksi([
['nama' => 'Laptop', 'harga' => 15_000_000],
['nama' => 'Monitor', 'harga' => 5_000_000],
['nama' => 'Mouse', 'harga' => 250_000],
]);
// foreach bekerja langsung pada objek
foreach ($koleksi as $indeks => $produk) {
echo "$indeks: {$produk['nama']} — Rp " . number_format($produk['harga']) . "\n";
}
IteratorAggregate — Cara Lebih Mudah dari Iterator
#
Mengimplementasikan Iterator penuh membutuhkan 5 method. IteratorAggregate hanya membutuhkan 1 — getIterator() yang mengembalikan sesuatu yang iterable:
<?php
class ProdukKoleksiSimple implements \IteratorAggregate, \Countable
{
private array $items = [];
public function tambah(array $produk): void
{
$this->items[] = $produk;
}
// Cukup implementasikan ini saja
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->items);
}
public function count(): int
{
return count($this->items);
}
}
$koleksi = new ProdukKoleksiSimple();
$koleksi->tambah(['nama' => 'Laptop']);
$koleksi->tambah(['nama' => 'Mouse']);
foreach ($koleksi as $produk) {
echo $produk['nama'] . "\n";
}
echo count($koleksi); // 2
Stringable — Objek yang Bisa Dikonversi ke String
#
<?php
class NamaPengguna implements \Stringable
{
public function __construct(
private string $depan,
private string $belakang,
) {}
// Wajib diimplementasikan
public function __toString(): string
{
return trim("{$this->depan} {$this->belakang}");
}
public function initials(): string
{
return strtoupper($this->depan[0] . $this->belakang[0]);
}
}
$nama = new NamaPengguna('Budi', 'Santoso');
echo $nama; // "Budi Santoso" — via __toString
echo "Halo, $nama!"; // "Halo, Budi Santoso!" — interpolasi bekerja
echo $nama->initials(); // "BS"
// Fungsi yang menerima string|Stringable
function sambut(string|\Stringable $nama): string
{
return "Selamat datang, $nama!";
}
echo sambut("Budi"); // via string langsung
echo sambut($nama); // via Stringable object
ArrayAccess — Objek yang Bisa Diakses Seperti Array
#
<?php
class Konfigurasi implements \ArrayAccess
{
private array $data = [];
public function __construct(array $data = [])
{
$this->data = $data;
}
public function offsetExists(mixed $offset): bool
{
return isset($this->data[$offset]);
}
public function offsetGet(mixed $offset): mixed
{
return $this->data[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $nilai): void
{
if ($offset === null) {
$this->data[] = $nilai;
} else {
$this->data[$offset] = $nilai;
}
}
public function offsetUnset(mixed $offset): void
{
unset($this->data[$offset]);
}
}
$config = new Konfigurasi(['host' => 'localhost', 'port' => 3306]);
// Digunakan seperti array biasa
echo $config['host']; // localhost
$config['database'] = 'mydb'; // set value
echo $config['database']; // mydb
unset($config['port']);
var_dump(isset($config['port'])); // false
Interface vs Abstract Class #
Ini adalah salah satu pertanyaan desain yang paling sering muncul. Keduanya mendefinisikan kontrak, tapi dengan cara yang berbeda:
| Aspek | Interface | Abstract Class |
|---|---|---|
| Implementasi method | ✗ Tidak ada | ✓ Boleh ada |
| Property instance | ✗ Tidak boleh | ✓ Boleh |
| Constructor | ✗ Tidak boleh | ✓ Boleh |
| Visibility method | Selalu public | Bebas |
| Konstanta | ✓ Boleh | ✓ Boleh |
| Jumlah yang bisa di-implement/extend | Banyak | Hanya satu |
| Merepresentasikan | Kemampuan / kontrak | Tipe dasar dengan sebagian implementasi |
flowchart TD
A{Apakah ada implementasi\nyang bisa dibagi?} -- Ya --> B{Apakah ini\n'is-a' relationship?}
B -- Ya --> C[Abstract Class\nContoh: Kendaraan → Mobil]
B -- Tidak --> D[Komposisi / Trait\nbukan pewarisan]
A -- Tidak --> E{Apakah satu kelas perlu\nbanyak kontrak berbeda?}
E -- Ya --> F[Interface\nContoh: Cacheable + Serializable]
E -- Tidak --> G{Apakah perlu\nuntuk dependency injection?}
G -- Ya --> F
G -- Tidak --> H[Bisa keduanya —\nAbstract atau Interface]<?php
// ✓ Interface — untuk kontrak yang bisa diimplementasikan oleh banyak kelas
// yang tidak punya relasi 'is-a'
interface Notifiable
{
public function kirimNotifikasi(string $pesan): bool;
}
// Email, SMS, Push Notification semua bisa "notifiable"
// tapi tidak ada relasi hierarki di antara mereka
class EmailNotifier implements Notifiable { /* ... */ }
class SmsNotifier implements Notifiable { /* ... */ }
class PushNotifier implements Notifiable { /* ... */ }
// ✓ Abstract class — untuk basis bersama dengan implementasi yang bisa dibagi
abstract class BaseRepository
{
public function __construct(protected \PDO $db) {}
// Implementasi bersama yang tersedia untuk semua turunan
protected function query(string $sql, array $params = []): \PDOStatement
{
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt;
}
// Kontrak yang harus diimplementasikan masing-masing
abstract public function find(int $id): ?array;
abstract public function tabelNama(): string;
// Template method yang pakai method abstract
public function hapus(int $id): bool
{
$tabel = $this->tabelNama();
return $this->query("DELETE FROM $tabel WHERE id = ?", [$id])
->rowCount() > 0;
}
}
// ✓ Kombinasi keduanya — paling ekspresif
interface ProdukRepositoryInterface extends ReadableInterface, WritableInterface
{
public function findByKategori(string $kategori): array;
}
abstract class AbstractProdukRepository implements ProdukRepositoryInterface
{
// Implementasi bersama untuk find() dan findAll()
// Method write masih abstract untuk diimplementasikan konkret
}
class MysqlProdukRepository extends AbstractProdukRepository
{
// Implementasi spesifik MySQL
}
Dependency Inversion dengan Interface #
Interface adalah kunci untuk menerapkan Dependency Inversion Principle (DIP) — salah satu prinsip SOLID. Prinsipnya: modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah; keduanya harus bergantung pada abstraksi.
<?php
// Tanpa interface — coupling ketat
class OrderController
{
private MySqlOrderRepository $repo; // bergantung langsung ke MySQL!
private SmtpMailer $mailer; // bergantung langsung ke SMTP!
public function __construct()
{
$this->repo = new MySqlOrderRepository(); // hard-coded
$this->mailer = new SmtpMailer(); // hard-coded
}
// Tidak bisa di-test tanpa koneksi MySQL dan SMTP nyata
// Tidak bisa ganti ke PostgreSQL atau mailer lain tanpa edit kelas ini
}
// ---
// Dengan interface — loose coupling
interface OrderRepositoryInterface
{
public function simpan(array $data): int;
public function cari(int $id): ?array;
}
interface MailerInterface
{
public function kirim(string $ke, string $subjek, string $isi): bool;
}
class OrderController
{
// Bergantung pada abstraksi, bukan implementasi
public function __construct(
private OrderRepositoryInterface $repo,
private MailerInterface $mailer,
private LoggerInterface $logger,
) {}
public function buatOrder(array $data): array
{
$this->logger->info("Membuat order", ['data' => $data]);
$orderId = $this->repo->simpan($data);
$this->mailer->kirim(
$data['email'],
"Konfirmasi Order #$orderId",
"Terima kasih atas pesanan kamu!"
);
return ['id' => $orderId, 'status' => 'created'];
}
}
// Di production
$controller = new OrderController(
repo: new MySqlOrderRepository($pdo),
mailer: new SmtpMailer($smtpConfig),
logger: new FileLogger('/var/log/orders.log'),
);
// Di testing — semua diganti implementasi sederhana
$controller = new OrderController(
repo: new InMemoryOrderRepository(),
mailer: new FakeMailer(),
logger: new NullLogger(),
);
// Test berjalan tanpa database, SMTP, atau file system
Ringkasan #
- Interface mendefinisikan kontrak — apa yang harus bisa dilakukan objek, bukan bagaimana melakukannya. Semua method interface otomatis
public abstract.- Satu kelas bisa implement banyak interface — ini keunggulan utama interface dibanding abstract class untuk mendefinisikan kemampuan yang ortogonal seperti
Cacheable,Serializable,Validatable.- Interface sebagai type hint adalah kunci Dependency Inversion — fungsi yang menerima interface bekerja dengan semua implementasinya tanpa perlu diubah.
- Interface bisa meng-extend interface lain — bahkan lebih dari satu. Gunakan ini untuk membangun hierarki kontrak yang granular:
ReadableInterface,WritableInterface, danRepositoryInterface extends ReadableInterface, WritableInterface.- Interface bawaan PHP penting:
Countable(agar bisa di-count()),IteratorAggregate(agar bisa di-foreach),Stringable(agar bisa dipakai sebagai string),ArrayAccess(agar bisa diakses seperti array).- Interface vs Abstract Class: gunakan interface untuk kontrak kemampuan yang bisa diimplementasikan kelas yang tidak berkerabat. Gunakan abstract class ketika ada implementasi bersama yang perlu dibagi antar kelas dalam satu hierarki.
NullLogger,InMemoryRepository,FakeMailer— implementasi interface yang tidak melakukan apa-apa atau menyimpan dalam memori adalah pola standar untuk testing yang cepat tanpa infrastruktur nyata.- Dependency Inversion — kelas tingkat tinggi (
OrderController) bergantung pada interface (LoggerInterface), bukan implementasi konkret (FileLogger). Ini membuat kode mudah di-test, mudah diubah, dan longgar kouplingnya.