We have presented the object-oriented paradigm as the panacea for complex data structures, and even though we have shown that we can define objects with properties and methods, and it looks pretty and fancy, it is not something that we could not solve with arrays. Encapsulation was one feature that made objects more useful than arrays, but their true power lies in inheritance.
Inheritance in OOP is the ability to pass the implementation of the class from parents to children. Yes, classes can have parents, and the technical way of referring to this feature is that a class extends from another class. When extending a class, we get all the properties and methods that are not defined as private, and the child class can use them as if they were its own. The limitation is that a class can only extend from one parent.
To show an example, let's consider our Customer
class. It contains the properties firstname
, surname
, email
, and id
. A customer is actually a specific type of person, one that is registered in our system, so he/she can get books. But there can be other types of persons in our system, like librarian or guest. And all of them would have some common properties to all people, that is, firstname
and surname
. So it would make sense if we create a Person
class, and make the Customer
class extend from it. The hierarchic tree would look as follows:
Note how Customer
is connected to Person
. The methods in Person
are not defined in Customer
, as they are implicit from the extension. Now save the new class in src/Domain/Person.php
, following our convention:
<?php namespace BookstoreDomain; class Person { protected $firstname; protected $surname; public function __construct(string $firstname, string $surname) { $this->firstname = $firstname; $this->surname = $surname; } public function getFirstname(): string { return $this->firstname; } public function getSurname(): string { return $this->surname; } }
The class defined in the preceding code snippet does not look special; we have just defined two properties, a constructor and two getters. Note though that we defined the properties as protected, because if we defined them as private, the children would not be able to access them. Now we can update our Customer
class by removing the duplicate properties and its getters:
<?php namespace BookstoreDomain; class Customer extends Person { private static $lastId = 0; private $id; private $email; public function __construct( int $id, string $name, string $surname, string $email ) { if (empty($id)) { $this->id = ++self::$lastId; } else { $this->id = $id; if ($id > self::$lastId) { self::$lastId = $id; } } $this->name = $name; $this->surname = $surname; $this->email = $email; } public static function getLastId(): int { return self::$lastId; } public function getId(): int { return $this->id; } public function getEmail(): string { return $this->email; } public function setEmail($email): string { $this->email = $email; } }
Note the new keyword extends
; it tells PHP that this class is a child of the Person
class. As both Person
and Customer
are in the same namespace, you do not have to add any use
statement, but if they were not, you should let it know how to find the parent. This code works fine, but we can see that there is a bit of duplication of code. The constructor of the Customer
class is doing the same job as the constructor of the Person
class! We will try to fix it really soon.
In order to reference a method or property of the parent class from the child, you can use $this
as if the property or method was in the same class. In fact, you could say it actually is. But PHP allows you to redefine a method in the child class that was already present in the parent. If you want to reference the parent's implementation, you cannot use $this
, as PHP will invoke the one in the child. To force PHP to use the parent's method, use the keyword parent::
instead of $this
. Update the constructor of the Customer
class as follows:
public function __construct(
int $id,
string $firstname,
string $surname,
string $email
) {
parent::__construct($firstname, $surname);
if (empty($id)) {
$this->id = ++self::$lastId;
} else {
$this->id = $id;
if ($id > self::$lastId) {
self::$lastId = $id;
}
}
$this->email = $email;
}
This new constructor does not duplicate code. Instead, it calls the constructor of the parent class Person
, sending $firstname
and $surname
, and letting the parent do what it already knows how to do. We avoid code duplication and, on top of that, we make it easier for any future changes to be made in the constructor of Person
. If we need to change the implementation of the constructor of Person
, we will change it in one place only, instead of in all the children.
As said before, when extending from a class, we get all the methods of the parent class. That is implicit, so they are not actually written down inside the child's class. What would happen if you implement another method with the same signature and/or name? You will be overriding the method.
As we do not need this feature in our classes, let's just add some code in our init.php
file to show this behavior, and then you can just remove it. Let's define a class Pops
, a class Child
that extends from the parent, and a sayHi
method in both of them:
class Pops { public function sayHi() { echo "Hi, I am pops."; } } class Child extends Pops{ public function sayHi() { echo "Hi, I am a child."; } } $pops = new Pops(); $child = new Child(); echo $pops->sayHi(); // Hi, I am pops. echo $child->sayHi(); // Hi, I am Child.
The highlighted code shows you that the method has been overridden, so when invoking it from a child's point of view, we will be using it rather than the one inherited from its father. But what happens if we want to reference the inherited one too? You can always reference it with the keyword parent
. Let's see how it works:
class Child extends Pops{
public function sayHi() {
echo "Hi, I am a child.";
parent::sayHi();
}
}
$child = new Child();
echo $child->sayHi(); // Hi, I am Child. Hi I am pops.
Now the child is saying hi
for both himself and his father. It seems very easy and handy, right? Well, there is a restriction. Imagine that, as in real life, the child was very shy, and he would not say hi to everybody. We could try to set the visibility of the method as protected, but see what happens:
class Child extends Pops{
protected function sayHi() {
echo "Hi, I am a child.";
}
}
When trying this code, even without trying to instantiate it, you will get a fatal error complaining about the access level of that method. The reason is that when overriding, the method has to have at least as much visibility as the one inherited. That means that if we inherit a protected one, we can override it with another protected or a public one, but never with a private one.
Remember that you can extend only from one parent class each time. That means that Customer
can only extend from Person
. But if we want to make this hierarchic tree more complex, we can create children classes that extend from Customer
, and those classes will extend implicitly from Person
too. Let's create two types of customer: basic and premium. These two customers will have the same properties and methods from Customer
and from Person
, plus the new ones that we implement in each one of them.
Save the following code as src/Domain/Customer/Basic.php
:
<?php namespace BookstoreDomainCustomer; use BookstoreDomainCustomer; class Basic extends Customer { public function getMonthlyFee(): float { return 5.0; } public function getAmountToBorrow(): int { return 3; } public function getType(): string { return 'Basic'; } }
And the following code as src/Domain/Customer/Premium.php
:
<?php namespace BookstoreDomainCustomer; use BookstoreDomainCustomer; class Premium extends Customer { public function getMonthlyFee(): float { return 10.0; } public function getAmountToBorrow(): int { return 10; } public function getType(): string { return 'Premium'; } }
Things to note in the preceding two codes are that we extend from Customer
in two different classes, and it is perfectly legal— we can extend from classes in different namespaces. With this addition, the hierarchic tree for Person
would look as follows:
We define the same methods in these two classes, but their implementations are different. The aim of this approach is to use both types of customers indistinctively, without knowing which one it is each time. For example, we could temporally have the following code in our init.php
. Remember to add the use
statement to import the class Customer
if you do not have it.
function checkIfValid(Customer $customer, array $books): bool { return $customer->getAmountToBorrow() >= count($books); }
The preceding function would tell us if a given customer could borrow all the books in the array. Notice that the type hinting of the method says Customer
, without specifying which one. This will accept objects that are instances of Customer
or any class that extends from Customer
, that is, Basic
or Premium
. Looks legit, right? Let's try to use it then:
$customer1 = new Basic(5, 'John', 'Doe', '[email protected]'); var_dump(checkIfValid($customer1, [$book1])); // ok $customer2 = new Customer(7, 'James', 'Bond', '[email protected]'); var_dump(checkIfValid($customer2, [$book1])); // fails
The first invocation works as expected, but the second one fails, even though we are sending a Customer
object. The problem arises because the parent does not know about any getAmountToBorrow
method! It also looks dangerous that we rely on the children to always implement that method. The solution lies in using abstract classes.
An abstract class is a class that cannot be instantiated. Its sole purpose is to make sure that its children are correctly implemented. Declaring a class as abstract is done with the keyword abstract
, followed by the definition of a normal class. We can also specify the methods that the children are forced to implement, without implementing them in the parent class. Those methods are called abstract methods, and are defined with the keyword abstract
at the beginning. Of course, the rest of the normal methods can stay there too, and will be inherited by its children:
<?php abstract class Customer extends Person { //... abstract public function getMonthlyFee(); abstract public function getAmountToBorrow(); abstract public function getType(); //... }
The preceding abstraction solves both problems. First, we will not be able to send any instance of the class Customer
, because we cannot instantiate it. That means that all the objects that the checkIfValid
method is going to accept are only the children from Customer
. On the other hand, declaring abstract methods forces all the children that extend the class to implement them. With that, we make sure that all objects will implement getAmountToBorrow
, and our code is safe.
The new hierarchic tree will define the three abstract methods in Customer
, and will omit them for its children. It is true that we are implementing them in the children, but as they are enforced by Customer
, and thanks to abstraction, we are sure that all classes extending from it will have to implement them, and that it is safe to do so. Let's see how this is done:
With the last new addition, your init.php
file should fail. The reason is that it is trying to instantiate the class Customer
, but now it is abstract, so you cannot. Instantiate a concrete class, that is, one that is not abstract, to solve the problem.