Class abstract pada chapter sebelumnya memang sudah bisa memaksa subclass mengimplementasikan method tertentu, tetapi PHP tetap membatasi satu class hanya boleh memiliki satu parent. Sekarang bayangkan class Kucing, Pesawat, dan Drone yang tidak saling berkerabat, tetapi ketiganya punya kemampuan yang sama: bisa terbang. Hubungan “bisa terbang” bukan hubungan keturunan, melainkan kemampuan yang bisa menempel pada class mana pun. Interface menjawab kebutuhan ini dengan menyediakan kontrak berisi daftar method yang wajib disediakan setiap class pengimplementasi. Pada chapter ini kita akan belajar menulis interface, mengimplementasikannya pada beberapa class, lalu memanfaatkannya agar kode menjadi fleksibel dan mudah diuji.

Interface sebagai Kontrak #

Interface dideklarasikan dengan kata kunci interface dan hanya berisi signature method — nama method, parameter, dan return type — tanpa isi (body). Class yang mengikat diri pada kontrak tersebut memakai kata kunci implements, lalu class tersebut wajib menyediakan implementasi untuk seluruh method yang diminta. Dengan kata lain, interface berkata: “jika kamu mengaku punya kemampuan ini, kamu harus membuktikannya dengan method berikut”.

php
<?php

interface HewanBersuara
{
    public function bersuara(): string;
}

class Kucing implements HewanBersuara
{
    public function bersuara(): string
    {
        return "Meong";
    }
}

class Anjing implements HewanBersuara
{
    public function bersuara(): string
    {
        return "Guk guk";
    }
}

function mainkanSuara(HewanBersuara $hewan): string
{
    return $hewan->bersuara();
}

echo mainkanSuara(new Kucing()) . "\n";
echo mainkanSuara(new Anjing()) . "\n";

Output yang Dihasilkan:

text
Meong
Guk guk

Mari kita telaah kode di atas baris per baris:

  1. interface HewanBersuara: mendeklarasikan kontrak baru bernama HewanBersuara yang meminta satu method bernama bersuara().
  2. public function bersuara(): string;: signature di dalam interface diakhiri titik koma — tidak ada kurung kurawal karena interface tidak menyediakan implementasi.
  3. class Kucing implements HewanBersuara: class Kucing menandatangani kontrak, sehingga wajib mendefinisikan bersuara() lengkap dengan body-nya.
  4. function mainkanSuara(HewanBersuara $hewan): fungsi ini menerima tipe HewanBersuara, bukan Kucing atau Anjing. Semua object yang mengimplementasikan interface tersebut boleh dikirim sebagai argumen — inilah polimorfisme lintas hierarki class.

PHP menegakkan kontrak ini dengan keras. Jika satu method saja tidak diimplementasikan, eksekusi berhenti dengan fatal error sebelum program sempat berjalan:

php
<?php

interface Pembayaran
{
    public function bayar(int $jumlah): string;
}

class PembayaranPalsu implements Pembayaran
{
    // Method bayar() sengaja tidak diimplementasikan
}

new PembayaranPalsu();

Output yang Dihasilkan:

text

Fatal error: Class PembayaranPalsu contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Pembayaran::bayar) in /home/z/my-project/php-course/content/php/object-oriented-programming/interfaces/.verify_tmp.php on line 8

Pesan galat di atas menyatakan bahwa PembayaranPalsu harus dideklarasikan abstract atau melengkapi method Pembayaran::bayar. Kontrak interface tidak bisa diselewengkan setengah jalan; class harus memenuhinya sepenuhnya atau tidak mengimplementasikannya sama sekali.

Mengimplementasi Banyak Interface #

Satu class hanya boleh extends satu parent, tetapi boleh implements banyak interface sekaligus — cukup pisahkan dengan koma. Kemampuan ini memungkinkan kita menyusun kemampuan (capability) secara bebas, seperti menyusun potongan puzzle:

php
<?php

interface BisaDihitung
{
    public function hitungTotal(): float;
}

interface BisaDideskripsikan
{
    public function deskripsi(): string;
}

class Keranjang implements BisaDihitung, BisaDideskripsikan
{
    private array $items = [];

    public function tambah(float $harga): void
    {
        $this->items[] = $harga;
    }

    public function hitungTotal(): float
    {
        return array_sum($this->items);
    }

    public function deskripsi(): string
    {
        return "Keranjang berisi " . count($this->items) . " item";
    }
}

$keranjang = new Keranjang();
$keranjang->tambah(15000.0);
$keranjang->tambah(25000.0);

echo $keranjang->deskripsi() . "\n";
echo "Total: Rp" . $keranjang->hitungTotal() . "\n";

