Inheritance yang kita pelajari sejauh ini bersifat terbuka: siapa pun boleh menurunkan class kita dan mengubah perilakunya lewat method overriding. Keterbukaan tersebut menjadi masalah ketika sebuah class dirancang dengan asumsi internal yang tidak boleh diganggu — misalnya class yang menjaga invariansi keamanan atau class publik yang menjadi API library. Satu subclass yang meng-override method secara keliru bisa meruntuhkan jaminan yang sengaja dibangun penulis class. PHP menyediakan kata kunci final untuk menutup pintu itu secara eksplisit: class final tidak bisa diwarisi, dan method final tidak bisa di-override. Pada chapter ini kita akan mempelajari perilaku final, galat yang muncul ketika aturannya dilanggar, dan cara merancang kode yang tetap mudah diperluas tanpa inheritance.

Kata kunci final ditulis sebelum class untuk menyatakan bahwa class tersebut adalah ujung dari jalur inheritance. Class final tetap bisa diinstansiasi dan dipakai seperti biasa — yang ditutup hanyalah kemungkinan dijadikan parent:

php
<?php

final class KunciApi
{
    public function __construct(private string $nilai)
    {
    }

    public function nilaiTersamar(): string
    {
        return str_repeat("*", 8) . substr($this->nilai, -4);
    }
}

$kunci = new KunciApi("sk_live_contoh1234");
echo $kunci->nilaiTersamar() . "\n";

Output yang Dihasilkan:

text
********1234

Class KunciApi menyimpan nilai rahasia dan hanya mengekspos versi tersamar melalui nilaiTersamar(). Dengan menandai class ini final, penulis class memastikan tidak ada jalur bagi pihak lain menurunkannya lalu mengekspos property $nilai lewat method publik baru. Instance-nya sendiri tetap dibuat dan dipakai secara normal, seperti terlihat pada pemanggilan di dua baris terakhir.

Class final Tidak Bisa Diwarisi #

Mencoba mewarisi class final langsung dihentikan PHP dengan fatal error — bahkan sebelum satu baris program pun berjalan, karena pelanggaran ini terdeteksi saat kompilasi:

php
<?php

final class Konfigurasi
{
}

class KonfigurasiLokal extends Konfigurasi
{
}

Output yang Dihasilkan:

text

Fatal error: Class KonfigurasiLokal cannot extend final class Konfigurasi in /home/z/my-project/php-course/content/php/object-oriented-programming/final-classes-dan-methods/.verify_tmp.php on line 7

Pesan galat tersebut menyebut secara gamblang bahwa KonfigurasiLokal tidak boleh mewarisi class final Konfigurasi. Tidak ada mekanisme untuk melewati larangan ini — bukan soal visibility, bukan pula soal namespace. Jika Anda mendapati pesan ini saat memakai library, artinya penulis library sengaja menutup class tersebut, dan kita perlu mencari jalur perluasan lain yang akan kita bahas di bagian akhir chapter.

Method final Tidak Bisa Di-override #

final juga bisa ditempelkan pada level method: class tetap boleh diwarisi, tetapi method yang ditandai final wajib diterima apa adanya oleh seluruh subclass. Method lain yang tidak final tetap bebas di-override, seperti contoh berikut:

php
<?php

class HargaDasar
{
    public function harga(): int
    {
        return 100000;
    }

    final public function hargaDiskon(): float
    {
        return round($this->harga() * 0.9, 2);
    }
}

class HargaPromo extends HargaDasar
{
    public function harga(): int
    {
        return 80000;
    }
}

$promo = new HargaPromo();
echo "Harga dasar: Rp" . $promo->harga() . "\n";
echo "Harga diskon: Rp" . $promo->hargaDiskon() . "\n";

Output yang Dihasilkan:

text
Harga dasar: Rp80000
Harga diskon: Rp72000

Class HargaPromo sah meng-override harga() karena method tersebut tidak final, dan hasilnya memang 80000. Method final hargaDiskon() tetap diwarisi apa adanya, tetapi bukan berarti kaku: rumus di dalamnya memanggil $this->harga(), dan berkat dynamic dispatch versi yang dipakai adalah milik HargaPromo, sehingga diskon dihitung dari 80000 menghasilkan 72000. Penulis class memakai final untuk mengunci rumus diskon 10% agar tidak bisa diganti subclass mana pun, sambil tetap membuka harga() sebagai titik kustomisasi.

Mencoba tetap meng-override method final tentu saja ditolak keras:

php
<?php

class HargaDasar
{
    final public function hargaDiskon(): float
    {
        return 90000.0;
    }
}

class HargaPromo extends HargaDasar
{
    public function hargaDiskon(): float
    {
        return 50000.0;
    }
}

Output yang Dihasilkan:

text

Fatal error: Cannot override final method HargaDasar::hargaDiskon() in /home/z/my-project/php-course/content/php/object-oriented-programming/final-classes-dan-methods/.verify_tmp.php on line 13

