OOP In Depth: Inheritance, Interfaces, Abstract Classes, and Traits

Course 2 · Ch 2
OOP In Depth: Inheritance, Interfaces, Abstract Classes, and Traits
Four tools for sharing structure and behaviour between related classes, without copy-pasting code

Chapter 1 built single, standalone classes. Real applications usually have several related classes that share common structure — a Dog and a Cat are both Animals, for instance. This chapter covers four distinct tools PHP offers for expressing that kind of relationship, each suited to a slightly different situation.

Inheritance — extends

<?php class Animal { public function __construct(protected string $name) {} public function eat() { echo "{$this->name} is eating.<br>"; } public function makeSound() { echo "Some generic animal sound.<br>"; } } class Dog extends Animal { public function makeSound() { // overrides the parent's version echo "{$this->name} says Woof!<br>"; } } $dog = new Dog("Rex"); $dog->eat(); // "Rex is eating." — inherited from Animal, unchanged $dog->makeSound(); // "Rex says Woof!" — Dog's own override is used instead ?>

Dog extends Animal means every Dog automatically gets everything Animal defines — eat() didn't need to be rewritten at all. Re-defining makeSound() inside Dog is called overriding — PHP always uses the most specific version available for whichever object the method is called on.

Animal
↙ ↘
Dog extends Animal
Cat extends Animal
Both Dog and Cat inherit eat() from Animal, but each overrides makeSound() with its own behaviour

parent:: — Calling the Parent's Version Anyway

<?php class Dog extends Animal { public function makeSound() { parent::makeSound(); // runs Animal's original version first echo "...and also Woof!<br>"; } } ?>

An override doesn't have to fully replace the parent's behaviour — parent::methodName() calls the original version too, useful for "do the normal thing, plus something extra."

Abstract Classes — A Blueprint That Can't Be Used Directly

<?php abstract class Shape { abstract public function getArea(): float; // no body — every subclass MUST provide one public function describe() { echo "Area: " . $this->getArea() . "<br>"; } } class Circle extends Shape { public function __construct(private float $radius) {} public function getArea(): float { return pi() * $this->radius ** 2; } } // $shape = new Shape(); // FATAL ERROR — cannot instantiate an abstract class $circle = new Circle(5); $circle->describe(); // "Area: 78.539..." ?>

An abstract class can mix concrete methods (describe(), fully written) with abstract ones (getArea(), no body — just a required signature). It cannot be instantiated directly with new; it exists purely to be extended, guaranteeing every subclass implements the methods marked abstract.

Interfaces — A Pure Contract, No Implementation At All

<?php interface Sellable { public function getPrice(): float; } class Book implements Sellable { public function __construct(private float $price) {} public function getPrice(): float { return $this->price; } } function printPrice(Sellable $item) { // accepts ANY class that implements Sellable echo "£" . $item->getPrice(); } printPrice(new Book(9.99)); ?>

An interface defines method signatures only — no bodies at all, not even partial ones. A class implements an interface to promise it provides those methods. The real power: printPrice() doesn't care whether it receives a Book, a Car, or anything else — only that it implements Sellable, so it definitely has a getPrice() method.

ToolWhat it gives you
extendsA class inherits from one parent class (single inheritance only)
abstract classPartial blueprint — some methods written, some left required-but-empty
interfacePure contract — method signatures only, a class can implement many
traitReusable method code, mixed into a class — covered next
A class can implement multiple interfaces, but extend only one class
PHP supports "single inheritance" — a class has exactly one direct parent via extends. But implements Sellable, Comparable, Loggable (comma-separated) is entirely valid, since interfaces are just promises about method signatures, not actual shared code to merge.

Traits — Sharing Actual Method Code, Without Inheritance

<?php trait Loggable { public function log(string $message) { echo "[LOG] $message<br>"; } } class Order { use Loggable; // pulls log() directly into this class } class User { use Loggable; // completely unrelated class, also gets log() this way } (new Order())->log("Order placed"); (new User())->log("User registered"); ?>

Order and User have no inheritance relationship to each other at all — they're unrelated classes that both happen to need logging behaviour. A trait solves exactly this: actual, reusable method code that gets copied into any class using use TraitName;, sidestepping the "only one parent class" limit of extends.

Choosing between these four tools
"Is-a" relationship with shared state and behaviour (a Dog IS an Animal) → extends. Want to guarantee a method exists, with no implementation to share → interface. Want a partial blueprint mixing some shared code with some required-but-unwritten methods → abstract class. Need to share actual method code across otherwise-unrelated classes → trait.

Coding Challenges

Challenge 1

Create an Animal class with a constructor setting $name, and a method makeSound() that echoes a generic message. Create Cat and Cow classes that extend it and override makeSound() with their own sound. Create one of each and call makeSound() on both.

📄 View solution
Challenge 2

Create an abstract class Employee with a constructor setting $name, an abstract method calculatePay(): float, and a concrete method describe() that echoes the name and calculated pay together. Create two subclasses, SalariedEmployee and HourlyEmployee, each implementing calculatePay() differently.

📄 View solution
Challenge 3

Create a trait Greetable with a method sayHello() that echoes "Hello from " followed by a $name property. Use this trait in two unrelated classes, Robot and Alien (neither extending the other, or any common parent), each with their own $name set via a constructor. Call sayHello() on one instance of each.

📄 View solution

Chapter 2 Quick Reference

  • extends — single inheritance; subclass gets everything the parent has, can override methods
  • parent::method() — calls the parent's original version from inside an override
  • abstract class — partial blueprint; cannot be instantiated; abstract methods MUST be implemented by subclasses
  • interface — pure method signatures, no bodies; a class can implement several
  • trait — reusable method code, mixed into otherwise unrelated classes via "use"
  • Decision guide: is-a → extends; guaranteed method, no shared code → interface; partial shared blueprint → abstract class; shared code across unrelated classes → trait
  • Next chapter: error handling — exceptions, try/catch/finally, custom exceptions