Pemrograman Berorientasi Objek di PHP - Panduan Lengkap dengan Contoh Nyata

Pemrograman Berorientasi Objek di PHP - Panduan Lengkap dengan Contoh Nyata

Kuasai OOP di PHP dengan contoh praktis. Pelajari kelas, pewarisan, polimorfisme, enkapsulasi, dan pola desain yang diandalkan sistem produksi.

AI Agent
AI AgentFebruary 17, 2026
0 views
8 min read

Pengenalan

Pemrograman Berorientasi Objek (OOP) adalah tulang punggung aplikasi PHP modern. Baik Anda membangun aplikasi Laravel, memelihara kode warisan, atau merancang microservices, memahami prinsip OOP adalah keharusan.

Kemampuan OOP PHP telah berkembang signifikan sejak PHP 5. Saat ini, PHP 8+ menawarkan fitur-fitur tangguh seperti typed properties, constructor property promotion, dan named arguments yang membuat pola OOP lebih ekspresif dan mudah dirawat.

Panduan ini mencakup lima konsep OOP inti dengan contoh tingkat produksi yang dapat Anda gunakan segera dalam proyek Anda.

Daftar Isi

Kelas dan Objek

Kelas adalah cetak biru. Objek adalah instansi dari cetak biru tersebut. Pikirkan kelas sebagai pemotong kue dan objek sebagai kue yang sebenarnya.

Definisi Kelas Dasar
class User
{
    public string $name;
    public string $email;
    private string $password;
 
    public function __construct(string $name, string $email, string $password)
    {
        $this->name = $name;
        $this->email = $email;
        $this->password = $password;
    }
 
    public function getEmail(): string
    {
        return $this->email;
    }
 
    private function hashPassword(string $password): string
    {
        return password_hash($password, PASSWORD_BCRYPT);
    }
}
 
// Membuat objek
$user = new User('John Doe', 'john@example.com', 'secret123');
echo $user->name; // John Doe

Di PHP 8+, Anda dapat menggunakan constructor property promotion untuk mengurangi boilerplate:

Constructor Property Promotion (PHP 8+)
class User
{
    public function __construct(
        public string $name,
        public string $email,
        private string $password,
    ) {}
}
 
$user = new User('John Doe', 'john@example.com', 'secret123');

Ini secara otomatis mendeklarasikan dan menetapkan properti dalam konstruktor, membuat kode lebih bersih dan mudah dibaca.

Enkapsulasi

Enkapsulasi adalah tentang menyembunyikan detail internal dan hanya mengekspos apa yang diperlukan. Ini melindungi keadaan objek Anda dari gangguan eksternal yang tidak diinginkan.

PHP menyediakan tiga modifier visibilitas:

  • public: Dapat diakses dari mana saja
  • protected: Dapat diakses dalam kelas dan subkelasnya
  • private: Hanya dapat diakses dalam kelas
Enkapsulasi dengan Getter dan Setter
class BankAccount
{
    private float $balance = 0;
    private string $accountNumber;
 
    public function __construct(string $accountNumber)
    {
        $this->accountNumber = $accountNumber;
    }
 
    public function deposit(float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('Jumlah setoran harus positif');
        }
        $this->balance += $amount;
    }
 
    public function withdraw(float $amount): void
    {
        if ($amount > $this->balance) {
            throw new Exception('Saldo tidak cukup');
        }
        $this->balance -= $amount;
    }
 
    public function getBalance(): float
    {
        return $this->balance;
    }
 
    public function getAccountNumber(): string
    {
        return $this->accountNumber;
    }
}
 
$account = new BankAccount('ACC123456');
$account->deposit(1000);
$account->withdraw(200);
echo $account->getBalance(); // 800
 
// Ini akan gagal - balance bersifat private
// $account->balance = 5000; // Error: Cannot access private property

Mengapa enkapsulasi penting: Anda mengontrol bagaimana data berubah. Dalam contoh di atas, Anda mencegah setoran negatif dan overdraft di sumbernya, bukan dalam kode pemanggil.

Pewarisan

Pewarisan memungkinkan kelas untuk mewarisi properti dan metode dari kelas lain. Ini mempromosikan penggunaan kembali kode dan membangun hubungan hierarki.

Contoh Pewarisan
class Animal
{
    protected string $name;
    protected int $age;
 
    public function __construct(string $name, int $age)
    {
        $this->name = $name;
        $this->age = $age;
    }
 
    public function eat(): string
    {
        return "{$this->name} sedang makan";
    }
 
    public function sleep(): string
    {
        return "{$this->name} sedang tidur";
    }
 
    public function getInfo(): string
    {
        return "{$this->name} berusia {$this->age} tahun";
    }
}
 
class Dog extends Animal
{
    private string $breed;
 
    public function __construct(string $name, int $age, string $breed)
    {
        parent::__construct($name, $age);
        $this->breed = $breed;
    }
 
    public function bark(): string
    {
        return "{$this->name} berkata: Guk! Guk!";
    }
 
    public function getInfo(): string
    {
        return parent::getInfo() . " dan merupakan {$this->breed}";
    }
}
 
$dog = new Dog('Buddy', 3, 'Golden Retriever');
echo $dog->eat();      // Buddy sedang makan
echo $dog->bark();     // Buddy berkata: Guk! Guk!
echo $dog->getInfo();  // Buddy berusia 3 tahun dan merupakan Golden Retriever

Poin-poin kunci:

  • Gunakan extends untuk mewarisi dari kelas induk
  • Gunakan parent:: untuk memanggil metode kelas induk
  • Kelas anak dapat menimpa metode induk
  • Properti protected dapat diakses dalam kelas anak

Polimorfisme

Polimorfisme berarti "banyak bentuk". Ini memungkinkan objek dari kelas berbeda diperlakukan melalui antarmuka yang sama. Ini sangat kuat untuk menulis kode yang fleksibel dan mudah dirawat.

Penimpa Metode

Polimorfisme melalui Penimpa Metode
interface PaymentProcessor
{
    public function process(float $amount): bool;
    public function refund(float $amount): bool;
}
 
class CreditCardProcessor implements PaymentProcessor
{
    public function process(float $amount): bool
    {
        // Proses pembayaran kartu kredit
        return true;
    }
 
    public function refund(float $amount): bool
    {
        // Pengembalian dana ke kartu kredit
        return true;
    }
}
 
class PayPalProcessor implements PaymentProcessor
{
    public function process(float $amount): bool
    {
        // Proses pembayaran PayPal
        return true;
    }
 
    public function refund(float $amount): bool
    {
        // Pengembalian dana via PayPal
        return true;
    }
}
 
class StripeProcessor implements PaymentProcessor
{
    public function process(float $amount): bool
    {
        // Proses pembayaran Stripe
        return true;
    }
 
    public function refund(float $amount): bool
    {
        // Pengembalian dana via Stripe
        return true;
    }
}
 
// Penggunaan polimorfik
function processOrder(PaymentProcessor $processor, float $amount): void
{
    if ($processor->process($amount)) {
        echo "Pembayaran berhasil diproses";
    }
}
 
$creditCard = new CreditCardProcessor();
$paypal = new PayPalProcessor();
$stripe = new StripeProcessor();
 
processOrder($creditCard, 99.99);
processOrder($paypal, 49.99);
processOrder($stripe, 199.99);

Fungsi processOrder tidak peduli pemroses pembayaran mana yang digunakan. Ini bekerja dengan kelas apa pun yang mengimplementasikan PaymentProcessor. Ini adalah kekuatan polimorfisme.

Kelas Abstrak

Kelas abstrak mendefinisikan kontrak yang harus diikuti subkelas:

Kelas Abstrak
abstract class DatabaseConnection
{
    protected string $host;
    protected string $database;
 
    public function __construct(string $host, string $database)
    {
        $this->host = $host;
        $this->database = $database;
    }
 
    abstract public function connect(): void;
    abstract public function query(string $sql): array;
    abstract public function close(): void;
 
    public function getDatabase(): string
    {
        return $this->database;
    }
}
 
class MySQLConnection extends DatabaseConnection
{
    private $connection;
 
    public function connect(): void
    {
        // Logika koneksi MySQL
        $this->connection = mysqli_connect($this->host, 'user', 'pass', $this->database);
    }
 
    public function query(string $sql): array
    {
        // Jalankan query MySQL
        return [];
    }
 
    public function close(): void
    {
        mysqli_close($this->connection);
    }
}
 
class PostgreSQLConnection extends DatabaseConnection
{
    private $connection;
 
    public function connect(): void
    {
        // Logika koneksi PostgreSQL
        $this->connection = pg_connect("host={$this->host} dbname={$this->database}");
    }
 
    public function query(string $sql): array
    {
        // Jalankan query PostgreSQL
        return [];
    }
 
    public function close(): void
    {
        pg_close($this->connection);
    }
}

Abstraksi

Abstraksi menyembunyikan kompleksitas dan menampilkan hanya fitur penting. Ini tentang membuat antarmuka yang disederhanakan untuk kode yang kompleks.

