Classes #

Classes are the foundation of object-oriented programming in PHP — blueprints that define the data (properties) and behavior (methods) that every object created from them has. Almost all modern PHP frameworks — Laravel, Symfony, CodeIgniter — are built entirely on classes. Understanding classes isn’t just knowing the syntax, but also grasping the principles that make them useful: encapsulation that keeps data consistent, inheritance that avoids duplication, polymorphism that makes code flexible, and knowing when a concept truly deserves to be a class rather than just a function. This article covers all aspects of PHP classes in depth, including modern features like constructor promotion, readonly properties, and anonymous classes.

Anatomy of a PHP Class #

A PHP class consists of several parts, each with its own rules and conventional ordering:

flowchart TD
    A[class ClassName] --> B[extends ParentClass]
    A --> C[implements Interface]
    A --> D[Class Contents]

    D --> D1["Constants\nconst NAME = value"]
    D --> D2["Properties\npublic/protected/private"]
    D --> D3["Constructor\n__construct()"]
    D --> D4["Public methods\nthe class's external API"]
    D --> D5["Protected methods\nfor subclasses"]
    D --> D6["Private methods\ninternal implementation"]

    style D1 fill:#dbeafe
    style D2 fill:#dcfce7
    style D3 fill:#fef9c3
    style D4 fill:#f3e8ff
    style D5 fill:#ffedd5
    style D6 fill:#fee2e2

Defining a Class #

A class is defined with the class keyword, followed by the class name in PascalCase format. Each class should ideally be in its own file with the same name as the class.

<?php
class Product
{
    // Properties — data owned by each instance
    public string  $name;
    public float   $price;
    private int    $stock;
    protected string $category;

    // Constructor — runs when new Product(...) is called
    public function __construct(
        string $name,
        float  $price,
        int    $stock     = 0,
        string $category  = 'general',
    ) {
        $this->name     = $name;
        $this->price    = $price;
        $this->stock    = $stock;
        $this->category = $category;
    }

    // Public methods — the outward-facing interface
    public function isAvailable(): bool
    {
        return $this->stock > 0;
    }

    public function formattedPrice(): string
    {
        return 'Rp ' . number_format($this->price, 0, ',', '.');
    }

    public function getStock(): int
    {
        return $this->stock;
    }

    // Private methods — internal implementation
    private function validateStock(int $amount): void
    {
        if ($amount < 0) {
            throw new \InvalidArgumentException("Stock must not be negative");
        }
    }

    public function addStock(int $amount): void
    {
        $this->validateStock($amount);
        $this->stock += $amount;
    }

    public function reduceStock(int $amount): void
    {
        $this->validateStock($amount);
        if ($amount > $this->stock) {
            throw new \RuntimeException("Insufficient stock");
        }
        $this->stock -= $amount;
    }
}

// Creating an instance (object)
$laptop = new Product('ProBook Laptop', 15_000_000, 5, 'electronics');

echo $laptop->name;            // ProBook Laptop
echo $laptop->formattedPrice();  // Rp 15.000.000
var_dump($laptop->isAvailable()); // bool(true)

$laptop->addStock(3);
echo $laptop->getStock();       // 8

Constructor Promotion (PHP 8.0+) #

Constructor promotion shortens the declaration of properties whose values come directly from constructor parameters. Instead of declaring properties above and then assigning them one by one in the constructor, just add a visibility modifier to the constructor parameters:

<?php
// ANTI-PATTERN: the old way — properties declared twice
class LegacyProduct
{
    public string $name;
    public float  $price;
    private int   $stock;

    public function __construct(string $name, float $price, int $stock = 0)
    {
        $this->name  = $name;
        $this->price = $price;
        $this->stock = $stock;
    }
}

// CORRECT: constructor promotion — more concise, identical result
class Product
{
    public function __construct(
        public string  $name,
        public float   $price,
        private int    $stock     = 0,
        protected string $category = 'general',
    ) {
        // the constructor body can still exist for initial validation
        if ($price < 0) {
            throw new \InvalidArgumentException("Price must not be negative");
        }
    }
}

