Skip to content

04: Classes & Inheritance

Inheritance Hero

Intermediate 90-120 min

Inheritance in PHP works similarly to Java—you use the extends keyword, can have abstract classes, and follow the same polymorphism principles. However, PHP has some unique features like late static binding and the parent keyword that differ from Java’s super. In this chapter, we’ll explore PHP’s inheritance model in depth, always comparing it to Java.

By the end of this chapter, you’ll understand how to build class hierarchies in PHP and leverage inheritance effectively.

::: info Time Estimate ⏱️ 90-120 minutes to complete this chapter :::

What you need:

  • Completed Chapter 3: OOP Basics
  • Solid understanding of Java inheritance
  • Familiarity with Java’s abstract classes and polymorphism

In this chapter, you’ll create:

  • A Shape hierarchy with Circle and Rectangle using abstract classes
  • An employee management system with inheritance
  • An abstract repository pattern implementation
  • A payment processing system demonstrating polymorphism

By the end of this chapter, you’ll be able to:

  • Use inheritance with the extends keyword
  • Create abstract classes and methods
  • Override methods with proper visibility rules
  • Use final to prevent inheritance or overriding
  • Understand late static binding (static:: vs self::)
  • Call parent methods with parent::
  • Apply polymorphism effectively in PHP

Learn how to extend classes in PHP and understand the similarities with Java.

::: code-group

<?php
declare(strict_types=1);
class Animal
{
protected string $name;
protected int $age;
public function __construct(string $name, int $age)
{
$this->name = $name;
$this->age = $age;
}
public function makeSound(): string
{
return "Some generic sound";
}
public function getInfo(): string
{
return "{$this->name} is {$this->age} years old";
}
}
class Dog extends Animal
{
private string $breed;
public function __construct(string $name, int $age, string $breed)
{
// Call parent constructor
parent::__construct($name, $age);
$this->breed = $breed;
}
// Override method
public function makeSound(): string
{
return "Woof! Woof!";
}
// Add new method
public function getBreed(): string
{
return $this->breed;
}
// Override and extend
public function getInfo(): string
{
return parent::getInfo() . " and is a {$this->breed}";
}
}
// Usage
$dog = new Dog("Buddy", 3, "Golden Retriever");
echo $dog->makeSound(); // "Woof! Woof!"
echo $dog->getInfo(); // "Buddy is 3 years old and is a Golden Retriever"
class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public String makeSound() {
return "Some generic sound";
}
public String getInfo() {
return name + " is " + age + " years old";
}
}
class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
// Call parent constructor
super(name, age);
this.breed = breed;
}
// Override method
@Override
public String makeSound() {
return "Woof! Woof!";
}
// Add new method
public String getBreed() {
return breed;
}
// Override and extend
@Override
public String getInfo() {
return super.getInfo() + " and is a " + breed;
}
}
// Usage
Dog dog = new Dog("Buddy", 3, "Golden Retriever");
System.out.println(dog.makeSound());
System.out.println(dog.getInfo());

:::

FeaturePHPJava
Extend keywordextendsextends
Call parent constructorparent::__construct()super()
Call parent methodparent::methodName()super.methodName()
Access parent members$this->parentPropertythis.parentField
Override annotationNo annotation (optional @Override in docs)@Override annotation
Multiple inheritanceNo (use interfaces/traits)No (use interfaces)

::: tip Parent vs Super

  • PHP: Use parent:: to call parent methods (static syntax)
  • Java: Use super. to call parent methods (object syntax)

Both serve the same purpose—accessing parent class members. :::

PHP requires explicit parent constructor calls:

<?php
declare(strict_types=1);
class Vehicle
{
public function __construct(
protected string $make,
protected string $model,
protected int $year
) {
echo "Vehicle constructor called\n";
}
}
class Car extends Vehicle
{
public function __construct(
string $make,
string $model,
int $year,
private int $doors
) {
// MUST explicitly call parent constructor
parent::__construct($make, $model, $year);
echo "Car constructor called\n";
}
public function getDetails(): string
{
return "{$this->year} {$this->make} {$this->model} ({$this->doors} doors)";
}
}
$car = new Car("Toyota", "Camry", 2024, 4);
// Output:
// Vehicle constructor called
// Car constructor called
echo $car->getDetails();

::: warning Constructor Inheritance Unlike Java, PHP does NOT automatically call the parent constructor. You must explicitly call parent::__construct() if you want to run the parent’s initialization code.

Java: Parent constructor called automatically PHP: Must call parent::__construct() explicitly :::

Constructor Property Promotion with Inheritance

Section titled “Constructor Property Promotion with Inheritance”

PHP 8’s constructor property promotion works with inheritance:

<?php
declare(strict_types=1);
// Parent with promoted properties
class Person
{
public function __construct(
protected string $name,
protected int $age
) {}
public function introduce(): string
{
return "I'm {$this->name}, {$this->age} years old";
}
}
// Child adds more promoted properties
class Employee extends Person
{
public function __construct(
string $name,
int $age,
protected string $employeeId,
protected float $salary
) {
parent::__construct($name, $age);
}
public function introduce(): string
{
return parent::introduce() . " and work here as #{$this->employeeId}";
}
public function getSalary(): float
{
return $this->salary;
}
}
// Usage
$employee = new Employee("Alice", 30, "EMP001", 75000);
echo $employee->introduce(); // "I'm Alice, 30 years old and work here as #EMP001"
echo $employee->getSalary(); // 75000

