Inheritance and Polymorphism in PHP OOP
- What is Inheritance?
- Inheritance in PHP OOP
- What is Polymorphism?
- Method Overriding
- Abstract Classes and Methods
- Final Classes and Methods
- Conclusion
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!
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!
0 Comments
No comments yet. Be the first to comment!
Would you like to say something? Please log in to join the discussion.
Login with GitHub