Lesson 4.3: Inheritance and Polymorphism

Inheritance is one of the most powerful concepts in object-oriented programming. Think of it like a family tree—children inherit traits from their parents, but they can also develop their own unique characteristics. In programming, a child class inherits properties and methods from a parent class while adding its own specialized features.

Imagine you're building an employee management system. All employees share common traits—they have names, employee IDs, and salaries. But different types of employees have specialized behaviors: managers can approve budgets, developers can commit code, and salespeople can access customer data. Instead of duplicating common employee code, inheritance lets you create a base Employee class and then specialized classes that inherit and extend that functionality.

Polymorphism is the ability for different classes to respond to the same method call in their own unique way. It's like asking different animals to "make a sound"—a dog barks, a cat meows, and a cow moos. They all respond to the same request, but each responds according to their nature. This concept enables incredibly flexible and maintainable code.

Modern PHP frameworks extensively use inheritance and polymorphism. Laravel's Eloquent models inherit from a base Model class, giving them database functionality while allowing each model to define its own specific behavior. Understanding these concepts is essential for working with professional PHP codebases.

Basic Inheritance with extends

The extends keyword creates an inheritance relationship between classes. The child class automatically gets all public and protected properties and methods from the parent class, plus it can add its own unique features.

<?php
class Animal {
    protected $name;
    protected $species;

    public function __construct($name, $species) {
        $this->name = $name;
        $this->species = $species;
    }

    public function eat($food) {
        echo "{$this->name} is eating $food\n";
    }

    public function makeSound() {
        echo "{$this->name} makes a generic animal sound\n";
    }
}
?>

Now let's create child classes that inherit from Animal:

<?php
class Dog extends Animal {
    private $breed;

    public function __construct($name, $breed) {
        parent::__construct($name, "Canine");
        $this->breed = $breed;
    }

    // Override parent method
    public function makeSound() {
        echo "{$this->name} barks: Woof! Woof!\n";
    }

    // Add new method specific to dogs
    public function fetch($item) {
        echo "{$this->name} fetches the $item\n";
    }
}

$dog = new Dog("Buddy", "Golden Retriever");
$dog->eat("dog food");    // Inherited method
$dog->makeSound();        // Overridden method
$dog->fetch("ball");      // New method
?>

Notice how Dog automatically inherits eat() from Animal, overrides makeSound() with its own implementation, and adds a new fetch() method.

Understanding parent and self Keywords

The parent keyword lets you access methods from the parent class, while self refers to the current class. These are essential for working with inheritance hierarchies effectively.

<?php
class Vehicle {
    protected $make;
    protected $model;
    protected $mileage;

    public function __construct($make, $model) {
        $this->make = $make;
        $this->model = $model;
        $this->mileage = 0;
    }

    public function drive($distance) {
        $this->mileage += $distance;
        echo "Drove $distance miles\n";
    }
}
?>

Now let's extend this with a specialized vehicle:

<?php
class ElectricCar extends Vehicle {
    private $batteryLevel;

    public function __construct($make, $model) {
        parent::__construct($make, $model);
        $this->batteryLevel = 100;
    }

    public function drive($distance) {
        $batteryUsed = $distance * 0.5;

        if ($this->batteryLevel >= $batteryUsed) {
            parent::drive($distance); // Call parent method
            $this->batteryLevel -= $batteryUsed;
            echo "Battery: {$this->batteryLevel}%\n";
        } else {
            echo "Not enough battery!\n";
        }
    }

    public function charge() {
        $this->batteryLevel = 100;
        echo "Battery fully charged\n";
    }
}

$car = new ElectricCar("Tesla", "Model 3");
$car->drive(50);
$car->charge();
?>

Using parent:: allows child classes to extend parent functionality rather than completely replacing it.

Polymorphism: Same Interface, Different Behavior

Polymorphism allows objects of different classes to be treated uniformly when they share common methods. This enables incredibly flexible code where the same operation can work with many different types of objects.

<?php
abstract class Shape {
    protected $color;

    public function __construct($color) {
        $this->color = $color;
    }

    abstract public function calculateArea();
    abstract public function getDescription();
}
?>

Let's create different shapes that implement the same interface:

<?php
class Circle extends Shape {
    private $radius;

    public function __construct($color, $radius) {
        parent::__construct($color);
        $this->radius = $radius;
    }

    public function calculateArea() {
        return pi() * $this->radius * $this->radius;
    }

    public function getDescription() {
        return "{$this->color} circle with radius {$this->radius}";
    }
}

class Rectangle extends Shape {
    private $width;
    private $height;

    public function __construct($color, $width, $height) {
        parent::__construct($color);
        $this->width = $width;
        $this->height = $height;
    }

    public function calculateArea() {
        return $this->width * $this->height;
    }

    public function getDescription() {
        return "{$this->color} rectangle ({$this->width} x {$this->height})";
    }
}
?>

Now here's the magic of polymorphism—one function that works with all shapes:

<?php
function processShape($shape) {
    echo $shape->getDescription() . "\n";
    echo "Area: " . number_format($shape->calculateArea(), 2) . "\n\n";
}

$shapes = [
    new Circle("red", 5),
    new Rectangle("blue", 4, 6),
    new Circle("green", 3)
];

foreach ($shapes as $shape) {
    processShape($shape);
}
?>

The processShape() function works with any shape because they all implement the same abstract methods. This is polymorphism—one interface, many implementations.

Real-World Example: Employee System

Let's build a practical employee management system that demonstrates inheritance and polymorphism working together.

<?php
abstract class Employee {
    protected $name;
    protected $email;
    protected $baseSalary;

    public function __construct($name, $email, $baseSalary) {
        $this->name = $name;
        $this->email = $email;
        $this->baseSalary = $baseSalary;
    }

    public function getName() {
        return $this->name;
    }

    abstract public function calculateMonthlyPay();
    abstract public function getJobTitle();
}
?>

Now let's create different employee types:

<?php
class Developer extends Employee {
    private $projects;

    public function __construct($name, $email, $baseSalary) {
        parent::__construct($name, $email, $baseSalary);
        $this->projects = [];
    }

    public function calculateMonthlyPay() {
        $projectBonus = count($this->projects) * 500;
        return ($this->baseSalary / 12) + $projectBonus;
    }

    public function getJobTitle() {
        return "Software Developer";
    }

    public function addProject($project) {
        $this->projects[] = $project;
    }
}

class Manager extends Employee {
    private $teamSize;

    public function __construct($name, $email, $baseSalary, $teamSize) {
        parent::__construct($name, $email, $baseSalary);
        $this->teamSize = $teamSize;
    }

    public function calculateMonthlyPay() {
        $managementBonus = $this->teamSize * 200;
        return ($this->baseSalary / 12) + $managementBonus;
    }

    public function getJobTitle() {
        return "Team Manager";
    }
}
?>

Here's how polymorphism makes payroll processing simple:

<?php
function generatePayroll($employees) {
    $totalPayroll = 0;

    foreach ($employees as $employee) {
        $monthlyPay = $employee->calculateMonthlyPay();
        $totalPayroll += $monthlyPay;

        echo $employee->getName() . " - " . $employee->getJobTitle() . "\n";
        echo "Monthly Pay: $" . number_format($monthlyPay, 2) . "\n\n";
    }

    echo "Total Payroll: $" . number_format($totalPayroll, 2) . "\n";
}

$employees = [
    new Developer("Alice", "[email protected]", 90000),
    new Manager("Bob", "[email protected]", 120000, 5)
];

$employees[0]->addProject("E-commerce Site");

generatePayroll($employees);
?>

The generatePayroll() function works with any employee type because they all implement the same abstract methods.

Method Overriding Best Practices

When overriding methods in child classes, following best practices ensures maintainable and predictable code.

<?php
class Logger {
    protected $logLevel;

    public function __construct($logLevel = 'INFO') {
        $this->logLevel = $logLevel;
    }

    public function log($message, $level = 'INFO') {
        $timestamp = date('Y-m-d H:i:s');
        echo "[$timestamp] [$level] $message\n";
    }
}
?>

Here's how to properly extend this logger:

<?php
class FileLogger extends Logger {
    private $filename;

    public function __construct($filename, $logLevel = 'INFO') {
        parent::__construct($logLevel);
        $this->filename = $filename;
    }

    public function log($message, $level = 'INFO') {
        // Call parent method first
        parent::log($message, $level);

        // Add file-specific behavior
        $timestamp = date('Y-m-d H:i:s');
        $logEntry = "[$timestamp] [$level] $message\n";
        file_put_contents($this->filename, $logEntry, FILE_APPEND);
    }
}

$logger = new FileLogger('app.log');
$logger->log("Application started", "INFO");
$logger->log("Database connection failed", "ERROR");
?>

This approach extends the parent functionality rather than completely replacing it, making the code more maintainable.

Understanding Abstract Classes

Abstract classes define contracts that child classes must implement while providing shared functionality. You cannot instantiate abstract classes directly—they exist only to be extended.

<?php
abstract class DatabaseConnection {
    protected $host;
    protected $database;

    public function __construct($host, $database) {
        $this->host = $host;
        $this->database = $database;
    }

    // Concrete method - shared by all connections
    public function getConnectionString() {
        return "Host: {$this->host}, Database: {$this->database}";
    }

    // Abstract methods - must be implemented by child classes
    abstract public function connect();
    abstract public function query($sql);
    abstract public function close();
}
?>

Different database types implement the contract differently:

<?php
class MySQLConnection extends DatabaseConnection {
    public function connect() {
        echo "Connecting to MySQL: {$this->getConnectionString()}\n";
        return true;
    }

    public function query($sql) {
        echo "MySQL Query: $sql\n";
        return "MySQL result";
    }

    public function close() {
        echo "MySQL connection closed\n";
    }
}

class PostgreSQLConnection extends DatabaseConnection {
    public function connect() {
        echo "Connecting to PostgreSQL: {$this->getConnectionString()}\n";
        return true;
    }

    public function query($sql) {
        echo "PostgreSQL Query: $sql\n";
        return "PostgreSQL result";
    }

    public function close() {
        echo "PostgreSQL connection closed\n";
    }
}
?>

Both database connections can be used interchangeably:

<?php
function runDatabaseOperation($connection) {
    $connection->connect();
    $connection->query("SELECT * FROM users");
    $connection->close();
}

$mysql = new MySQLConnection("localhost", "myapp");
$postgres = new PostgreSQLConnection("server.com", "myapp");

runDatabaseOperation($mysql);
runDatabaseOperation($postgres);
?>

Practical Inheritance Tips

Here are some key principles to follow when using inheritance:

Keep inheritance hierarchies shallow: Deep inheritance chains become difficult to understand and maintain. Try to limit yourself to 3-4 levels maximum.

<?php
// Good: Simple, clear hierarchy
class Media { }
class Video extends Media { }
class YouTubeVideo extends Video { }

// Avoid: Too many levels
class Base { }
class Level1 extends Base { }
class Level2 extends Level1 { }
class Level3 extends Level2 { }
class Level4 extends Level3 { } // Getting too deep
?>

Use composition when inheritance doesn't make sense: Not every code reuse scenario requires inheritance.

<?php
// Instead of inheritance, use composition
class Email {
    private $logger;

    public function __construct($logger) {
        $this->logger = $logger;
    }

    public function send($to, $subject, $body) {
        // Send email logic
        $this->logger->log("Email sent to $to");
    }
}
?>

Key Takeaways

Inheritance creates "is-a" relationships where child classes are specialized versions of parent classes. Use it when you have common functionality that can be shared across related classes, but each class needs its own specific behavior.

Polymorphism enables "one interface, many implementations" where different classes can respond to the same method calls in their own unique ways. This creates flexible, maintainable code that can work with new classes without modification.

The parent keyword lets you extend rather than replace parent functionality, creating more maintainable inheritance hierarchies. Always call parent constructors when needed and use parent::methodName() to build upon existing functionality.

Abstract classes define contracts that child classes must implement while providing shared functionality. This ensures consistent interfaces while allowing specialized implementations.

Use inheritance judiciously—prefer composition over inheritance when the relationship isn't clearly "is-a". Not every code reuse scenario requires inheritance; sometimes simple class collaboration is more appropriate.

These concepts form the foundation for advanced OOP patterns and are essential for working with modern PHP frameworks and libraries. Master inheritance and polymorphism, and you'll be ready to tackle sophisticated object-oriented designs.

In the next lesson, we'll explore interfaces and abstract classes in detail, learning how to create even more flexible and maintainable object-oriented architectures.