Extending a Class
<?php
class Animal {
public function __construct(public string $name) {}
public function speak(): string {
return "Some sound";
}
}
class Dog extends Animal {
public function speak(): string {
return "Woof!";
}
}
$d = new Dog("Rex");
echo $d->name; // Rex <- inherited
echo $d->speak(); // Woof! <- overridden
var_dump($d instanceof Dog); // true
var_dump($d instanceof Animal); // true (also a parent!)
Calling parent::
<?php
class Logger {
public function __construct(protected string $prefix) {}
public function log(string $msg): void {
echo "[{$this->prefix}] $msg\n";
}
}
class TimestampLogger extends Logger {
public function __construct(string $prefix) {
parent::__construct($prefix); // CALL PARENT - or fields stay unset
}
public function log(string $msg): void {
$time = date("H:i:s");
parent::log("$time | $msg"); // call parent's implementation
}
}
(new TimestampLogger("APP"))->log("Started");
// [APP] 14:33:12 | Started
If your child class defines a constructor, the parent's constructor will NOT run unless you call it explicitly. Forget this and the parent's properties stay uninitialized - a common source of TypeErrors with typed properties.
Method Overriding
<?php
class PaymentMethod {
public function fee(float $amount): float {
return 0.0; // default: no fee
}
}
class CreditCard extends PaymentMethod {
public function fee(float $amount): float {
return $amount * 0.029 + 0.30; // 2.9% + $0.30
}
}
class BankTransfer extends PaymentMethod {
public function fee(float $amount): float {
return min(5.00, $amount * 0.01); // 1% capped at $5
}
}
function checkout(PaymentMethod $method, float $amount): float {
return $amount + $method->fee($amount);
}
echo checkout(new CreditCard(), 100); // 103.20
echo checkout(new BankTransfer(), 100); // 101.00
Abstract Classes & Methods
An abstract class cannot be instantiated. It exists to be extended:
<?php
abstract class Shape {
abstract public function area(): float;
abstract public function perimeter(): float;
// Concrete shared method
public function describe(): string {
return sprintf(
"Area: %.2f, Perimeter: %.2f",
$this->area(),
$this->perimeter()
);
}
}
class Circle extends Shape {
public function __construct(public float $radius) {}
public function area(): float {
return M_PI * $this->radius ** 2;
}
public function perimeter(): float {
return 2 * M_PI * $this->radius;
}
}
// new Shape(); // FATAL - cannot instantiate abstract
$c = new Circle(5);
echo $c->describe();
final - Prevent Override
<?php
// final class - cannot be extended
final class Money {
public function __construct(public readonly int $cents) {}
}
// class Wallet extends Money {} // FATAL
// final method - cannot be overridden
class Container {
public final function id(): string {
return spl_object_hash($this);
}
}
class Child extends Container {
// public function id(): string { ... } // FATAL
}
Polymorphism
Code that operates on the parent type automatically works with any subtype - the key benefit of OOP:
<?php
function printShapes(array $shapes): void {
foreach ($shapes as $shape) {
echo $shape->describe() . "\n";
}
}
$mix = [
new Circle(5),
new Square(4),
new Triangle(3, 4, 5),
];
printShapes($mix);
// No need to know concrete types - all answer area()/perimeter()
Composition Over Inheritance
A famous design principle. Before reaching for extends, ask "could this be a field instead?":
<?php
// ANTI-PATTERN - inheritance for code reuse
class Database {
public function query(string $sql) { /* ... */ }
}
class UserService extends Database { // UserService IS-A Database? No!
public function find(int $id) { return $this->query("..."); }
}
// BETTER - composition
class UserService {
public function __construct(private Database $db) {}
public function find(int $id) {
return $this->db->query("...");
}
}
// Now UserService is easy to test - inject a fake $db
Inherit only when the child is a true subtype of the parent - a Dog IS-A Animal, a Manager IS-A Employee. Otherwise compose. The Liskov Substitution Principle says: anywhere the parent is used, the child should work without surprises.