Output yang Dihasilkan:

text
Keranjang berisi 2 item
Total: Rp40000

Class Keranjang menandatangani dua kontrak sekaligus: BisaDihitung dan BisaDideskripsikan. Karena itu, object Keranjang bisa dikirim ke fungsi mana pun yang meminta salah satu dari kedua interface tersebut. Kelak ketika kita menambah class Nota atau Faktur, class-class tersebut cukup mengimplementasikan interface yang sama tanpa perlu berkerabat dengan Keranjang.

Interface juga bisa mewarisi interface lain, bahkan dari beberapa sekaligus. Pola ini berguna untuk menyusun kontrak besar dari kontrak-kontrak kecil:

php
<?php

interface BisaBergerak
{
    public function bergerak(): string;
}

interface BisaBerhenti
{
    public function berhenti(): string;
}

interface Kendaraan extends BisaBergerak, BisaBerhenti
{
}

class Sepeda implements Kendaraan
{
    public function bergerak(): string
    {
        return "Sepeda melaju";
    }

    public function berhenti(): string
    {
        return "Sepeda rem tangan";
    }
}

$sepeda = new Sepeda();
echo $sepeda->bergerak() . "\n";
echo $sepeda->berhenti() . "\n";

Output yang Dihasilkan:

text
Sepeda melaju
Sepeda rem tangan

Interface Kendaraan menggabungkan dua kontrak yang lebih kecil, sehingga class yang mengimplementasikan Kendaraan otomatis wajib mengimplementasikan kedua method dari BisaBergerak dan BisaBerhenti. Perhatikan juga bahwa Sepeda kini bisa diperlakukan sebagai Kendaraan, BisaBergerak, ataupun BisaBerhenti oleh operator instanceof dan type declaration.

Konstanta dalam Interface #

Selain signature method, interface boleh mendeklarasikan konstanta dengan public const. Nilai konstanta ini ikut tersedia di dalam class pengimplementasi dan tidak boleh ditimpa:

php
<?php

interface StatusPesanan
{
    public const BARU = "baru";
    public const DIPROSES = "diproses";
    public const SELESAI = "selesai";
}

class Pesanan implements StatusPesanan
{
    public function __construct(private string $status = self::BARU)
    {
    }

    public function ubahStatus(string $statusBaru): void
    {
        $this->status = $statusBaru;
    }

    public function statusSaatIni(): string
    {
        return $this->status;
    }
}

$pesanan = new Pesanan();
echo $pesanan->statusSaatIni() . "\n";

$pesanan->ubahStatus(StatusPesanan::DIPROSES);
echo $pesanan->statusSaatIni() . "\n";

Output yang Dihasilkan:

text
baru
diproses

Constructor class Pesanan memakai self::BARU sebagai nilai bawaan, yang menunjuk konstanta dari interface yang diimplementasinya. Pemanggilan StatusPesanan::DIPROSES di luar class juga sah karena konstanta interface bersifat publik. Pola seperti ini menghidupkan kembali semangat konstanta dari chapter sebelumnya: nilai tetap yang punya satu sumber kebenaran.

Interface vs Class Abstract #

Karena keduanya sama-sama bisa memaksa class turunan menyediakan method, kapan sebaiknya kita memilih interface dan kapan class abstract? Tabel berikut merangkum perbedaannya:

AspekInterfaceClass Abstract
Kata kunciinterface + implementsabstract class + extends
Jumlah per classBoleh beberapa sekaligusHanya satu parent
Isi methodHanya signature, tanpa bodyCampuran method konkret dan abstract
PropertyTidak boleh (hanya konstanta)Bebas mendeklarasikan property
ConstructorTidak bolehBoleh
Visibilitas methodSelalu publicpublic, protected, atau private
Wujud objectTidak bisa diinstansiasiTidak bisa diinstansiasi

Pilih class abstract ketika beberapa class berkerabat erat dan ingin berbagi kode konkret pada satu tempat bersama — misalnya Repository dasar yang menyediakan koneksi database. Pilih interface ketika Anda hanya ingin mendefinisikan kemampuan tanpa memedulikan dari mana class berasal — misalnya BisaDihitung yang bisa dipakai Keranjang, Nota, maupun Faktur. Keduanya juga sering dipakai bersama: sebuah class abstract mengimplementasikan interface agar kontrak tetap terjaga sambil berbagi implementasi.

Memprogram ke Interface #

Praktik paling berharga dari interface adalah bergantung pada kontrak, bukan pada class konkret — prinsip yang sering disebut program to an interface, not an implementation. Mari kita lihat wujudnya pada contoh layanan pesanan yang membutuhkan pencatat log:

php
<?php

interface LoggerInterface
{
    public function log(string $level, string $pesan): void;
}

class FileLogger implements LoggerInterface
{
    public function log(string $level, string $pesan): void
    {
        echo "[file] [$level] $pesan\n";
    }
}

class InMemoryLogger implements LoggerInterface
{
    public array $entri = [];

    public function log(string $level, string $pesan): void
    {
        $this->entri[] = "[$level] $pesan";
    }
}

class LayananPesanan
{
    public function __construct(private LoggerInterface $logger)
    {
    }

    public function proses(string $nomorPesanan): void
    {
        $this->logger->log("info", "Pesanan $nomorPesanan diproses");
    }
}

$layananProduksi = new LayananPesanan(new FileLogger());
$layananProduksi->proses("ORD-001");

$loggerUji = new InMemoryLogger();
$layananUji = new LayananPesanan($loggerUji);
$layananUji->proses("ORD-002");

echo "Jumlah entri logger uji: " . count($loggerUji->entri) . "\n";
echo $loggerUji->entri[0] . "\n";

Output yang Dihasilkan:

text
[file] [info] Pesanan ORD-001 diproses
Jumlah entri logger uji: 1
[info] Pesanan ORD-002 diproses

Mari kita bedah contoh di atas:

  1. LayananPesanan hanya mengenal LoggerInterface: class ini tidak peduli apakah log berakhir di file, terminal, atau memori. Satu-satunya hal yang dipahami LayananPesanan adalah kontrak log().
  2. FileLogger: implementasi produksi yang (dalam contoh ini) mencetak ke layar seolah menulis ke file.
  3. InMemoryLogger: implementasi ringan yang menyimpan entri ke array $entri agar mudah diperiksa.
  4. Dua layanan, dua logger: layanan produksi memakai FileLogger, sedangkan skenario pengujian memakai InMemoryLogger — kode LayananPesanan tidak berubah sedikit pun.

Kemampuan menukar implementasi inilah yang membuat interface sangat bersahabat dengan pengujian otomatis. Ketika menulis unit test, kita bisa menyuntikkan implementasi palsu (test double) tanpa menyentuh file sungguhan atau jaringan. Contoh LoggerInterface di atas bukan karangan kosong: PHP-FIG merilis standar PSR-3 dengan interface LoggerInterface yang dipakai ratusan package di ekosistem PHP, sehingga logger apa pun bisa saling dipertukarkan.

Mengecek Interface dengan instanceof #

Operator instanceof yang kita kenal pada chapter inheritance bekerja juga terhadap interface. Pemeriksaan seperti ini berguna ketika kode perlu bertanya: “apakah object ini menandatangani kontrak tertentu?”

php
<?php

interface BisaTerbang
{
    public function terbang(): string;
}

class Burung implements BisaTerbang
{
    public function terbang(): string
    {
        return "terbang dengan sayap";
    }
}

class Ikan
{
}

function cekKemampuan(object $hewan): void
{
    if ($hewan instanceof BisaTerbang) {
        echo get_class($hewan) . " bisa terbang: " . $hewan->terbang() . "\n";
    } else {
        echo get_class($hewan) . " tidak bisa terbang\n";
    }
}

cekKemampuan(new Burung());
cekKemampuan(new Ikan());

Output yang Dihasilkan:

text
Burung bisa terbang: terbang dengan sayap
Ikan tidak bisa terbang

Object Burung lolos pemeriksaan instanceof BisaTerbang sehingga method terbang() aman dipanggil, sedangkan object Ikan tidak lolos dan jalur else yang diambil. Pola seperti ini membuat fungsi tetap aman meski menerima object generik. Meski berguna, pemeriksaan instanceof yang terlalu sering justru pertanda desain yang kurang pas — lebih baik memakai type declaration interface secara langsung seperti contoh LoggerInterface di atas.

Ringkasan #

Interface mendefinisikan kontrak method tanpa implementasi; class menandatanganinya lewat implements dan PHP menegakkan kontrak tersebut dengan fatal error bila ada method yang kurang. Satu class boleh mengimplementasikan banyak interface, interface bisa mewarisi interface lain, dan konstanta boleh ikut dibawa di dalamnya. Dengan bergantung pada interface alih-alih class konkret, kode menjadi mudah diuji dan implementasi bisa dipertukarkan tanpa mengubah kode pemanggilnya. Namun interface tetap tidak membawa implementasi — bagaimana membagikan kode konkret ke class yang tidak berkerabat? Jawabannya adalah trait, yang kita bahas pada chapter berikutnya.

Referensi #