::: tip Best Practice When using constructor promotion with inheritance:

  1. Parent properties - Use promoted properties in the parent for clean code
  2. Child-specific - Add child-specific promoted properties in child constructor
  3. Call parent first - Always call parent::__construct() at the beginning
  4. Keep it simple - Don’t mix promoted and non-promoted properties unnecessarily :::

Master abstract classes and methods in PHP.

::: code-group

<?php
declare(strict_types=1);
abstract class Shape
{
public function __construct(
protected string $color
) {}
// Abstract methods (must be implemented by subclasses)
abstract public function calculateArea(): float;
abstract public function calculatePerimeter(): float;
// Concrete method (inherited by all subclasses)
public function getColor(): string
{
return $this->color;
}
public function describe(): string
{
return "A {$this->color} " . static::class . " with area " .
number_format($this->calculateArea(), 2);
}
}
class Circle extends Shape
{
public function __construct(
string $color,
private float $radius
) {
parent::__construct($color);
}
public function calculateArea(): float
{
return M_PI * $this->radius ** 2;
}
public function calculatePerimeter(): float
{
return 2 * M_PI * $this->radius;
}
public function getRadius(): float
{
return $this->radius;
}
}
class Rectangle extends Shape
{
public function __construct(
string $color,
private float $width,
private float $height
) {
parent::__construct($color);
}
public function calculateArea(): float
{
return $this->width * $this->height;
}
public function calculatePerimeter(): float
{
return 2 * ($this->width + $this->height);
}
}
// Cannot instantiate abstract class
// $shape = new Shape("red"); // Error!
// Create concrete instances
$circle = new Circle("red", 5);
echo $circle->describe() . "\n";
// "A red Circle with area 78.54"
$rectangle = new Rectangle("blue", 4, 6);
echo $rectangle->describe() . "\n";
// "A blue Rectangle with area 24.00"
// Polymorphism
function printShapeInfo(Shape $shape): void
{
echo "Color: {$shape->getColor()}\n";
echo "Area: {$shape->calculateArea()}\n";
echo "Perimeter: {$shape->calculatePerimeter()}\n";
}
printShapeInfo($circle);
printShapeInfo($rectangle);
abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// Abstract methods
public abstract double calculateArea();
public abstract double calculatePerimeter();
// Concrete method
public String getColor() {
return color;
}
public String describe() {
return "A " + color + " " + this.getClass().getSimpleName() +
" with area " + String.format("%.2f", calculateArea());
}
}
class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
@Override
public double calculatePerimeter() {
return 2 * Math.PI * radius;
}
}
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
@Override
public double calculatePerimeter() {
return 2 * (width + height);
}
}
// Usage
Circle circle = new Circle("red", 5);
Rectangle rectangle = new Rectangle("blue", 4, 6);
// Polymorphism
void printShapeInfo(Shape shape) {
System.out.println("Color: " + shape.getColor());
System.out.println("Area: " + shape.calculateArea());
System.out.println("Perimeter: " + shape.calculatePerimeter());
}

:::

RulePHPJava
Cannot instantiate✅ Same✅ Same
Can have concrete methods✅ Yes✅ Yes
Can have abstract methods✅ Yes✅ Yes
Abstract methods in subclassMust implementMust implement
Can have constructors✅ Yes✅ Yes
Can have properties✅ Yes✅ Yes
Multiple inheritance❌ No❌ No

::: tip When to Use Abstract Classes Use abstract classes when:

  • You want to provide common implementation for subclasses
  • You need to enforce a contract (abstract methods)
  • Subclasses share state (properties)
  • You want to use protected members
  • You’re modeling an “is-a” relationship

Abstract class vs Interface (covered in Chapter 5):

  • Abstract class: Provides implementation + contract
  • Interface: Only defines contract (no implementation in PHP < 8.0) :::

Understand method overriding rules and visibility in PHP.

<?php
declare(strict_types=1);
class ParentClass
{
public function publicMethod(): string
{
return "Parent public method";
}
protected function protectedMethod(): string
{
return "Parent protected method";
}
private function privateMethod(): string
{
return "Parent private method";
}
}
class ChildClass extends ParentClass
{
// ✅ Can override public as public
public function publicMethod(): string
{
return "Child public method";
}
// ✅ Can override protected as protected or public
public function protectedMethod(): string
{
return "Child protected method (now public)";
}
// ✅ Private methods are NOT inherited, so this is a new method
private function privateMethod(): string
{
return "Child private method (not an override)";
}
// ❌ Cannot reduce visibility
// protected function publicMethod(): string { } // Error!
}
Parent VisibilityChild Can Use
publicpublic only
protectedprotected or public
privateN/A (not inherited)

::: warning Visibility Rules You can make methods MORE visible (protected → public) but NOT less visible (public → protected/private).

This is the same in Java! :::

PHP 7.4+ (including PHP 8.4) enforces return type compatibility (covariance):

<?php
declare(strict_types=1);
class Animal {}
class Dog extends Animal {}
class AnimalFactory
{
public function create(): Animal
{
return new Animal();
}
}
class DogFactory extends AnimalFactory
{
// ✅ Covariant return type (PHP 7.4+, PHP 8.4 compatible)
public function create(): Dog
{
return new Dog();
}
}
// This works because Dog is a subtype of Animal
$factory = new DogFactory();
$animal = $factory->create(); // Returns Dog, which is an Animal

Learn how to prevent inheritance and method overriding.

::: code-group

