Skip to main content

Command Palette

Search for a command to run...

Exploring Inheritance in PHP

Harnessing Object-Oriented Techniques: A Deep Dive into PHP Class Inheritance

Updated
6 min read
Exploring Inheritance in PHP
L

Hey there, I'm a young developer building software on the web. Currently building snippet.land and Tubitor.

Hello!

Now you know the fundamentals of classes in PHP, it's time to move on to inheritance.

In case you missed the previous posts, you can follow the series here.

What is Inheritance?

The concept of inheritance allows us to create a class that derives from another class. This new class is called a subclass and extends an existing class. For example, we may have a user class and want to add admin users to our app. Instead of creating a whole new AdminUser class with mostly the same functionality, we can instead use inheritance and create a subclass of the User class.

This new subclass inherits all functionality from the User class, but we can override and extend with functionality specific to admin users.

Creating the Superclass

A superclass is a class that is inherited by subclasses. A superclass can be any class, so let's create one:

class User {
    public function __construct(public string $name) {
        //
    }

    public function getDisplayName(): string {
        return $this->name;
    }
}

// Usage
$user = new User("John");
echo $user->getDisplayName(); // "John"

It's a simple user class where each user has a name and a display name that is displayed on the website.

If we want to introduce different types of users, we can create subclasses that inherit, or extend, the user class. We can do so using the extend keyword in the class definition:

class VerifiedUser extends User {

}

// Usage
$verified_user = new VerifiedUser("Jane");
echo $verified_user->getDisplayName(); // "Jane"

As you can see, we haven't added anything to the class yet, but the constructor and getDisplayName methods still work! This is because our class inherits the functionality from the superclass, which in this example is the User class.

Overriding Methods

We now effectively have two identical classes, which isn't very helpful. However, our verified users should have a cool verified badge next to their name!

To implement this, we can override the existing getDisplayName method (that is currently inherited from the User class) with new functionality:

class VerifiedUser extends User {
    public function getDisplayName(): string {
        return $this->name . " ✔";
    }
}

// Usage
$verified_user = new VerifiedUser("Jane");
echo $verified_user->getDisplayName(); // "Jane ✔"

The Override Attribute

Since PHP 8.3, we can explicitly tell PHP that we want to override an existing method using the #[\Override] attribute:

class VerifiedUser extends User {
    #[\Override]
    public function getDisplayName(): string {
        return $this->name . " ✔";
    }
}

In case the function we want to override doesn't exist on the superclass, this will now throw an error:

class VerifiedUser extends User {
    #[\Override]
    public function getName(): string {
        return $this->name . " ✔";
    }
}

// Fatal error: VerifiedUser::getName() has #[\Override] attribute,
// but no matching parent method exists

Adding Methods

Besides overriding existing methods, we can add new methods and properties to our new subclass:

class VerifiedUser extends User {
    public function getDisplayName(): string {
        return $this->name . " ✔";
    }

    public function isVerifiedSince(): DateTime {
        return new DateTime("1 month ago");
    }
}

// Usage
$verified_user = new VerifiedUser("Jane");
echo $verified_user->isVerifiedSince()->format("Y-m-d"); // "2024-04-19"

Composition

Now you might ask, why not simply create a new class? Inheritance is cool, but if my class doesn't have that much functionality, can't I simply create a new class from scratch?

And of course, you could, but then you're missing out on a great feature:

Everywhere you can use the User class, you can now also use the VerifiedUser class, since it is a subclass.

This means, let's say we have a function that accepts a User instance:

function sayHelloTo(User $user) {
    echo "Hello " . $user->getDisplayName();
}

This is not limited to the User class, but also accepts subclasses:

$user = new User("John");
sayHelloTo($user); // "Hello John"

$verified_user = new VerifiedUser("Jane");
sayHelloTo($verified_user); // "Hello Jane ✔"

And we didn't have to change the code of the function, it just works!

However, this does not work in the other direction. If we have a function that accepts a VerifiedUser, we can't pass it an instance of the User class:

function wasVerifiedThisYear(VerifiedUser $user) {
    return $user->isVerifiedSince()->format("Y") === date("Y");
}

$user = new User("John");
var_dump( wasVerifiedThisYear($user) );
// Fatal error: Uncaught TypeError: wasVerifiedThisYear():
// Argument #1 ($user) must be of type VerifiedUser, User given

$verified_user = new VerifiedUser("Jane");
var_dump( wasVerifiedThisYear($verified_user ) );
// bool(true)

Instanceof Operator

The instanceof operator can be used to check if a given object is an instance of a specific class:

$user = new User("John");

var_dump( $user instanceof User ); // bool(true)
var_dump( $user instanceof VerifiedUser ); // bool(false)

Once we know that an object is an instance of a certain class, we can safely call methods specific to this class:

function getVerifiedInfo(User $user): string {
    if(! $user instanceof VerifiedUser) {
        return $user->getDisplayName() . " is not verified";
    }

    return $user->getDisplayName() . " is verified since " . $user->isVerifiedSince()->format("Y-m-d");
}

$user = new User("John");
echo getVerifiedInfo($user); // "John is not verified"

$verified_user = new VerifiedUser("Jane");
echo getVerifiedInfo($verified_user ); // "Jane ✔ is verified since 2024-04-19"

Calling Methods from the Superclass

Let’s go back to our User example from the beginning:

class User {
    public function __construct(public string $name) {
        //
    }

    public function getDisplayName(): string {
        return $this->name;
    }
}

class VerifiedUser extends User {
    public function getDisplayName(): string {
        return $this->name . " ✔";
    }
}

// Usage
$user = new User("John");
echo $user->getDisplayName(); // "John"

$verified_user = new VerifiedUser("Jane");
echo $verified_user->getDisplayName(); // "Jane ✔"

You can see, the VerifiedUser class overrides the getDisplayName method completely. But in this example, we only want to append a little checkmark symbol to the name.

Instead of copying the logic to get the display name from the superclass User, we can call the method we are overriding by prefixing it with parent::, like this:

 class VerifiedUser extends User {
    public function getDisplayName(): string {
        return parent::getDisplayName() . " ✔";
    }
}

The output is the same as before, but now, if we change how the default display name is generated in the superclass, these changes will also be reflected in the VerfiedUser class:

class User {
    public function __construct(public string $name) {
        //
    }

    public function getDisplayName(): string {
        # Let's say we change how the name is displayed:
        return "@" . strtolower($this->name);
    }
}

class VerifiedUser extends User {
    public function getDisplayName(): string {
        # We're not changing anything here!
        return parent::getDisplayName() . " ✔";
    }
}

// Usage
$user = new User("John");
echo $user->getDisplayName(); // "@john"

$verified_user = new VerifiedUser("Jane");
# Notice how the new format is reflected here, even though
# we haven't touched the VerifiedUser class. And the
# checkmark is still added!
echo $verified_user->getDisplayName(); // "@jane ✔"

Next Steps

Extending and overriding methods of existing classes is a very powerful feature. But it’s not only helpful when working with your own classes, it can also come in very handy when you need to change the functionality of a third-party library.

Whenever you’re ready, head over to the next part of this series.

Mastering PHP: From Basics to Advanced Design Patterns

Part 1 of 3

Transform PHP skills from basic to advanced. Learn OOP concepts, dependency injection, DI containers, and design patterns. Perfect for all levels, from beginner to experienced developers.

Up next

Deep Dive into Classes and Objects in PHP

Understanding Classes in PHP for Better Code Structure