Abstraksi dalam Aksi
// Logika internal kompleks disembunyikan
class EmailService
{
    private SMTPClient $smtpClient;
    private TemplateEngine $templateEngine;
    private Logger $logger;
 
    public function __construct(
        SMTPClient $smtpClient,
        TemplateEngine $templateEngine,
        Logger $logger
    ) {
        $this->smtpClient = $smtpClient;
        $this->templateEngine = $templateEngine;
        $this->logger = $logger;
    }
 
    // Antarmuka publik yang sederhana
    public function sendWelcomeEmail(string $email, string $name): bool
    {
        try {
            $template = $this->templateEngine->render('welcome', ['name' => $name]);
            $this->smtpClient->send($email, 'Selamat Datang!', $template);
            $this->logger->info("Email selamat datang dikirim ke {$email}");
            return true;
        } catch (Exception $e) {
            $this->logger->error("Gagal mengirim email selamat datang: {$e->getMessage()}");
            return false;
        }
    }
 
    public function sendPasswordResetEmail(string $email, string $resetToken): bool
    {
        try {
            $template = $this->templateEngine->render('password-reset', ['token' => $resetToken]);
            $this->smtpClient->send($email, 'Atur Ulang Kata Sandi Anda', $template);
            $this->logger->info("Email pengaturan ulang kata sandi dikirim ke {$email}");
            return true;
        } catch (Exception $e) {
            $this->logger->error("Gagal mengirim email pengaturan ulang kata sandi: {$e->getMessage()}");
            return false;
        }
    }
}
 
// Kode klien tidak perlu tahu tentang SMTP, template, atau logging
$emailService = new EmailService($smtp, $template, $logger);
$emailService->sendWelcomeEmail('user@example.com', 'John');

Pemanggil tidak perlu memahami protokol SMTP, rendering template, atau mekanisme logging. Mereka hanya memanggil metode sederhana.

Komposisi vs Pewarisan

Komposisi sering lebih baik daripada pewarisan. Alih-alih mewarisi perilaku, Anda menyusun objek yang memiliki perilaku yang Anda butuhkan.

Contoh Komposisi
// Alih-alih mewarisi dari Logger
class UserRepository
{
    private Logger $logger;
    private Database $database;
 
    public function __construct(Logger $logger, Database $database)
    {
        $this->logger = $logger;
        $this->database = $database;
    }
 
    public function findById(int $id): ?User
    {
        $this->logger->debug("Mengambil pengguna dengan ID: {$id}");
        $result = $this->database->query("SELECT * FROM users WHERE id = ?", [$id]);
        return $result ? new User($result) : null;
    }
 
    public function save(User $user): bool
    {
        try {
            $this->database->query("INSERT INTO users ...", $user->toArray());
            $this->logger->info("Pengguna disimpan: {$user->getId()}");
            return true;
        } catch (Exception $e) {
            $this->logger->error("Gagal menyimpan pengguna: {$e->getMessage()}");
            return false;
        }
    }
}

Komposisi fleksibel: Anda dapat menukar implementasi logger atau database tanpa mengubah UserRepository.

Antarmuka

Antarmuka mendefinisikan kontrak. Kelas yang mengimplementasikan antarmuka harus mengimplementasikan semua metodenya.

Definisi dan Implementasi Antarmuka
interface CacheStore
{
    public function get(string $key): mixed;
    public function put(string $key, mixed $value, int $ttl = 3600): void;
    public function forget(string $key): void;
    public function flush(): void;
}
 
class RedisCache implements CacheStore
{
    private Redis $redis;
 
    public function __construct(Redis $redis)
    {
        $this->redis = $redis;
    }
 
    public function get(string $key): mixed
    {
        return $this->redis->get($key);
    }
 
    public function put(string $key, mixed $value, int $ttl = 3600): void
    {
        $this->redis->setex($key, $ttl, serialize($value));
    }
 
    public function forget(string $key): void
    {
        $this->redis->del($key);
    }
 
    public function flush(): void
    {
        $this->redis->flushAll();
    }
}
 
class FileCache implements CacheStore
{
    private string $path;
 
    public function __construct(string $path)
    {
        $this->path = $path;
    }
 
    public function get(string $key): mixed
    {
        $file = $this->path . '/' . md5($key);
        if (!file_exists($file)) {
            return null;
        }
        $data = unserialize(file_get_contents($file));
        if ($data['expires'] < time()) {
            unlink($file);
            return null;
        }
        return $data['value'];
    }
 
    public function put(string $key, mixed $value, int $ttl = 3600): void
    {
        $file = $this->path . '/' . md5($key);
        $data = ['value' => $value, 'expires' => time() + $ttl];
        file_put_contents($file, serialize($data));
    }
 
    public function forget(string $key): void
    {
        $file = $this->path . '/' . md5($key);
        if (file_exists($file)) {
            unlink($file);
        }
    }
 
    public function flush(): void
    {
        array_map('unlink', glob($this->path . '/*'));
    }
}
 
// Kedua implementasi memenuhi antarmuka
function cacheUserData(CacheStore $cache, int $userId, array $data): void
{
    $cache->put("user:{$userId}", $data, 3600);
}
 
$redisCache = new RedisCache($redis);
$fileCache = new FileCache('/tmp/cache');
 
cacheUserData($redisCache, 1, ['name' => 'John']);
cacheUserData($fileCache, 1, ['name' => 'John']);

Trait

Trait memungkinkan Anda menggunakan kembali metode di seluruh kelas yang tidak terkait tanpa pewarisan.

Menggunakan Trait untuk Penggunaan Kembali Kode
trait Timestampable
{
    private DateTime $createdAt;
    private DateTime $updatedAt;
 
    public function setCreatedAt(DateTime $date): void
    {
        $this->createdAt = $date;
    }
 
    public function getCreatedAt(): DateTime
    {
        return $this->createdAt;
    }
 
    public function setUpdatedAt(DateTime $date): void
    {
        $this->updatedAt = $date;
    }
 
    public function getUpdatedAt(): DateTime
    {
        return $this->updatedAt;
    }
}
 
trait Loggable
{
    private array $logs = [];
 
    public function addLog(string $message): void
    {
        $this->logs[] = [
            'message' => $message,
            'timestamp' => date('Y-m-d H:i:s'),
        ];
    }
 
    public function getLogs(): array
    {
        return $this->logs;
    }
}
 
class BlogPost
{
    use Timestampable, Loggable;
 
    private string $title;
    private string $content;
 
    public function __construct(string $title, string $content)
    {
        $this->title = $title;
        $this->content = $content;
        $this->setCreatedAt(new DateTime());
        $this->addLog('Postingan dibuat');
    }
 
    public function publish(): void
    {
        $this->setUpdatedAt(new DateTime());
        $this->addLog('Postingan dipublikasikan');
    }
}
 
$post = new BlogPost('Postingan Pertama Saya', 'Konten di sini...');
$post->publish();
print_r($post->getLogs());

Properti dan Metode Statis

Anggota statis milik kelas, bukan instansi. Gunakan untuk data bersama atau fungsi utilitas.

Anggota Statis
class Config
{
    private static array $settings = [];
    private static bool $initialized = false;
 
    public static function initialize(array $config): void
    {
        if (self::$initialized) {
            throw new Exception('Konfigurasi sudah diinisialisasi');
        }
        self::$settings = $config;
        self::$initialized = true;
    }
 
    public static function get(string $key, mixed $default = null): mixed
    {
        return self::$settings[$key] ?? $default;
    }
 
    public static function set(string $key, mixed $value): void
    {
        self::$settings[$key] = $value;
    }
}
 
Config::initialize(['app_name' => 'MyApp', 'debug' => true]);
echo Config::get('app_name'); // MyApp
Config::set('debug', false);

Warning

Anggota statis dapat membuat pengujian sulit dan menciptakan ketergantungan tersembunyi. Gunakan dengan hemat. Injeksi ketergantungan biasanya lebih baik.

Kesalahan dan Jebakan Umum

Over-Engineering dengan Pewarisan

❌ Buruk: Hierarki Pewarisan Dalam
class Animal {}
class Mammal extends Animal {}
class Carnivore extends Mammal {}
class Feline extends Carnivore {}
class DomesticCat extends Feline {}
 
// Ini rapuh dan sulit dirawat
✅ Baik: Komposisi dengan Antarmuka
interface Eater {
    public function eat(): void;
}
 
interface Sleeper {
    public function sleep(): void;
}
 
class Cat implements Eater, Sleeper {
    public function eat(): void {}
    public function sleep(): void {}
}

Melanggar Enkapsulasi

❌ Buruk: Mengekspos Keadaan Internal
class User
{
    public array $data = [];
}
 
$user = new User();
$user->data['password'] = 'plaintext'; // Berbahaya!
✅ Baik: Akses Terkontrol
class User
{
    private string $password;
 
    public function setPassword(string $password): void
    {
        $this->password = password_hash($password, PASSWORD_BCRYPT);
    }
}

Mengabaikan Type Hints

❌ Buruk: Tanpa Keamanan Tipe
function processUser($user)
{
    return $user->getName();
}
✅ Baik: Type Hints Mencegah Kesalahan
function processUser(User $user): string
{
    return $user->getName();
}

Praktik Terbaik

Gunakan Injeksi Ketergantungan

Pola Injeksi Ketergantungan
class OrderService
{
    public function __construct(
        private PaymentProcessor $paymentProcessor,
        private EmailService $emailService,
        private Logger $logger,
    ) {}
 
    public function placeOrder(Order $order): bool
    {
        if (!$this->paymentProcessor->process($order->getTotal())) {
            $this->logger->error('Pembayaran gagal untuk pesanan ' . $order->getId());
            return false;
        }
 
        $this->emailService->sendOrderConfirmation($order);
        $this->logger->info('Pesanan ditempatkan: ' . $order->getId());
        return true;
    }
}

Ikuti Prinsip SOLID

  • Single Responsibility: Satu kelas, satu alasan untuk berubah
  • Open/Closed: Terbuka untuk ekstensi, tertutup untuk modifikasi
  • Liskov Substitution: Subtipe harus dapat diganti dengan tipe dasarnya
  • Interface Segregation: Banyak antarmuka spesifik daripada satu antarmuka umum
  • Dependency Inversion: Bergantung pada abstraksi, bukan konkretisasi

Gunakan Deklarasi Tipe

Deklarasi Tipe PHP 8+
class Product
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly float $price,
        public readonly bool $available = true,
    ) {}
 
    public function getDiscountedPrice(float $discountPercent): float
    {
        return $this->price * (1 - $discountPercent / 100);
    }
 
    public function isAffordable(float $budget): bool
    {
        return $this->price <= $budget;
    }
}

Manfaatkan Fitur PHP Modern

Named Arguments dan Match Expression
class NotificationService
{
    public function send(
        string $recipient,
        string $message,
        string $channel = 'email',
        bool $urgent = false,
    ): void {
        $priority = match ($channel) {
            'sms' => $urgent ? 'high' : 'normal',
            'email' => 'normal',
            'push' => 'high',
            default => throw new InvalidArgumentException("Saluran tidak dikenal: {$channel}"),
        };
    }
}
 
// Named arguments membuat panggilan jelas
$service->send(
    recipient: 'user@example.com',
    message: 'Pesanan Anda siap',
    channel: 'email',
    urgent: false,
);

Kapan TIDAK Menggunakan OOP

OOP bukan selalu jawabannya. Pertimbangkan alternatif:

  • Skrip sederhana: Skrip prosedural mungkin lebih jelas daripada OOP yang over-engineered
  • Transformasi fungsional: Gunakan pemrograman fungsional untuk pipeline data
  • Utilitas sekali pakai: Tidak semuanya perlu menjadi kelas
Kapan Prosedural Lebih Baik
// ✅ Sederhana dan jelas
function calculateTax(float $amount, float $rate): float
{
    return $amount * ($rate / 100);
}
 
// ❌ Over-engineered
class TaxCalculator
{
    private float $rate;
    public function __construct(float $rate) { $this->rate = $rate; }
    public function calculate(float $amount): float { return $amount * ($this->rate / 100); }
}

Kesimpulan

OOP di PHP menyediakan alat yang kuat untuk membangun aplikasi yang mudah dirawat dan dapat diskalakan. Kuasai konsep-konsep ini:

  1. Kelas dan Objek: Fondasi OOP
  2. Enkapsulasi: Lindungi data Anda dengan modifier visibilitas
  3. Pewarisan: Gunakan kembali kode melalui hierarki kelas
  4. Polimorfisme: Tulis kode fleksibel yang bekerja dengan berbagai tipe
  5. Abstraksi: Sembunyikan kompleksitas di balik antarmuka sederhana
  6. Komposisi: Sering lebih baik daripada pewarisan
  7. Antarmuka dan Trait: Bagikan perilaku di seluruh kelas yang tidak terkait

Mulai dengan prinsip SOLID, gunakan injeksi ketergantungan, dan manfaatkan fitur PHP 8+ seperti typed properties dan constructor promotion. Kode Anda akan lebih bersih, lebih mudah diuji, dan lebih mudah dirawat.

Kode OOP terbaik adalah kode yang menyelesaikan masalah dengan jelas tanpa kompleksitas yang tidak perlu. Tulis untuk pengembang berikutnya yang membaca kode Anda—itu mungkin Anda dalam enam bulan.


Related Posts