Galat Cannot override final method muncul pada saat kompilasi, jauh sebelum ada kesempatan menjalankan program. Perilaku ini menjadikan final sebagai dokumentasi yang ditegakkan mesin: maksud penulis class terkunci dan PHP memastikan tidak ada yang melanggarnya diam-diam.

Class final di Standard Library PHP #

Bukan hanya kode kita — PHP sendiri memakai final secara luas di class-class internalnya. Reflection bisa membuktikannya:

php
<?php

$kelasFinal = [];
foreach (get_declared_classes() as $kelas) {
    $refleksi = new ReflectionClass($kelas);
    if ($refleksi->isFinal() && $refleksi->isInternal()) {
        $kelasFinal[] = $kelas;
    }
}

echo "Jumlah class internal final: " . count($kelasFinal) . "\n";
echo "Contoh lima di antaranya:\n";
foreach (array_slice($kelasFinal, 0, 5) as $kelas) {
    echo "- $kelas\n";
}

Output yang Dihasilkan:

text
Jumlah class internal final: 55
Contoh lima di antaranya:
- InternalIterator
- Closure
- Generator
- WeakReference
- WeakMap

Dari keluaran di atas terlihat puluhan class internal PHP yang final, termasuk Closure, Generator, WeakReference, dan WeakMap. Class-class tersebut mengelola resource dan invariansi internal yang rapuh: sebuah Generator, misalnya, menjaga mesin eksekusi yang menangguhkan (suspend) dan melanjutkan (resume) function — membiarkan subclass mengubah perilakunya membuka peluang korupsi state yang tidak bisa dijamin lagi oleh tim pengembang PHP. Menandai class seperti ini final adalah cara paling jujur mengatakan: “perilaku lengkapnya sudah di sini, dan kontraknya tidak dirancang untuk diturunkan”.

Merancang Ekstensi melalui Komposisi #

Lalu bagaimana pengguna class final menambahkan perilaku? Jawabannya adalah komposisi: bungkus object final di dalam class kita sendiri, dan rancang titik masukan lewat interface. Perhatikan pola berikut:

php
<?php

interface SumberData
{
    public function ambilData(): array;
}

final class PengolahLaporan
{
    public function __construct(private SumberData $sumber)
    {
    }

    public function laporanRingkas(): string
    {
        $data = $this->sumber->ambilData();
        return "Total " . array_sum($data) . " dari " . count($data) . " entri";
    }
}

class SumberArray implements SumberData
{
    public function __construct(private array $data)
    {
    }

    public function ambilData(): array
    {
        return $this->data;
    }
}

$laporan = new PengolahLaporan(new SumberArray([100, 250, 50]));
echo $laporan->laporanRingkas() . "\n";

Output yang Dihasilkan:

text
Total 400 dari 3 entri

Class PengolahLaporan ditandai final karena perilaku perangkumnya memang tidak dimaksudkan untuk diubah lewat subclass. Namun class yang sama tetap terbuka untuk variasi: sumber datanya disuntikkan lewat constructor dengan type declaration SumberData, sehingga pengguna baru cukup menulis class seperti SumberArray — atau membaca dari database, API, dan berkas — tanpa menyentuh sedikit pun isi PengolahLaporan. Kelak dibutuhkan sumber CSV? Buat SumberCsv implements SumberData, selesai. Inilah maksud prinsip “tertutup untuk modifikasi, terbuka untuk perluasan”: kode inti dikunci, variasi masuk dari luar.

final dan Stabilitas API #

Kata kunci final memainkan peran besar dalam menjaga stabilitas API jangka panjang. Ketika sebuah method tidak final, setiap perubahan internal pada method tersebut berisiko merusak subclass di kode pengguna yang meng-override-nya — penulis library menjadi sandera oleh implementasi turunan yang tidak dilihatnya. Dengan menandai class atau method final, penulis library memperoleh kebebasan refactor: selama signature publiknya tetap, isi boleh ditulis ulang tanpa merusak siapa pun.

Ada satu ketidaksimetrian yang bermanfaat untuk diingat: menghapus final dari class yang sudah rilis adalah perubahan yang aman (hanya membuka kemampuan baru), sedangkan menambahkan final adalah perubahan yang merusak (memecah semua subclass yang sudah ada). Karena itu penulis library cenderung memulai dengan final lalu membukanya ketika ada kasus perluasan yang nyata. Dalam kode aplikasi internal — di mana seluruh subclass berada di bawah kendali tim yang sama — keputusan final atau tidak bisa lebih longgar; yang terpenting adalah kesepakatan itu disengaja, bukan kebetulan.

Ringkasan #

final menutup class dari inheritance dan method dari overriding, ditegakkan PHP dengan fatal error sejak tahap kompilasi. Standard library PHP sendiri memakainya secara luas untuk melindungi invariansi internal, dan pola yang sama berlaku untuk kode kita: kunci inti, lalu buka pintu variasi lewat interface dan komposisi. Prinsip “tertutup untuk modifikasi, terbuka untuk perluasan” tersebut sebenarnya adalah gerbang menuju topik berikutnya: object composition, cara merakit object besar dari object-object kecil tanpa menyentuh inheritance sama sekali.

Referensi #