<?php
declare(strict_types=1);
// Final class - cannot be extended
final class ImmutableValue
{
public function __construct(
private readonly mixed $value
) {}
public function getValue(): mixed
{
return $this->value;
}
}
// Error: Cannot extend final class
// class ExtendedValue extends ImmutableValue {}
class BaseService
{
// Final method - cannot be overridden
final public function authenticate(string $token): bool
{
// Critical authentication logic
return hash('sha256', $token) === $this->getExpectedHash();
}
protected function getExpectedHash(): string
{
return 'expected-hash';
}
// Regular method - can be overridden
public function process(): void
{
echo "Base processing\n";
}
}
class UserService extends BaseService
{
// Error: Cannot override final method
// public function authenticate(string $token): bool {}
// ✅ Can override non-final method
public function process(): void
{
echo "User processing\n";
}
}
// Final class - cannot be extended
final class ImmutableValue {
private final Object value;
public ImmutableValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
// Error: Cannot extend final class
// class ExtendedValue extends ImmutableValue {}
class BaseService {
// Final method - cannot be overridden
final public boolean authenticate(String token) {
// Critical authentication logic
return token.hashCode() == getExpectedHash();
}
protected int getExpectedHash() {
return 12345;
}
// Regular method - can be overridden
public void process() {
System.out.println("Base processing");
}
}
class UserService extends BaseService {
// Error: Cannot override final method
// public boolean authenticate(String token) {}
// Can override non-final method
@Override
public void process() {
System.out.println("User processing");
}
}

:::

::: tip When to Use Final Use final to:

  • Prevent inheritance: When a class shouldn’t be subclassed (e.g., utility classes)
  • Prevent overriding: When a method is critical and shouldn’t be modified (e.g., security, core logic)
  • Optimization: Final classes/methods can be optimized by the runtime

Don’t overuse: Only use when there’s a clear reason. Excessive use makes code less flexible. :::


Understand the difference between self::, parent::, and static::.

This is a PHP-specific feature with no direct Java equivalent:

<?php
declare(strict_types=1);
class BaseModel
{
protected static string $tableName = 'base_table';
// Using self:: (early binding)
public static function getTableWithSelf(): string
{
return self::$tableName; // Always refers to BaseModel::$tableName
}
// Using static:: (late binding)
public static function getTableWithStatic(): string
{
return static::$tableName; // Refers to the called class's $tableName
}
public static function createSelf(): static
{
return new self(); // Always creates BaseModel
}
public static function createStatic(): static
{
return new static(); // Creates instance of called class
}
}
class UserModel extends BaseModel
{
protected static string $tableName = 'users';
}
class ProductModel extends BaseModel
{
protected static string $tableName = 'products';
}
// Early binding (self::)
echo BaseModel::getTableWithSelf(); // "base_table"
echo UserModel::getTableWithSelf(); // "base_table" (refers to parent!)
echo ProductModel::getTableWithSelf(); // "base_table" (refers to parent!)
// Late static binding (static::)
echo BaseModel::getTableWithStatic(); // "base_table"
echo UserModel::getTableWithStatic(); // "users" (correct!)
echo ProductModel::getTableWithStatic(); // "products" (correct!)
// Object creation
$base = BaseModel::createSelf(); // BaseModel instance
$user = UserModel::createSelf(); // BaseModel instance (wrong!)
$userCorrect = UserModel::createStatic(); // UserModel instance (correct!)
KeywordBindingUse Case
self::Early (compile-time)When you specifically want the defining class
static::Late (runtime)When you want the called class (polymorphic behavior)
parent::Parent classWhen you want to call parent’s implementation

::: tip Late Static Binding Use Cases Use static:: for:

  • Factory methods: return new static() creates instance of called class
  • Polymorphic static methods: Different behavior per subclass
  • Active Record pattern: User::find(), Product::find(), etc.

Most of the time, use static:: for static methods in inheritance hierarchies. :::

<?php
declare(strict_types=1);
abstract class Repository
{
protected static string $table;
protected static string $primaryKey = 'id';
public static function find(int $id): ?static
{
// Late binding: uses child class's $table
$table = static::$table;
$pk = static::$primaryKey;
// Simulate database query
echo "SELECT * FROM {$table} WHERE {$pk} = {$id}\n";
// Return instance of called class
return new static();
}
public static function all(): array
{
$table = static::$table;
echo "SELECT * FROM {$table}\n";
return [];
}
}
class UserRepository extends Repository
{
protected static string $table = 'users';
}
class ProductRepository extends Repository
{
protected static string $table = 'products';
}
// Each class uses its own table name
$user = UserRepository::find(1); // SELECT * FROM users WHERE id = 1
$product = ProductRepository::find(5); // SELECT * FROM products WHERE id = 5
UserRepository::all(); // SELECT * FROM users
ProductRepository::all(); // SELECT * FROM products

Apply polymorphism effectively in PHP.

<?php
declare(strict_types=1);
abstract class PaymentMethod
{
public function __construct(
protected float $amount
) {}
abstract public function process(): bool;
abstract public function getTransactionFee(): float;
public function getTotalAmount(): float
{
return $this->amount + $this->getTransactionFee();
}
}
class CreditCardPayment extends PaymentMethod
{
public function __construct(
float $amount,
private string $cardNumber,
private string $cvv
) {
parent::__construct($amount);
}
public function process(): bool
{
echo "Processing credit card payment: \${$this->amount}\n";
echo "Card: ****" . substr($this->cardNumber, -4) . "\n";
return true;
}
public function getTransactionFee(): float
{
return $this->amount * 0.029 + 0.30; // 2.9% + $0.30
}
}
class PayPalPayment extends PaymentMethod
{
public function __construct(
float $amount,
private string $email
) {
parent::__construct($amount);
}
public function process(): bool
{
echo "Processing PayPal payment: \${$this->amount}\n";
echo "PayPal account: {$this->email}\n";
return true;
}
public function getTransactionFee(): float
{
return $this->amount * 0.034 + 0.30; // 3.4% + $0.30
}
}
class BitcoinPayment extends PaymentMethod
{
public function __construct(
float $amount,
private string $walletAddress
) {
parent::__construct($amount);
}
public function process(): bool
{
echo "Processing Bitcoin payment: \${$this->amount}\n";
echo "Wallet: {$this->walletAddress}\n";
return true;
}
public function getTransactionFee(): float
{
return 1.00; // Fixed fee
}
}
// Polymorphic function - accepts any PaymentMethod
function processPayment(PaymentMethod $payment): void
{
echo "\n=== Processing Payment ===\n";
echo "Amount: \${$payment->getTotalAmount()}\n";
echo "Fee: \${$payment->getTransactionFee()}\n";
if ($payment->process()) {
echo "Payment successful!\n";
} else {
echo "Payment failed!\n";
}
}
// All payment types can be processed polymorphically
$payments = [
new CreditCardPayment(100, "4532123456789012", "123"),
new PayPalPayment(100, "user@example.com"),
new BitcoinPayment(100, "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")
];
foreach ($payments as $payment) {
processPayment($payment);
}
<?php
declare(strict_types=1);
function handlePayment(PaymentMethod $payment): void
{
// Type checking
if ($payment instanceof CreditCardPayment) {
echo "Processing credit card...\n";
// Can access CreditCardPayment-specific methods
} elseif ($payment instanceof PayPalPayment) {
echo "Processing PayPal...\n";
} elseif ($payment instanceof BitcoinPayment) {
echo "Processing Bitcoin...\n";
}
// Process regardless of type
$payment->process();
}
// Check against parent class
$cc = new CreditCardPayment(50, "4532123456789012", "123");
var_dump($cc instanceof CreditCardPayment); // true
var_dump($cc instanceof PaymentMethod); // true (inheritance)
var_dump($cc instanceof PayPalPayment); // false

Section 7: Liskov Substitution Principle (LSP)

Section titled “Section 7: Liskov Substitution Principle (LSP)”

Understand the Liskov Substitution Principle and how to apply it in PHP.

The Liskov Substitution Principle states: “Objects of a superclass should be replaceable with objects of a subclass without breaking the application.”

In simpler terms: If class B extends class A, you should be able to use B anywhere you use A without unexpected behavior.

<?php
declare(strict_types=1);
// ❌ BAD: Violates LSP
class Rectangle
{
protected float $width;
protected float $height;
public function setWidth(float $width): void
{
$this->width = $width;
}
public function setHeight(float $height): void
{
$this->height = $height;
}
public function getArea(): float
{
return $this->width * $this->height;
}
}
class Square extends Rectangle
{
// Problem: Square changes behavior of Rectangle
public function setWidth(float $width): void
{
$this->width = $width;
$this->height = $width; // Square must have equal sides
}
public function setHeight(float $height): void
{
$this->width = $height;
$this->height = $height; // Square must have equal sides
}
}
// This function works with Rectangle
function resizeRectangle(Rectangle $rect): void
{
$rect->setWidth(5);
$rect->setHeight(10);
// Expects area = 50
}
$rectangle = new Rectangle();
resizeRectangle($rectangle);
echo $rectangle->getArea(); // 50 ✅ correct
$square = new Square();
resizeRectangle($square);
echo $square->getArea(); // 100 ❌ unexpected! (should be 50)
// LSP violated: Square can't be used where Rectangle is expected
<?php
declare(strict_types=1);
// ✅ GOOD: LSP compliant
interface Shape
{
public function getArea(): float;
public function getPerimeter(): float;
}
class Rectangle implements Shape
{
public function __construct(
private float $width,
private float $height
) {}
public function setWidth(float $width): void
{
$this->width = $width;
}
public function setHeight(float $height): void
{
$this->height = $height;
}
public function getArea(): float
{
return $this->width * $this->height;
}
public function getPerimeter(): float
{
return 2 * ($this->width + $this->height);
}
}
class Square implements Shape
{
public function __construct(
private float $side
) {}
public function setSide(float $side): void
{
$this->side = $side;
}
public function getArea(): float
{
return $this->side ** 2;
}
public function getPerimeter(): float
{
return 4 * $this->side;
}
}
// Now both work independently without inheritance issues
function printArea(Shape $shape): void
{
echo "Area: {$shape->getArea()}\n";
}
printArea(new Rectangle(5, 10)); // 50
printArea(new Square(7)); // 49

::: tip Follow LSP Do:

  • Subclass should fulfill parent’s contract
  • Preserve expected behavior
  • Don’t strengthen preconditions (input requirements)
  • Don’t weaken postconditions (output guarantees)
  • Subclass should be truly “is-a” relationship

Don’t:

  • Change expected behavior
  • Throw new exceptions not in parent
  • Require more than parent requires
  • Promise less than parent promises :::

Learn the Template Method pattern using abstract classes.

The Template Method pattern defines the skeleton of an algorithm in a base class, letting subclasses override specific steps:

<?php
declare(strict_types=1);
abstract class DataImporter
{
// Template method - defines the algorithm structure
final public function import(string $source): void
{
echo "Starting import from: $source\n";
$this->validateSource($source);
$data = $this->readData($source);
$processed = $this->processData($data);
$this->saveData($processed);
$this->sendNotification();
echo "Import complete!\n";
}
// Hook method - can be overridden but has default
protected function validateSource(string $source): void
{
if (empty($source)) {
throw new InvalidArgumentException("Source cannot be empty");
}
}
// Abstract steps - must be implemented
abstract protected function readData(string $source): array;
abstract protected function processData(array $data): array;
abstract protected function saveData(array $data): void;
// Hook method with default implementation
protected function sendNotification(): void
{
echo "Sending default notification\n";
}
}
class CSVImporter extends DataImporter
{
protected function readData(string $source): array
{
echo "Reading CSV from $source\n";
// Simulate CSV reading
return [
['name' => 'Alice', 'email' => 'alice@example.com'],
['name' => 'Bob', 'email' => 'bob@example.com']
];
}
protected function processData(array $data): array
{
echo "Processing CSV data\n";
// Transform data
return array_map(function($row) {
return [
'name' => strtoupper($row['name']),
'email' => strtolower($row['email'])
];
}, $data);
}
protected function saveData(array $data): void
{
echo "Saving " . count($data) . " records to database\n";
// Simulate save
}
}
class JSONImporter extends DataImporter
{
protected function readData(string $source): array
{
echo "Reading JSON from $source\n";
// Simulate JSON reading
return json_decode('[{"name":"Charlie","email":"charlie@example.com"}]', true);
}
protected function processData(array $data): array
{
echo "Processing JSON data\n";
return $data; // No transformation needed
}
protected function saveData(array $data): void
{
echo "Saving " . count($data) . " JSON records\n";
}
// Override hook to customize behavior
protected function sendNotification(): void
{
echo "Sending custom JSON import notification via Slack\n";
}
}
class XMLImporter extends DataImporter
{
// More strict validation
protected function validateSource(string $source): void
{
parent::validateSource($source);
if (!str_ends_with($source, '.xml')) {
throw new InvalidArgumentException("Source must be an XML file");
}
}
protected function readData(string $source): array
{
echo "Reading XML from $source\n";
return [['name' => 'Diana', 'email' => 'diana@example.com']];
}
protected function processData(array $data): array
{
echo "Processing XML data\n";
return $data;
}
protected function saveData(array $data): void
{
echo "Saving XML data\n";
}
}
// Usage
$csvImporter = new CSVImporter();
$csvImporter->import("users.csv");
echo "\n";
$jsonImporter = new JSONImporter();
$jsonImporter->import("users.json");
echo "\n";
$xmlImporter = new XMLImporter();
$xmlImporter->import("users.xml");

::: tip Template Method Benefits Advantages:

  • Code reuse: Common algorithm in base class
  • Flexibility: Subclasses customize specific steps
  • Control: Base class controls the algorithm flow
  • Extension points: Clear places for customization

When to use:

  • Multiple classes with similar algorithms
  • Want to avoid code duplication
  • Need controlled extension points
  • Algorithm steps are well-defined :::

Understand when to use composition instead of inheritance.

<?php
declare(strict_types=1);
// ❌ BAD: Deep inheritance hierarchy
class Animal {
public function eat(): void {
echo "Eating...\n";
}
}
class Mammal extends Animal {
public function breathe(): void {
echo "Breathing...\n";
}
}
class Dog extends Mammal {
public function bark(): void {
echo "Woof!\n";
}
}
class Bird extends Animal {
public function fly(): void {
echo "Flying...\n";
}
}
// Problem: What about a Penguin? It's a bird that can't fly!
// Problem: What about a Bat? It's a mammal that can fly!
// Inheritance becomes problematic
<?php
declare(strict_types=1);
// ✅ GOOD: Use composition
interface Eatable {
public function eat(): void;
}
interface Swimmable {
public function swim(): void;
}
interface Flyable {
public function fly(): void;
}
class Dog implements Eatable, Swimmable
{
public function eat(): void {
echo "Dog is eating\n";
}
public function swim(): void {
echo "Dog is swimming\n";
}
public function bark(): void {
echo "Woof!\n";
}
}
class Penguin implements Eatable, Swimmable
{
public function eat(): void {
echo "Penguin is eating fish\n";
}
public function swim(): void {
echo "Penguin is swimming\n";
}
// Note: No fly method - penguins can't fly!
}
class Eagle implements Eatable, Flyable
{
public function eat(): void {
echo "Eagle is eating\n";
}
public function fly(): void {
echo "Eagle is flying high\n";
}
}
class Bat implements Eatable, Flyable
{
public function eat(): void {
echo "Bat is eating insects\n";
}
public function fly(): void {
echo "Bat is flying at night\n";
}
}

Real-World Example: Logger with Composition

Section titled “Real-World Example: Logger with Composition”
<?php
declare(strict_types=1);
// Instead of inheritance hierarchy, use composition
interface LogWriter
{
public function write(string $message): void;
}
class FileLogWriter implements LogWriter
{
public function __construct(
private string $filename
) {}
public function write(string $message): void
{
echo "Writing to file {$this->filename}: $message\n";
}
}
class DatabaseLogWriter implements LogWriter
{
public function write(string $message): void
{
echo "Writing to database: $message\n";
}
}
class CloudLogWriter implements LogWriter
{
public function write(string $message): void
{
echo "Sending to cloud: $message\n";
}
}
// Logger uses composition, not inheritance
class Logger
{
/** @var LogWriter[] */
private array $writers = [];
public function addWriter(LogWriter $writer): void
{
$this->writers[] = $writer;
}
public function log(string $level, string $message): void
{
$formatted = "[" . date('Y-m-d H:i:s') . "] [$level] $message";
foreach ($this->writers as $writer) {
$writer->write($formatted);
}
}
public function info(string $message): void
{
$this->log('INFO', $message);
}
public function error(string $message): void
{
$this->log('ERROR', $message);
}
}
// Usage: Flexible composition
$logger = new Logger();
$logger->addWriter(new FileLogWriter('/var/log/app.log'));
$logger->addWriter(new DatabaseLogWriter());
$logger->addWriter(new CloudLogWriter());
$logger->info("Application started");
$logger->error("Something went wrong");
// Can easily change behavior at runtime
$logger2 = new Logger();
$logger2->addWriter(new FileLogWriter('/var/log/debug.log'));
$logger2->info("Debug mode");
Use Inheritance WhenUse Composition When
True “is-a” relationship”has-a” or “uses-a” relationship
Subclass is a specialized versionNeed flexible behavior changes
Shared implementation neededWant to combine multiple behaviors
Polymorphism is essentialPrefer runtime flexibility
Hierarchy is stableRequirements may change

::: tip Favor Composition Over Inheritance “Favor composition over inheritance” is a famous design principle because:

  1. Flexibility: Easy to change behavior at runtime
  2. Less coupling: Components are independent
  3. Easier testing: Mock individual components
  4. Avoid fragile base class: Changes to parent don’t break children
  5. Multiple behaviors: Combine features from multiple sources

Use inheritance when:

  • True “is-a” relationship exists
  • Polymorphism is needed
  • Shared behavior is core to the abstraction :::

Section 10: Practical Example - Employee System

Section titled “Section 10: Practical Example - Employee System”

Build a complete employee management system using inheritance.

<?php
declare(strict_types=1);
abstract class Employee
{
private static int $nextId = 1;
protected int $id;
public function __construct(
protected string $name,
protected string $email,
protected float $baseSalary
) {
$this->id = self::$nextId++;
}
abstract public function calculateSalary(): float;
abstract public function getRole(): string;
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function getEmail(): string
{
return $this->email;
}
public function getBaseSalary(): float
{
return $this->baseSalary;
}
public function getDetails(): string
{
return sprintf(
"ID: %d | Name: %s | Role: %s | Salary: $%.2f",
$this->id,
$this->name,
$this->getRole(),
$this->calculateSalary()
);
}
}
class FullTimeEmployee extends Employee
{
public function __construct(
string $name,
string $email,
float $baseSalary,
private float $bonus = 0
) {
parent::__construct($name, $email, $baseSalary);
}
public function calculateSalary(): float
{
return $this->baseSalary + $this->bonus;
}
public function getRole(): string
{
return "Full-Time Employee";
}
public function setBonus(float $bonus): void
{
$this->bonus = $bonus;
}
}
class ContractEmployee extends Employee
{
public function __construct(
string $name,
string $email,
private float $hourlyRate,
private int $hoursWorked
) {
parent::__construct($name, $email, 0);
}
public function calculateSalary(): float
{
return $this->hourlyRate * $this->hoursWorked;
}
public function getRole(): string
{
return "Contract Employee";
}
public function addHours(int $hours): void
{
$this->hoursWorked += $hours;
}
}
class Manager extends FullTimeEmployee
{
public function __construct(
string $name,
string $email,
float $baseSalary,
float $bonus,
private int $teamSize
) {
parent::__construct($name, $email, $baseSalary, $bonus);
}
public function getRole(): string
{
return "Manager (Team of {$this->teamSize})";
}
// Manager gets additional bonus based on team size
public function calculateSalary(): float
{
$teamBonus = $this->teamSize * 500;
return parent::calculateSalary() + $teamBonus;
}
}
// Company class to manage employees
class Company
{
/** @var Employee[] */
private array $employees = [];
public function hire(Employee $employee): void
{
$this->employees[] = $employee;
echo "Hired: {$employee->getName()} as {$employee->getRole()}\n";
}
public function calculateTotalPayroll(): float
{
$total = 0;
foreach ($this->employees as $employee) {
$total += $employee->calculateSalary();
}
return $total;
}
public function listEmployees(): void
{
echo "\n=== Employee List ===\n";
foreach ($this->employees as $employee) {
echo $employee->getDetails() . "\n";
}
echo "\nTotal Payroll: $" . number_format($this->calculateTotalPayroll(), 2) . "\n";
}
public function getEmployeesByType(string $className): array
{
return array_filter(
$this->employees,
fn($e) => $e instanceof $className
);
}
}
// Usage
$company = new Company();
$company->hire(new FullTimeEmployee("Alice Johnson", "alice@company.com", 75000, 5000));
$company->hire(new FullTimeEmployee("Bob Smith", "bob@company.com", 65000, 3000));
$company->hire(new ContractEmployee("Charlie Brown", "charlie@contractor.com", 50, 160));
$company->hire(new Manager("Diana Prince", "diana@company.com", 95000, 10000, 5));
$company->listEmployees();
// Get specific employee types
$managers = $company->getEmployeesByType(Manager::class);
echo "\nManagers: " . count($managers) . "\n";

Explore advanced inheritance concepts and runtime introspection in PHP.

PHP provides powerful functions to inspect inheritance relationships at runtime:

<?php
declare(strict_types=1);
class Vehicle {
protected string $model;
public function __construct(string $model) {
$this->model = $model;
}
}
class Car extends Vehicle {
private int $doors;
public function __construct(string $model, int $doors) {
parent::__construct($model);
$this->doors = $doors;
}
}
class SportsCar extends Car {
private int $horsepower;
public function __construct(string $model, int $doors, int $horsepower) {
parent::__construct($model, $doors);
$this->horsepower = $horsepower;
}
}
// Runtime inspection
$sportsCar = new SportsCar("Ferrari", 2, 650);
echo get_class($sportsCar); // "SportsCar"
echo get_parent_class($sportsCar); // "Car"
echo get_parent_class(\Car::class); // "Vehicle"
echo get_parent_class(\Vehicle::class); // false (no parent)
// Check if a class is a subclass
if (is_subclass_of($sportsCar, \Vehicle::class)) {
echo "SportsCar is a subclass of Vehicle";
}
// Get all interfaces implemented
$interfaces = class_implements($sportsCar);
print_r($interfaces);
// Reflection for advanced inspection
$reflection = new \ReflectionClass($sportsCar);
echo "Parent: " . $reflection->getParentClass()->getName() . "\n";
echo "All methods: " . implode(", ", array_map(fn($m) => $m->getName(), $reflection->getMethods()));

When cloning objects in inheritance hierarchies, you need to handle nested objects properly:

<?php
declare(strict_types=1);
class Engine {
public int $horsepower;
public string $type;
public function __construct(int $horsepower, string $type) {
$this->horsepower = $horsepower;
$this->type = $type;
}
}
class Vehicle {
protected string $model;
protected Engine $engine;
public function __construct(string $model, Engine $engine) {
$this->model = $model;
$this->engine = $engine;
}
public function __clone(): void {
// Deep clone the engine to prevent shared references
$this->engine = clone $this->engine;
}
}
class Car extends Vehicle {
private int $doors;
public function __construct(string $model, Engine $engine, int $doors) {
parent::__construct($model, $engine);
$this->doors = $doors;
}
public function __clone(): void {
parent::__clone(); // Don't forget to call parent
// Add custom cloning if needed
}
}
$original = new Car("BMW", new Engine(250, "V6"), 4);
$clone = clone $original;
// Modify cloned object
$clone->engine->horsepower = 300;
// Original remains untouched thanks to __clone()
echo $original->engine->horsepower; // 250

Design Decision: Abstract Classes vs Interfaces

Section titled “Design Decision: Abstract Classes vs Interfaces”

Here’s a systematic approach for choosing between abstract classes and interfaces:

<?php
declare(strict_types=1);
// Use Abstract Class when:
// 1. You have shared implementation
// 2. Classes have an "is-a" relationship
// 3. You need protected members
// 4. You want to share state
abstract class AbstractVehicle {
protected string $licensePlate;
public function __construct(string $licensePlate) {
$this->licensePlate = $licensePlate;
}
// Shared implementation
protected function getFormattedLicense(): string {
return "License: " . $this->licensePlate;
}
abstract public function startEngine(): void;
}
// Use Interface when:
// 1. You only define contracts
// 2. Classes can do something (capabilities)
// 3. Multiple inheritance needed
interface Drivable {
public function drive(float $distance): float;
}
interface Parkingable {
public function park(): void;
}
class ElectricCar extends AbstractVehicle implements Drivable, Parkingable {
private float $batteryLevel;
public function __construct(string $licensePlate, float $batteryLevel = 100.0) {
parent::__construct($licensePlate);
$this->batteryLevel = $batteryLevel;
}
public function startEngine(): void {
echo "Electric engine starting silently...";
}
public function drive(float $distance): float {
$used = $distance * 0.5; // kWh per km
$this->batteryLevel -= $used;
return $this->batteryLevel;
}
public function park(): void {
echo "Electric car parking and charging...";
}
}

Here’s how inheritance affects object serialization:

<?php
declare(strict_types=1);
class Person {
public string $name;
public int $age;
public function __construct(string $name, int $age) {
$this->name = $name;
$this->age = $age;
}
public function __serialize(): array {
return ['name' => $this->name, 'age' => $this->age];
}
public function __unserialize(array $data): void {
$this->name = $data['name'];
$this->age = $data['age'];
}
}
class Employee extends Person {
private string $employeeId;
protected float $salary;
public function __construct(string $name, int $age, string $employeeId, float $salary) {
parent::__construct($name, $age);
$this->employeeId = $employeeId;
$this->salary = $salary;
}
public function __serialize(): array {
return array_merge(parent::__serialize(), [
'employeeId' => $this->employeeId,
'salary' => $this->salary
]);
}
public function __unserialize(array $data): void {
parent::__unserialize($data);
$this->employeeId = $data['employeeId'];
$this->salary = $data['salary'];
}
}
$employee = new Employee("Alice", 30, "EMP123", 75000);
$serialized = serialize($employee);
$unserialized = unserialize($serialized);
echo "Unserialized: " . $unserialized->name . " - " . $unserialized->employeeId;

::: tip Inheritance Design Guide Use Abstract Class when:

  • You have shared implementation code
  • Classes have a true “is-a” relationship
  • You need protected members or state sharing
  • You want to use final methods for critical logic

Use Interface when:

  • You need multiple inheritance
  • Classes only need contracts (no implementation)
  • You want to define capabilities (“can-do”)
  • You need maximum flexibility

Use Traits (Chapter 5) when:

  • You need code reuse without inheritance
  • You want horizontal composition
  • Single inheritance limitation is a problem :::

Create a vehicle hierarchy with proper inheritance.

Requirements:

  • Abstract Vehicle base class
  • Car, Motorcycle, and Truck subclasses
  • Abstract getFuelEfficiency() method
  • Calculate trip cost based on distance and fuel price
Solution
<?php
declare(strict_types=1);
abstract class Vehicle
{
public function __construct(
protected string $make,
protected string $model,
protected int $year
) {}
abstract public function getFuelEfficiency(): float; // mpg
public function calculateTripCost(float $distance, float $fuelPrice): float
{
$gallonsNeeded = $distance / $this->getFuelEfficiency();
return $gallonsNeeded * $fuelPrice;
}
public function getInfo(): string
{
return "{$this->year} {$this->make} {$this->model}";
}
}
class Car extends Vehicle
{
public function __construct(
string $make,
string $model,
int $year,
private int $doors
) {
parent::__construct($make, $model, $year);
}
public function getFuelEfficiency(): float
{
return 30.0; // 30 mpg
}
}
class Motorcycle extends Vehicle
{
public function getFuelEfficiency(): float
{
return 50.0; // 50 mpg
}
}
class Truck extends Vehicle
{
public function __construct(
string $make,
string $model,
int $year,
private float $cargoCapacity
) {
parent::__construct($make, $model, $year);
}
public function getFuelEfficiency(): float
{
return 18.0; // 18 mpg
}
}
// Test
$vehicles = [
new Car("Toyota", "Camry", 2024, 4),
new Motorcycle("Harley Davidson", "Street 750", 2024),
new Truck("Ford", "F-150", 2024, 2000)
];
$distance = 300; // miles
$fuelPrice = 3.50; // per gallon
foreach ($vehicles as $vehicle) {
echo $vehicle->getInfo() . "\n";
echo "Fuel efficiency: {$vehicle->getFuelEfficiency()} mpg\n";
echo "Trip cost: $" . number_format($vehicle->calculateTripCost($distance, $fuelPrice), 2) . "\n\n";
}

Build a notification system with multiple delivery methods.

Requirements:

  • Abstract Notification class
  • EmailNotification, SMSNotification, PushNotification subclasses
  • send() method
  • Track notification delivery status
Solution
<?php
declare(strict_types=1);
abstract class Notification
{
protected bool $sent = false;
protected ?string $sentAt = null;
public function __construct(
protected string $recipient,
protected string $message
) {}
abstract protected function deliver(): bool;
final public function send(): bool
{
if ($this->sent) {
throw new RuntimeException("Notification already sent");
}
if ($this->deliver()) {
$this->sent = true;
$this->sentAt = date('Y-m-d H:i:s');
return true;
}
return false;
}
public function isSent(): bool
{
return $this->sent;
}
public function getSentAt(): ?string
{
return $this->sentAt;
}
}
class EmailNotification extends Notification
{
public function __construct(
string $recipient,
string $message,
private string $subject
) {
parent::__construct($recipient, $message);
}
protected function deliver(): bool
{
echo "Sending email to {$this->recipient}\n";
echo "Subject: {$this->subject}\n";
echo "Message: {$this->message}\n";
return true;
}
}
class SMSNotification extends Notification
{
protected function deliver(): bool
{
echo "Sending SMS to {$this->recipient}\n";
echo "Message: {$this->message}\n";
return true;
}
}
class PushNotification extends Notification
{
public function __construct(
string $recipient,
string $message,
private string $deviceToken
) {
parent::__construct($recipient, $message);
}
protected function deliver(): bool
{
echo "Sending push notification to device {$this->deviceToken}\n";
echo "Message: {$this->message}\n";
return true;
}
}
// Test
$notifications = [
new EmailNotification("user@example.com", "Welcome!", "Welcome to our service"),
new SMSNotification("+1234567890", "Your code is: 123456"),
new PushNotification("user123", "New message received", "device-token-xyz")
];
foreach ($notifications as $notification) {
$notification->send();
echo "Sent at: {$notification->getSentAt()}\n\n";
}

Build a tool to inspect inheritance relationships at runtime.

Requirements:

  • Use get_parent_class(), is_subclass_of(), and reflection
  • Display class hierarchy tree (parent → child → grandchild)
  • Show implemented interfaces
  • Compare regular clone vs deep clone behavior
Solution
<?php
declare(strict_types=1);
class InheritanceInspector {
public static function inspectClass(string $className): array {
$reflection = new \ReflectionClass($className);
return [
'class' => $className,
'parent' => $reflection->getParentClass()?->getName(),
'interfaces' => class_implements($className) ?: [],
'methods' => array_map(fn($m) => $m->getName(), $reflection->getMethods()),
'properties' => array_keys($reflection->getDefaultProperties())
];
}
public static function showHierarchy(string $className, int $indent = 0): void {
$spaces = str_repeat(" ", $indent);
echo $spaces . "├── " . $className . "\n";
if ($parent = \get_parent_class($className)) {
self::showHierarchy($parent, $indent + 1);
}
}
}
// Test classes
class Vehicle {
public string $type;
public function __construct(string $type) { $this->type = $type; }
}
class Engine {
public int $horsepower;
public function __construct(int $horsepower) { $this->horsepower = $horsepower; }
public function __clone(): void {
echo "Engine cloned\n";
}
}
class Car extends Vehicle {
private Engine $engine;
public function __construct(string $type, Engine $engine) {
parent::__construct($type);
$this->engine = $engine;
}
public function __clone(): void {
echo "Deep cloning Car...\n";
$this->engine = clone $this->engine;
}
}
// Test the inspector
$info = InheritanceInspector::inspectClass(Car::class);
print_r($info);
InheritanceInspector::showHierarchy(Car::class);
// Test cloning
$original = new Car("Sedan", new Engine(200));
$clone = clone $original;

Before moving to the next chapter, ensure you can:

  • Use the extends keyword to create class hierarchies
  • Call parent constructors with parent::__construct()
  • Create abstract classes and implement abstract methods
  • Override methods with proper visibility rules
  • Use final to prevent inheritance/overriding
  • Understand self:: vs static:: vs parent::
  • Apply polymorphism with type hinting
  • Use instanceof for type checking
  • Build class hierarchies that model real-world relationships

::: tip Ready for More? In Chapter 5: Interfaces & Traits, we’ll explore interfaces (similar to Java) and traits (a PHP-specific feature for code reuse). :::


PHP Documentation: