Mastering Object Oriented Programming in PHP

Inheritance and Polymorphism in PHP OOP

Last Updated on January 01, 2024 14 min read

Hi stranger! Welcome back to another tutorial in the "Mastering Object-Oriented Programming in PHP" series. THis is the 3rd tutorial in the series. If you've been with me since the beginning, thank you for your continued enthusiasm, and if you're just hopping on board, a warm welcome to you as well!

For those who might have missed the other articles in the series, here are the links to them to catch up:

The focus of this article is majorly on two out of the four pillars of OOP: Inheritance and Polymorphism. These concepts lay the groundwork for scalable, dynamic, and efficient code in OOP, and they are part of the reason why OOP is so powerful. So, let's get started!

Table of contents

What is Inheritance?

This Inheritance word has been passed around so much, and my best bet is that you've already figured out to some extent what it means. But, let's take a look at the formal definition of Inheritance in OOP. Inheritance is a mechanism that allows a class to inherit the properties and methods of another class. The class that inherits the properties and methods of another class is known as the child class, and the class inherited from is called the parent class.

Inheritance allows us to build classes on top of existing classes, allowing us to reuse code and cut down on the amount of code we write. As I indicated before in the series, OOP is inspired by how humans think and interact with their surroundings. Inheritance is also based on that model. A lot of objects around us are specializations of other objects. For example, a car is a specialization of a vehicle, a dog is a specialization of an animal, and so on. There are so many vehicles and animals, but they all share some common properties and methods. Defining the properties and methods in each of them individually is a complete waste of time. What we can do is to extract all common properties and methods into a separate class, and then create other individual classes to extend that base class.

Enough talk, let's see some code.

Inheritance in PHP OOP

Implementing inheritance in PHP OOP is pretty straightforward — use the extends keyword. Let's take a look at an example:

class Animal {
    public function __construct(
        public string $name, 
        public int $age, 
        public string $color
    ) 
    {
    }

    public function eat() : void {
        echo $this->name . " is eating";
    }

    public function sleep() : void {
        echo $this->name . " is sleeping";
    }
}

This Animal class defines the properties and methods that are common to all animals. Now, let's create a Dog and Cat class that will inherit from the Animal class.

class Dog extends Animal {
    public function bark() : void {
        echo $this->name . " is barking";
    }
}

class Cat extends Animal {
    public function meow() : void {
        echo $this->name . " is meowing";
    }
}


$dog = new Dog("Rex", 5, "Brown");
$dog->eat(); // Rex is eating
$dog->bark(); // Rex is barking
$dog->sleep(); // Rex is sleeping

$cat = new Cat("Tom", 3, "White");
$cat->eat(); // Tom is eating
$cat->meow(); // Tom is meowing
$cat->sleep(); // Tom is sleeping

The Dog and Cat classes inherited from the Animal class, and all the baggage that comes with it. (The properties and methods). They also have their own unique properties and methods. bark() for dog and meow() for cat.

From the examples, it is clear that multiple classes can inherit from a single class in PHP. This is called multilevel inheritance. A child clas can have its own child class as well, so we can basically have a family tree of classes. This is called hierarchical inheritance. Eg. Animal class is the parent class of Dog and Cat classes, and Dog class is the parent class of GermanShepherd and Bulldog classes.

class GermanShepherd extends Dog {
    public function __construct(
        public string $name, 
        public int $age, 
        public string $color
    ) 
    {
    }

    public function guard() : void{
        echo $this->name . " is guarding";
    }
}

class Bulldog extends Dog {
    public function __construct(
        public string $name, 
        public int $age, 
        public string $color
    ) 
    {
    }

    public function guard() : void{
        echo $this->name . " is guarding";
    }
}

What is Polymorphism?

Polymorphism works closely with inheritance. It allows objects of different classes to be treated as if they were objects of the same class. It allows us to perform a single action in different ways. It enables methods to be invoked without neccearily knowing the exact type of the object. Polymorphism is a Greek word that means "many forms". It is a powerful feature of OOP that allows us to write flexible and reusable code.

Let's use an analogy to drive this home. Let's say we have a Vehicle class that has two methods - drive() and startEngine(). Assuming we have three classes - Car, Motorcycle and Bicycle that inherit from the Vehicle class and thus it's methods.

class Vehicle {
    public function startEngine() : void{
        echo "Starting the engine of the vehicle.";
    }
    public function move() : void{
        echo "Driving a vehicle";
    }
}

class Car extends Vehicle {
    public function startEngine() : void{
        echo "Starting the engine of the car with a key.";
    }
    public function move(): void {
        echo "Driving a car with 4 wheels";
    }
}

class Motorcycle extends Vehicle {
    public function startEngine() : void {
        echo "Starting the engine of the motorcycle with a kick.";
    }
    public function move() : void {
        echo "Driving a motorcycle with 2 wheels";
    }
}

class Bicycle extends Vehicle {
    public function startEngine() : void {
        echo "Bicycles don't have engines. Just pedal.";
    }
    public function move() : void {
        echo "Driving a bicycle with 2 wheels";
    }
}

$car = new Car();
$car->startEngine(); // Starting the engine of the car with a key.
$car->move(); // Driving a car with 4 wheels

$motorcycle = new Motorcycle();
$motorcycle->startEngine(); // Starting the engine of the motorcycle with a kick.
$motorcycle->move(); // Driving a motorcycle with 2 wheels

$bicycle = new Bicycle();
$bicycle->startEngine(); // Bicycles don't have engines. Just pedal.
$bicycle->move(); // Driving a bicycle with 2 wheels

Now there's nothing new to see here. It's just the regular inheritance we've been doing since. Let's now see how polymorphism comes into play.

class Player {
    private Vehicle $vehicle;
    
    public function setVehicle(Vehicle $vehicle) {
        $this->vehicle = $vehicle;
    }
    
    public function drive() {
        $this->vehicle->startEngine();
        $this->vehicle->move();
    }
}

$car = new Car();
$motorcycle = new Motorcycle();
$bicycle = new Bicycle();

$player = new Player();

$player->setVehicle($car);
$player->drive(); // Starting the engine of the car with a key. Driving a car with 4 wheels

$player->setVehicle($motorcycle);
$player->drive(); // Starting the engine of the motorcycle with a kick. Driving a motorcycle with 2 wheels

Here's what's happening: The $vehicle property and setVehicle() method of the Player class expect a Vehicle type. However, we were able to pass in a Car and Motorcycle object. The extra magic comes in when we call the startEngine() and move() methods of the $vehicle. The Player class doesn't need to know the exact type of the object it's dealing with. It just calls the methods and the appropriate methods are called. This is polymorphism in action.

Method Overriding

The one thing that makes polymorphism possible is method overriding. Method overriding is the ability of a child class to override a method of its parent class. In the example earlier, the Car, Motorcycle and Bicycle classes all override the startEngine() and move() methods of the Vehicle class. In some languages, methods are not overridden by default. You have to explicitly specify that you want to override a method. In PHP, methods are overridden by default.

Also, some languages support method overloading. Method overloading is the ability to have multiple methods with the same name but different parameters. PHP doesn't support method overloading.

Here's an example of how method overloading would have looked like if we had it in PHP.

class Vehicle {
    public function startEngine() {
        echo "Starting the engine of the vehicle.";
    }
    public function move() {
        echo "Driving a vehicle";
    }
}

class Car extends Vehicle {
    public function startEngine() {
        echo "Starting the engine of the car with a key.";
    }
    public function startEngine(string $key) {
        echo "Starting the engine of the car with a $key.";
    }
    public function move() {
        echo "Driving a car with 4 wheels";
    }
}

$car = new Car();
$car->startEngine(); // Starting the engine of the car with a key.
$car->startEngine('blue key'); // Starting the engine of the car with a blue key.

The above code will throw an error because PHP doesn't support method overloading. It's just wishful thinking!

Covariance and Contravariance

Covariance and contravariance sound like big terms, but trust me, they're not as complicated as they seem.

Covariance is the easiest to understand. It allows the return type of a method to be a more specific type in a child class than the type returned by the same method in the parent class. That was a lot to take in, so let's see an example.

class Vehicle {
    public static function make(): Vehicle {
        return new Vehicle();
    }
}

class Car extends Vehicle {
    public static function make(): Car {
        return new Car();
    }
}

The make() method of the Vehicle class returns a Vehicle type. But in the Car class, we specialized the return type to Car. This is covariance in action. You can read up on Liscov Substitution Principle to understand why this is possible. This implies that the return type of a child class cannot be less specific.

Contravariance on the other hand is a bit more complicated. It works with method parameters instead of return types. It allows the parameter type of a method to be a less specific type in a child class than the type of the same parameter in the parent class. Using our Vehicle and Car classes, if a method expects Vehicle, naturally, we can pass in a Car object. Nothing new here. We've seen and done that before. But what if the method expects a Car object, can we pass in a Vehicle object? This is where contravariance comes in. Let's see an example.

class Driver {
    public function drive(Car $car) {
        echo "Driving a car";
    }
}

$driver = new Driver();
$driver->drive(new Car()); // Driving a car

Now if you try to pass in a Vehicle object, you'll get an error. Contravariance comes into play if we have a class that inherits from Driver.

class UberDriver extends Driver {
    public function drive(Vehicle $vehicle) {
        echo "Driving a vehicle";
    }
}

$uberDriver = new UberDriver();
$uberDriver->drive(new Vehicle()); // Driving a vehicle

Even though the parent class expects a Car or any child class of it, we were able to override the method and make it expect a Vehicle object. This is contravariance in action. It can be a bit confusing at first, but you'll get the hang of it.

Instanceof Operator

Now in our earlier example of the UberDriver, what if we want to make check if the object passed in is a Car object? We can't use the is_a() function because it will return false since the object passed in is a Vehicle object. This is where the instanceof operator comes in. It allows us to check if an object is an instance of a class or a child class of it. So we can do something like this:

class UberDriver extends Driver {
    public function drive(Vehicle $vehicle) {
        if($vehicle instanceof Car) {
            echo "Driving a car";
        } else {
            echo "Driving a vehicle";
        }
    }
}

$uberDriver = new UberDriver();
$uberDriver->drive(new Vehicle()); // Driving a vehicle
$uberDriver->drive(new Car()); // Driving a car

instanceof checks if an object is an instance of a class or a child class of it. It doesn't check if an object is an instance of a parent class. So if we do something like:

    $car = new Car();
    $car instanceof Vehicle; 

It should give us true, but something like:

    $vehicle = new Vehicle();
    $vehicle instanceof Car; 

will return false.

Abstract Classes and Methods

Let's reflect this in the context of our vehicles analogy. In the real sense, we're never going to have a vehicle. We're going to have a car, motorcycle, bicycle, etc. So it doesn't make sense to have a Vehicle class. That's where the concept of Abstract classes and methods come in.

Abstract classes are classes that cannot be instantiated on their own(ie an object cannot be created from that class). They are meant to be inherited from. They serve as blueprints for other classes, similar to regular classes, but with a key distinction: they can contain abstract methods, which have no implementation. Abstract methods are methods that are declared but not implemented. They are meant to be implemented in child classes. Abstract classes can also contain regular methods with implementations. So let's refactor our code to use abstract classes.

abstract class Vehicle {
    abstract public function start(): void;
    abstract public function stop(): void;
    public function refuel(): void {
        // Refueling process for vehicles
    }
}

class Car extends Vehicle {
    public function start(): void {
        echo "Starting the engine of the car with a key.";
    }
    public function stop(): void {
        echo "Stopping the engine of the car.";
    }
}

class Motorcycle extends Vehicle {
    public function start(): void {
        echo "Starting the engine of the motorcycle with a kick.";
    }
    public function stop(): void {
        echo "Stopping the engine of the motorcycle.";
    }
}

class Bicycle extends Vehicle {
    public function start(): void {
        echo "Bicycles don't have engines. Just pedal.";
    }
    public function stop(): void {
        echo "Stopping the bicycle.";
    }
}

In this abstract class, we define start() and stop() as abstract methods. These methods are not implemented in the abstract class but provide a common interface that any class inheriting from Vehicle must implement. THe concrete classes that inherit from Vehicle must implement the start() and stop() methods (the IDE will throw an error if you don't). The refuel() method is a regular method with an implementation. It can be overridden in the child classes if need be.

Now this doesn't mean that we can't have Vehicle as a return type or method parameter. We can still have it. We just can't instantiate it. So we can do something like:

class Driver {
    public function drive(Vehicle $vehicle) {
        $vehicle->start();
        $vehicle->move();
        $vehicle->stop();
    }
}

Why Use Abstract Classes and Methods?

Yes, in a way, it looks like making classes abstract is redundant. Why not just make them regular classes? Well, there are a couple of reasons why we use abstract classes and methods. Abstract classes and methods allow you to create blueprints for other classes to follow, enforcing a certain structure in your codebase. The abstract keyword also allows you to prevent a class from being instantiated, thus keeping things strict, tight and organized - there's no chance for errors to creep in on that end.

Final Classes and Methods

Speaking of keeping things strict, the final keyword is another way to do that. The final keyword prevents a class from being inherited from, and a method from being overridden. Now there's a lot of debate on whether or not to use the final keyword. Some people say it's a good practice to use it, while others say it's not. We literally have a package called unfinalize used to remove final from classes in packages (and it has a lot of fans actually). I'm not going to take sides here. I'll just show you how to use it, and you can decide for yourself.

final class Car extends Vehicle {
    public function start(): void {
        echo "Starting the engine of the car with a key.";
    }
    public function stop(): void {
        echo "Stopping the engine of the car.";
    }
}

The Car class is now a final class. It cannot be inherited from. If you try to do so, you'll get an error. The same goes for methods. If you try to override a final method, you'll get an error.

Conclusion

Alright folks, we'll stop here for today. We've covered a lot of ground in this article. I wish I could go on and on, but I have to stop somewhere. There's so much the brain can take at a go so let's not push it. I really hope you enjoyed reading this article as much as I enjoyed writing it. If you have any questions, feel free to reach out to me on Twitter. I'll be happy to help. In the next article, we'll be expanding on inheritance by looking at interfaces and traits. So stay tuned for that. Until then, happy coding!