// Usage is identical
$p = new Product('Mouse', 250_000, 10);
echo $p->name;  // Mouse
echo $p->price; // 250000

Constructor promotion can also be combined with non-promoted parameters in a single constructor — promoted parameters are distinguished by the presence of a visibility modifier.


Visibility (Access Modifiers) #

PHP has three visibility levels that control where properties and methods can be accessed:

ModifierFrom the class itselfFrom subclassesFrom outside the class
public
protected
private
<?php
class BankAccount
{
    private float  $balance     = 0;
    private array  $history     = [];
    protected string $accountNumber;

    public function __construct(string $accountNumber, float $initialBalance = 0)
    {
        $this->accountNumber = $accountNumber;
        $this->balance       = $initialBalance;
    }

    // Public — the API anyone may use
    public function getBalance(): float
    {
        return $this->balance;
    }

    public function deposit(float $amount): void
    {
        $this->validateAmount($amount);
        $this->balance += $amount;
        $this->recordHistory('deposit', $amount);
    }

    public function withdraw(float $amount): void
    {
        $this->validateAmount($amount);
        if ($amount > $this->balance) {
            throw new \RuntimeException("Insufficient balance");
        }
        $this->balance -= $amount;
        $this->recordHistory('withdraw', $amount);
    }

    // Private — only for this class's internal implementation
    private function validateAmount(float $amount): void
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException("Amount must be positive");
        }
    }

    private function recordHistory(string $type, float $amount): void
    {
        $this->history[] = [
            'type'    => $type,
            'amount'  => $amount,
            'time'    => time(),
            'balance' => $this->balance,
        ];
    }

    public function getHistory(): array
    {
        return $this->history; // return a copy, not a reference
    }
}

$account = new BankAccount('BCA-001', 1_000_000);
$account->deposit(500_000);
$account->withdraw(200_000);
echo $account->getBalance(); // 1300000

// $account->balance = 999999999; // Fatal Error — private property
// $account->validateAmount(100); // Fatal Error — private method

The Encapsulation Principle #

Encapsulation isn’t just about hiding properties — it’s about maintaining the object’s invariants: conditions that must always hold true for the object’s lifetime.

<?php
// ANTI-PATTERN: everything public — no invariant protection
class LegacyTemperature
{
    public float $celsius;

    public function __construct(float $celsius)
    {
        $this->celsius = $celsius; // can be set to -999 without validation
    }
}

$temp = new LegacyTemperature(25);
$temp->celsius = -999; // valid in PHP, but makes no physical sense

// CORRECT: encapsulation maintains the invariant
class Temperature
{
    private float $celsius;

    // The absolute minimum temperature is -273.15°C (0 Kelvin)
    private const ABSOLUTE_MINIMUM = -273.15;

    public function __construct(float $celsius)
    {
        $this->setCelsius($celsius);
    }

    public function getCelsius(): float { return $this->celsius; }

    public function getFahrenheit(): float
    {
        return ($this->celsius * 9 / 5) + 32;
    }

    public function getKelvin(): float
    {
        return $this->celsius - self::ABSOLUTE_MINIMUM;
    }

    public function setCelsius(float $celsius): void
    {
        if ($celsius < self::ABSOLUTE_MINIMUM) {
            throw new \InvalidArgumentException(
                "Temperature must not go below " . self::ABSOLUTE_MINIMUM . "°C"
            );
        }
        $this->celsius = $celsius;
    }
}

$temp = new Temperature(25);
echo $temp->getFahrenheit(); // 77
echo $temp->getKelvin();     // 298.15
// $temp->setCelsius(-300);  // InvalidArgumentException

Inheritance #

Inheritance allows a child class to inherit properties and methods from a parent class, then extend or change their behavior. PHP only supports single inheritance — one class can extend only one parent class.

<?php
class Vehicle
{
    public function __construct(
        protected string $brand,
        protected int    $year,
        protected float  $price,
    ) {}

    public function getInfo(): string
    {
        return "{$this->brand} ({$this->year})";
    }

    public function getFormattedPrice(): string
    {
        return 'Rp ' . number_format($this->price, 0, ',', '.');
    }

    // A method that child classes can override
    public function description(): string
    {
        return $this->getInfo() . ' — ' . $this->getFormattedPrice();
    }
}

class Car extends Vehicle
{
    public function __construct(
        string $brand,
        int    $year,
        float  $price,
        private int $numberOfDoors = 4,
    ) {
        parent::__construct($brand, $year, $price); // must call parent
    }

    // Override the parent method
    public function description(): string
    {
        return parent::description() . ", {$this->numberOfDoors} doors";
    }
}

class ElectricMotorcycle extends Vehicle
{
    public function __construct(
        string $brand,
        int    $year,
        float  $price,
        private int $batteryCapacity, // kWh
    ) {
        parent::__construct($brand, $year, $price);
    }

    public function description(): string
    {
        return parent::description() . ", battery {$this->batteryCapacity} kWh";
    }

    public function estimatedRange(): int
    {
        return $this->batteryCapacity * 6; // rough estimate: 6 km per kWh
    }
}

$avanza     = new Car('Toyota Avanza', 2023, 250_000_000, 5);
$vespaPrime = new ElectricMotorcycle('Vespa Elettrica', 2023, 95_000_000, 4);

echo $avanza->description();
// Toyota Avanza (2023) — Rp 250.000.000, 5 doors

echo $vespaPrime->description();
// Vespa Elettrica (2023) — Rp 95.000.000, battery 4 kWh

echo $vespaPrime->estimatedRange(); // 24

// instanceof checks whether an object is an instance of a class or its parent
var_dump($avanza instanceof Car);      // true
var_dump($avanza instanceof Vehicle);  // true — because Car extends Vehicle
var_dump($avanza instanceof ElectricMotorcycle); // false

parent:: and Calling Parent Methods #

<?php
class Logger
{
    public function log(string $message): void
    {
        echo "[LOG] $message\n";
    }
}

class DatabaseLogger extends Logger
{
    public function log(string $message): void
    {
        parent::log($message);                  // call the parent implementation first
        $this->saveToDatabase($message);        // then add your own behavior
    }

    private function saveToDatabase(string $message): void
    {
        // save to DB...
    }
}

Abstract Classes #

An abstract class is a class that can’t be instantiated directly — it can only be a parent. Abstract methods are methods without an implementation that must be overridden by child classes.

<?php
abstract class Payment
{
    protected float $amount;
    protected string $reference;

    public function __construct(float $amount)
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException("Amount must be positive");
        }
        $this->amount    = $amount;
        $this->reference = $this->generateReference();
    }

    // Abstract — every child class MUST implement these
    abstract public function process(): bool;
    abstract public function methodName(): string;

    // Concrete methods — available to all child classes
    public function getReference(): string
    {
        return $this->reference;
    }

    public function getAmount(): float
    {
        return $this->amount;
    }

    private function generateReference(): string
    {
        return strtoupper(uniqid('PAY-'));
    }

    // Template method — orchestration defined by the parent
    final public function pay(): array
    {
        $success = $this->process();

        return [
            'reference' => $this->reference,
            'method'    => $this->methodName(),
            'amount'    => $this->amount,
            'status'    => $success ? 'success' : 'failed',
        ];
    }
}

class BankTransferPayment extends Payment
{
    public function __construct(
        float          $amount,
        private string $destinationAccount,
    ) {
        parent::__construct($amount);
    }

    public function process(): bool
    {
        // Bank transfer logic
        return true;
    }

    public function methodName(): string
    {
        return 'Bank Transfer to ' . $this->destinationAccount;
    }
}

class GopayPayment extends Payment
{
    public function __construct(
        float          $amount,
        private string $phoneNumber,
    ) {
        parent::__construct($amount);
    }

    public function process(): bool
    {
        // GoPay API logic
        return true;
    }

    public function methodName(): string
    {
        return 'GoPay ' . $this->phoneNumber;
    }
}

// new Payment(100000); // Fatal Error — abstract classes can't be instantiated

$transfer = new BankTransferPayment(500_000, 'BCA-1234567890');
$result   = $transfer->pay();
print_r($result);
// Array ( [reference] => PAY-xxx [method] => Bank Transfer... [amount] => 500000 [status] => success )

final — Preventing Override and Inheritance #

The final keyword on a class prevents it from being extended. On a method, it prevents the method from being overridden by child classes:

<?php
// Final class — cannot be extended
final class UUID
{
    private string $value;

    public function __construct()
    {
        $this->value = sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }

    public function __toString(): string
    {
        return $this->value;
    }
}

// class UUID4 extends UUID {} // Fatal Error — final class

// Final method — the class can be extended, but this method can't be overridden
class Config
{
    final public function getVersion(): string
    {
        return '2.0.0'; // the version must not be changed by child classes
    }
}

class LocalConfig extends Config
{
    // public function getVersion(): string { } // Fatal Error — overriding a final method
}

Static Properties and Methods #

Static properties and methods belong to the class, not to instances. They can be accessed without creating an object using ClassName::$property or ClassName::method().

<?php
class Connection
{
    private static ?PDO $instance = null;
    private static int  $queryCount = 0;

    // Singleton pattern — only one connection across the whole application
    public static function get(): PDO
    {
        if (self::$instance === null) {
            self::$instance = new PDO(
                'mysql:host=localhost;dbname=myapp',
                'root',
                '',
                [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
            );
        }
        return self::$instance;
    }

    public static function incrementQueryCount(): void
    {
        self::$queryCount++;
    }

    public static function getQueryCount(): int
    {
        return self::$queryCount;
    }
}

// Every part of the application gets the same PDO
$db1 = Connection::get();
$db2 = Connection::get();
var_dump($db1 === $db2); // true — the same instance

echo Connection::getQueryCount(); // 0

self:: vs static:: — Late Static Binding #

<?php
class ParentClass
{
    public static function create(): static
    {
        return new static(); // static:: — uses the class that was called
    }

    public static function className(): string
    {
        return static::class; // the actual class name at runtime
    }
}

class ChildClass extends ParentClass
{
    public string $type = 'child';
}

$parent = ParentClass::create();
$child  = ChildClass::create(); // returns a ChildClass instance, not ParentClass

var_dump($parent instanceof ParentClass); // true
var_dump($child instanceof ChildClass);   // true

echo ChildClass::className();  // "ChildClass" — not "ParentClass"

Magic Methods #

Magic methods are special methods called automatically by PHP in certain situations. Their names always start with a double underscore __.

<?php
class Collection
{
    private array $items = [];

    public function __construct(array $items = [])
    {
        $this->items = $items;
    }

    // Called when the object is used in a string context
    public function __toString(): string
    {
        return implode(', ', $this->items);
    }

    // Called on isset($obj->name) or empty($obj->name)
    public function __isset(string $name): bool
    {
        return isset($this->items[$name]);
    }

    // Called when accessing $obj->name for a property that doesn't exist
    public function __get(string $name): mixed
    {
        return $this->items[$name] ?? null;
    }

    // Called on $obj->name = value for a property that doesn't exist
    public function __set(string $name, mixed $value): void
    {
        $this->items[$name] = $value;
    }

    // Called on unset($obj->name) for a property that doesn't exist
    public function __unset(string $name): void
    {
        unset($this->items[$name]);
    }

    // Called when the object is used as a function: $obj()
    public function __invoke(mixed $item): static
    {
        $new = clone $this;
        $new->items[] = $item;
        return $new;
    }

    // Called on var_dump($obj) — controls debug output
    public function __debugInfo(): array
    {
        return [
            'item_count' => count($this->items),
            'items'      => $this->items,
        ];
    }
}

$collection = new Collection(['apple', 'mango']);
echo $collection;           // "apple, mango" — via __toString

$collection2 = $collection('orange'); // via __invoke — adds 'orange'
echo $collection2;          // "apple, mango, orange"

__clone() and Shallow vs Deep Copy #

<?php
class Order
{
    public array $items = [];
    public \DateTime $date;

    public function __construct()
    {
        $this->date = new \DateTime();
    }

    // Called when cloning $obj
    public function __clone()
    {
        // Shallow copy: $this->date would still point to the same object
        // Deep copy: create a new DateTime instance
        $this->date = clone $this->date;
    }
}

$order1 = new Order();
$order1->items = ['laptop', 'mouse'];

$order2 = clone $order1; // __clone() is called
$order2->items[] = 'keyboard'; // doesn't affect order1

var_dump(count($order1->items)); // 2 — unchanged
var_dump(count($order2->items)); // 3

Readonly Properties (PHP 8.1+) #

A readonly property can only be assigned once — usually in the constructor. After that its value can’t be changed, making the object partially immutable:

<?php
class MoneyTransfer
{
    public function __construct(
        public readonly string    $id,
        public readonly float     $amount,
        public readonly string    $from,
        public readonly string    $to,
        public readonly \DateTime $created,
    ) {}
}

$transfer = new MoneyTransfer(
    id:      'TRF-001',
    amount:  500_000,
    from:    'BCA-111',
    to:      'Mandiri-222',
    created: new \DateTime(),
);

echo $transfer->id;     // TRF-001
echo $transfer->amount; // 500000

// $transfer->amount = 0; // Fatal Error: Cannot modify readonly property

Readonly Classes (PHP 8.2+) #

An entire class can be made readonly, making all its properties automatically readonly:

<?php
readonly class Coordinates
{
    public function __construct(
        public float $lat,
        public float $lng,
        public float $altitude = 0,
    ) {}

    public function distanceTo(Coordinates $destination): float
    {
        // Haversine formula — approximate distance between two points on Earth
        $r    = 6371; // Earth's radius in km
        $dLat = deg2rad($destination->lat - $this->lat);
        $dLng = deg2rad($destination->lng - $this->lng);

        $a = sin($dLat / 2) ** 2
           + cos(deg2rad($this->lat))
           * cos(deg2rad($destination->lat))
           * sin($dLng / 2) ** 2;

        return $r * 2 * atan2(sqrt($a), sqrt(1 - $a));
    }
}

$jakarta  = new Coordinates(-6.2088, 106.8456);
$surabaya = new Coordinates(-7.2575, 112.7521);

echo round($jakarta->distanceTo($surabaya)) . " km"; // ~666 km

Anonymous Classes #

An anonymous class is a class without a name that’s created and instantiated at the same time. Useful for simple implementations of interfaces or abstract classes, especially in testing contexts:

<?php
interface Logger
{
    public function log(string $message): void;
}

// Anonymous class as a simple implementation
function processData(array $data, Logger $logger): void
{
    $logger->log("Processing " . count($data) . " items");
    // process data...
    $logger->log("Done");
}

// In production — use a real implementation
processData($data, new FileLogger('/var/log/app.log'));

// In testing — an anonymous class for a quick mock
$logBuffer = [];
processData($data, new class($logBuffer) implements Logger {
    public function __construct(private array &$buffer) {}

    public function log(string $message): void
    {
        $this->buffer[] = $message;
    }
});

// Check that the correct log was written
assert($logBuffer[0] === "Processing 5 items");

Summary #

  • Constructor promotion (PHP 8.0+) eliminates property declaration boilerplate — just add a visibility modifier to constructor parameters.
  • Visibility (public, protected, private) isn’t just access control — it’s about maintaining object invariants so data stays consistent and valid.
  • Good encapsulation hides implementation and only exposes the API that’s needed. Change internal state only through methods that validate input.
  • Inheritance is right for “is-a” relationships (a Car is a Vehicle). For “has-a” or “can-do” relationships, use composition or interfaces.
  • parent::__construct() must be called in a child class’s constructor when the parent class has a constructor that needs to run.
  • Abstract classes force child classes to implement specific methods while providing shared implementations for others.
  • final on a class prevents inheritance; on a method it prevents overriding — use it for stable APIs whose behavior must not change.
  • static:: vs self:: — use static:: for late static binding (referring to the actual runtime class), self:: to refer to the class that defines the method.
  • Readonly properties (PHP 8.1+) and readonly classes (PHP 8.2+) make objects immutable — values can only be set once in the constructor, never changed afterward.
  • Anonymous classes are useful for simple interface implementations, especially in testing as a replacement for heavy mock libraries.

← Previous: Functions   Next: Interfaces →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact