Free Handbook · Every example compiled & verified

Inheritance, Interfaces & Traits

extends and parent::, overriding and final, abstract classes, interfaces, polymorphism, traits with insteadof and as, late static binding, and Countable, IteratorAggregate and Stringable.

0 / 148 lessons🔥 0 day streak
ShareXLinkedIn

Module 07 · what you'll be able to do

  • Extend a class, call parent:: methods and constructors, and override safely with #[\Override] and final
  • Choose between an abstract class and an interface, and implement several interfaces at once
  • Replace type-checking if/switch chains with polymorphism
  • Share code with traits and resolve method collisions with insteadof and as
  • Explain self:: vs static:: and make your own objects work with count(), foreach and echo
01

extends and parent::

class Manager extends Employee makes Manager a subclass: it inherits every public and protected property and method of Employee, and can add its own or override inherited ones. PHP has single inheritance — one parent per class. parent::method() calls the parent's version, which is how an override extends behaviour instead of copying it. A child constructor must call parent::__construct() itself; PHP does not do it for you. The #[\Override] attribute (PHP 8.3) makes PHP check that a parent method with that name really exists.

phpmain.php
<?php
declare(strict_types=1);

class Employee
{
    public function __construct(
        protected string $name,
        protected float $salary,
    ) {}

    public function describe(): string
    {
        return "{$this->name} earns {$this->salary}";
    }

    public function annualCost(): float
    {
        return $this->salary * 12;
    }
}

class Manager extends Employee
{
    private array $reports = [];

    public function __construct(string $name, float $salary, private float $bonus)
    {
        parent::__construct($name, $salary);      // let the parent set its part
    }

    public function addReport(Employee $e): void
    {
        $this->reports[] = $e;
    }

    #[\Override]
    public function annualCost(): float
    {
        return parent::annualCost() + $this->bonus; // extend, do not repeat
    }

    #[\Override]
    public function describe(): string
    {
        return parent::describe() . " and manages " . count($this->reports);
    }
}

$dev = new Employee('Ravi', 90000);
$lead = new Manager('Asha', 150000, 200000);
$lead->addReport($dev);

echo $dev->describe(), "\n";
echo $lead->describe(), "\n";
echo $lead->annualCost(), "\n";
var_dump($lead instanceof Employee, $dev instanceof Manager);
echo get_parent_class($lead), "\n";
Outputcompiled & run with real PHP
Ravi earns 90000
Asha earns 150000 and manages 1
2000000
bool(true)
bool(false)
Employee

A Manager is an Employee, so it can go anywhere an Employee is expected — including addReport(). That "is a" test is the one to apply before you reach for extends.

Your turn

Remove the parent::__construct(...) line and call $lead->describe(). Which error do you get, and which property is it about?

Prefer composition over deep inheritance
Inheritance chains deeper than two levels are hard to change: a tweak in the grandparent ripples through every descendant. Most modern PHP code uses interfaces plus composition (an object that has a mailer, rather than is a mailer) and marks classes final by default. The Design Patterns course shows the patterns this leads to.
02

Overriding rules and final

An overriding method must stay compatible with the parent's, so that code written for the parent still works with the child: same or wider parameter types, same or narrower return type, and any extra parameters must be optional. Visibility can be widened (protected to public) but never narrowed. final public function forbids overriding a method; final class forbids extending the class at all. A final method in the parent that calls an overridable one is the template method pattern: the parent fixes the algorithm, children fill in a step.

phpmain.php
<?php
declare(strict_types=1);

class Notifier
{
    public function send(string $to, string $msg): bool
    {
        echo "generic: $to <- $msg\n";
        return true;
    }

    final public function sendAll(array $recipients, string $msg): int
    {
        $sent = 0;
        foreach ($recipients as $r) {
            if ($this->send($r, $msg)) {        // calls the child's send()
                $sent++;
            }
        }
        return $sent;
    }
}

class SmsNotifier extends Notifier
{
    #[\Override]
    public function send(string $to, string $msg, bool $flash = false): bool
    {
        if (strlen($msg) > 20) {
            echo "sms to $to: too long, skipped\n";
            return false;
        }
        echo "sms to $to: $msg\n";
        return true;
    }
}

final class EmailNotifier extends Notifier
{
}

$sms = new SmsNotifier();
echo $sms->sendAll(['98450', '98860'], 'OTP 4411'), " sent\n";
echo $sms->sendAll(['98450'], 'This message is far too long'), " sent\n";
echo (new EmailNotifier())->sendAll(['[email protected]'], 'Hi'), " sent\n";
Outputcompiled & run with real PHP
sms to 98450: OTP 4411
sms to 98860: OTP 4411
2 sent
sms to 98450: too long, skipped
0 sent
generic: [email protected] <- Hi
1 sent

The extra $flash parameter is allowed because it has a default: every existing call send($to, $msg) still works.

Error you will hit

Fatal error: Cannot override final method

php
<?php
class Invoice
{
    final public function total(): int
    {
        return 100;
    }
}

class DiscountedInvoice extends Invoice
{
    public function total(): int
    {
        return 90;
    }
}
Fatal error: Cannot override final method Invoice::total() in main.php on line 12
Stack trace:
#0 {main}
Why PHP said that

The author of Invoice declared total() final — a promise that every invoice computes its total the same way. PHP checks this when it loads the class, before any code runs, so it is a compile-time error with no "Uncaught".

The fix

Do not fight final. Override the step the parent left open, or wrap the object instead of extending it (composition).

php
<?php
class Invoice
{
    final public function total(): int
    {
        return 100 - $this->discount();
    }

    protected function discount(): int
    {
        return 0;
    }
}

class DiscountedInvoice extends Invoice
{
    #[\Override]
    protected function discount(): int
    {
        return 10;
    }
}
Error you will hit

Fatal error: Declaration must be compatible

php
<?php
class Repository
{
    public function find(string $id): ?array
    {
        return null;
    }
}

class UserRepository extends Repository
{
    public function find(int $id): ?array
    {
        return ['id' => $id];
    }
}
Fatal error: Declaration of UserRepository::find(int $id): ?array must be compatible with Repository::find(string $id): ?array in main.php on line 12
Stack trace:
#0 {main}
Why PHP said that

Code that holds a Repository may call find("abc"). If a UserRepository were passed in, that call would break, because the child only accepts int. PHP refuses the override (the Liskov substitution principle, enforced by the engine).

The fix

Keep the parameter type the same or make it wider (int|string). If the child truly needs a different contract, give the method a different name.

php
<?php
class Repository
{
    public function find(string $id): ?array
    {
        return null;
    }
}

class UserRepository extends Repository
{
    #[\Override]
    public function find(int|string $id): ?array
    {
        return ['id' => (int) $id];
    }
}
03

Abstract classes

An abstract class is a parent that is not complete on its own. It can hold properties and finished methods like any class, plus abstract methods — a signature with no body that every concrete child must implement. You can never create an abstract class with new. Use one when several classes share real code and each must supply one missing piece.

phpmain.php
<?php
declare(strict_types=1);

abstract class Shape
{
    public function __construct(protected string $label) {}

    abstract public function area(): float;      // no body: children write it

    public function describe(): string           // shared code that uses it
    {
        return sprintf("%s with area %.2f", $this->label, $this->area());
    }
}

class Circle extends Shape
{
    public function __construct(private float $r)
    {
        parent::__construct('circle');
    }

    public function area(): float
    {
        return M_PI * $this->r ** 2;
    }
}

class Rectangle extends Shape
{
    public function __construct(private float $w, private float $h)
    {
        parent::__construct('rectangle');
    }

    public function area(): float
    {
        return $this->w * $this->h;
    }
}

$shapes = [new Circle(1), new Rectangle(3, 4.5)];
foreach ($shapes as $s) {
    echo $s->describe(), "\n";
}
Outputcompiled & run with real PHP
circle with area 3.14
rectangle with area 13.50
Your turn

Add a Square that extends Rectangle and takes a single side length. How little code does it need?

Error you will hit

Error: Cannot instantiate abstract class

php
<?php
abstract class Shape
{
    abstract public function area(): float;
}

$s = new Shape();
Fatal error: Uncaught Error: Cannot instantiate abstract class Shape in main.php:7
Stack trace:
#0 {main}
  thrown in main.php on line 7
Why PHP said that

A Shape has no area() body, so an object of it would have a method that cannot run. PHP only lets you create concrete subclasses.

The fix

Create one of the concrete children. If you wanted a default area(), the class should not be abstract.

php
<?php
abstract class Shape
{
    abstract public function area(): float;
}

class Square extends Shape
{
    public function __construct(private float $side) {}

    public function area(): float
    {
        return $this->side ** 2;
    }
}

$s = new Square(2);
04

Interfaces

An interface is a pure contract: a list of public method signatures (and optionally constants) with no code and no properties. A class implements it by providing every method. A class can implement many interfaces while extending only one class, and code that type-hints an interface accepts any class that implements it — the heart of testable, swappable code. instanceof checks whether an object fulfils a contract.

phpmain.php
<?php
declare(strict_types=1);

interface HasId
{
    public function id(): string;
}

interface Payable
{
    public const CURRENCY = 'INR';                // interface constant

    public function amountDue(): int;             // in paise
}

final class Invoice implements HasId, Payable     // several interfaces
{
    public function __construct(private string $number, private int $total) {}

    public function id(): string
    {
        return "INV-{$this->number}";
    }

    public function amountDue(): int
    {
        return $this->total;
    }
}

final class Subscription implements Payable
{
    public function __construct(private int $monthly, private int $months) {}

    public function amountDue(): int
    {
        return $this->monthly * $this->months;
    }
}

function collect(Payable ...$items): string       // any Payable fits
{
    $sum = 0;
    foreach ($items as $item) {
        $sum += $item->amountDue();
        if ($item instanceof HasId) {
            echo "billing ", $item->id(), "\n";
        }
    }
    return number_format($sum / 100, 2) . " " . Payable::CURRENCY;
}

echo collect(new Invoice('1042', 250000), new Subscription(49900, 3)), "\n";
Outputcompiled & run with real PHP
billing INV-1042
3,997.00 INR

Abstract class

  • Can hold properties, a constructor and finished methods
  • A class extends only one
  • Models "is a kind of" with shared code
  • Methods can be protected

Interface

  • Only public method signatures and constants
  • A class implements as many as it needs
  • Models "can do" — a capability or a role
  • The type you put in parameters to keep code swappable
Error you will hit

Fatal error: Class contains 1 abstract method and must therefore be declared abstract

php
<?php
interface PaymentGateway
{
    public function charge(int $amount): bool;
    public function refund(int $amount): bool;
}

class CardGateway implements PaymentGateway
{
    public function charge(int $amount): bool
    {
        return true;
    }
}
Fatal error: Class CardGateway contains 1 abstract method and must therefore be declared abstract or implement the remaining method (PaymentGateway::refund) in main.php on line 8
Stack trace:
#0 {main}
Why PHP said that

Implementing an interface is all or nothing. CardGateway promised refund() and did not write it; the message names the missing method at the end, in brackets.

The fix

Implement every method of the interface, with compatible signatures. Your editor can generate the stubs for you.

php
<?php
interface PaymentGateway
{
    public function charge(int $amount): bool;
    public function refund(int $amount): bool;
}

class CardGateway implements PaymentGateway
{
    public function charge(int $amount): bool
    {
        return true;
    }

    public function refund(int $amount): bool
    {
        return true;
    }
}
05

Polymorphism

Polymorphism means "many shapes": the same call, $rule->apply($total), runs different code depending on the object's class. PHP decides which method to run at run time, from the object itself, not from the type written in the parameter. The payoff is that adding a new behaviour means adding a class — not editing a growing if ($type === ...) chain in ten places.

VisualizeOne call, two methods: how PHP picksStep 1 / 6
<?php
interface Greeter
{
public function greet(): string;
}
class English implements Greeter
{
public function greet(): string { return "Hi"; }
}
class Hindi implements Greeter
{
public function greet(): string { return "Namaste"; }
}
foreach ([new English(), new Hindi()] as $g) {
echo $g->greet(), " ";
}
Line 14

The array holds two objects of different classes. Both are Greeters.

Variables now
$gEnglish object
All 6 steps as a table
StepLineWhat happenedVariables now
114The array holds two objects of different classes. Both are Greeters.$g = English object
215$g->greet(): PHP looks at the object's actual class, English, and finds its greet().$g = English object
38English::greet() runs and returns "Hi".
414Next pass: $g is now a Hindi object.$g = Hindi object
515The same line of code, but the lookup now finds Hindi::greet().$g = Hindi object
612Hindi::greet() returns "Namaste". The loop never checked a type.
phpmain.php
<?php
declare(strict_types=1);

interface DiscountRule
{
    public function apply(float $total): float;
}

final class NoDiscount implements DiscountRule
{
    public function apply(float $total): float
    {
        return $total;
    }
}

final class PercentOff implements DiscountRule
{
    public function __construct(private float $percent) {}

    public function apply(float $total): float
    {
        return $total * (1 - $this->percent / 100);
    }
}

final class FlatOff implements DiscountRule
{
    public function __construct(private float $amount) {}

    public function apply(float $total): float
    {
        return max(0.0, $total - $this->amount);
    }
}

function checkout(float $total, DiscountRule $rule): string
{
    return $rule::class . ": " . number_format($rule->apply($total), 2);
}

foreach ([new NoDiscount(), new PercentOff(10), new FlatOff(150)] as $rule) {
    echo checkout(1000, $rule), "\n";
}
Outputcompiled & run with real PHP
NoDiscount: 1,000.00
PercentOff: 900.00
FlatOff: 850.00

checkout() never asks which rule it has. A new "buy two, get one" rule is one new class and zero edits to checkout() — the open/closed principle in practice.

Your turn

Add a CappedPercentOff rule: a percentage, but never more than a maximum amount off.

06

Traits: sharing code without inheritance

A trait is a bundle of methods (and properties) that PHP copies into any class that says use TraitName;. It gives unrelated classes the same code without forcing them into one inheritance tree — Laravel's SoftDeletes and HasFactory are traits. A trait is not a type: you cannot type-hint it or check it with instanceof, so pair a trait with an interface when callers need a contract.

phpmain.php
<?php
declare(strict_types=1);

trait Timestamps
{
    private array $touches = [];

    public function touch(string $when): void
    {
        $this->touches[] = $when;
    }

    public function lastTouched(): ?string
    {
        return $this->touches === [] ? null : $this->touches[array_key_last($this->touches)];
    }
}

trait SoftDeletes
{
    private bool $deleted = false;

    public function delete(): void
    {
        $this->deleted = true;
    }

    public function isDeleted(): bool
    {
        return $this->deleted;
    }
}

class Post
{
    use Timestamps, SoftDeletes;          // both traits' members are copied in

    public function __construct(public string $title) {}
}

class Comment
{
    use Timestamps;
}

$p = new Post('Hello');
$p->touch('Jan 05');
$p->touch('Jan 09');
$p->delete();
echo $p->lastTouched(), " ", var_export($p->isDeleted(), true), "\n";

$c = new Comment();
var_dump($c->lastTouched());
echo implode(", ", class_uses($p)), "\n";
Outputcompiled & run with real PHP
Jan 09 true
NULL
Timestamps, SoftDeletes

Two traits, one method name: insteadof and as

If two traits define the same method, PHP refuses to guess. Inside the use block, A::m insteadof B picks which one wins, and B::m as other keeps the loser under another name. as can also change visibility. A method written in the class itself always beats a trait method.

phpmain.php
<?php
trait EmailSender
{
    public function send(): string
    {
        return "email sent";
    }
}

trait SmsSender
{
    public function send(): string
    {
        return "sms sent";
    }
}

class Notifier
{
    use EmailSender, SmsSender {
        EmailSender::send insteadof SmsSender;            // send() = email
        SmsSender::send as sendSms;                        // keep sms as sendSms()
        EmailSender::send as protected sendEmailInternal;  // alias + visibility
    }
}

$n = new Notifier();
echo $n->send(), " / ", $n->sendSms(), "\n";
var_dump(method_exists($n, 'sendEmailInternal'), is_callable([$n, 'sendEmailInternal']));
Outputcompiled & run with real PHP
email sent / sms sent
bool(true)
bool(false)

method_exists sees the protected alias; is_callable from outside the class does not, because it is protected.

Error you will hit

Fatal error: Trait method has not been applied because of collision

php
<?php
trait EmailSender
{
    public function send(): string
    {
        return "email";
    }
}

trait SmsSender
{
    public function send(): string
    {
        return "sms";
    }
}

class Notifier
{
    use EmailSender, SmsSender;
}
Fatal error: Trait method SmsSender::send has not been applied as Notifier::send, because of collision with EmailSender::send in main.php on line 18
Stack trace:
#0 {main}
Why PHP said that

Both traits bring a send(). Copying both would give the class two methods with one name, and PHP will not pick one silently.

The fix

Resolve the collision explicitly with insteadof, and alias the other with as if you still need it.

php
<?php
trait EmailSender
{
    public function send(): string
    {
        return "email";
    }
}

trait SmsSender
{
    public function send(): string
    {
        return "sms";
    }
}

class Notifier
{
    use EmailSender, SmsSender {
        EmailSender::send insteadof SmsSender;
        SmsSender::send as sendSms;
    }
}
07

Late static binding: self:: vs static::

self:: means "the class where this code is written". static:: means "the class that was actually called" — decided late, at run time, which is why it is called late static binding. The difference only shows up with inheritance: a static method defined in a parent and called as Child::method(). Return type static and new static() use the same idea, and they are how ORMs like Eloquent make User::create() return a User.

VisualizeSame parent method, two different answersStep 1 / 6
<?php
class A
{
public static function who(): string { return 'A'; }
public static function viaSelf(): string { return self::who(); }
public static function viaStatic(): string { return static::who(); }
}
class B extends A
{
public static function who(): string { return 'B'; }
}
echo B::viaSelf(), B::viaStatic();
Line 12

B::viaSelf(): B has no viaSelf, so PHP runs the one inherited from A. The called class is B.

Variables now
called classB
All 6 steps as a table
StepLineWhat happenedVariables now
112B::viaSelf(): B has no viaSelf, so PHP runs the one inherited from A. The called class is B.called class = B
25self::who() — self is fixed to A, the class this line was written in.called class = B self = A
34A::who() returns "A".
412B::viaStatic(): again the inherited method from A, called on B.called class = B
56static::who() — static is the called class, B.called class = B static = B
610B::who() returns "B".
phpmain.php
<?php
declare(strict_types=1);

class Model
{
    public static function create(): static
    {
        return new static();                  // the class that was called
    }

    public static function createSelf(): self
    {
        return new self();                    // always Model
    }

    public static function table(): string
    {
        return strtolower(static::class) . 's';
    }
}

class User extends Model
{
}

class Order extends Model
{
}

echo get_class(User::create()), "\n";
echo get_class(User::createSelf()), "\n";
echo User::table(), " ", Order::table(), "\n";
Outputcompiled & run with real PHP
User
Model
users orders

Rule of thumb: use static:: and new static() in base classes that are meant to be extended; use self:: for constants and helpers that must never change per subclass (and in final classes, where they are the same thing).

Quick check

In the trace above, what would A::viaStatic() return?

08

Built-in interfaces: Stringable, Countable and IteratorAggregate

PHP ships interfaces that plug your objects into the language itself. Implement Countable and count($obj) works. Implement IteratorAggregate (return an iterator from getIterator()) and foreach works. Stringable is implemented automatically by any class with __toString() (Module 06); writing it explicitly documents the intent. ArrayAccess (for $obj['key']) and JsonSerializable (for json_encode) work the same way.

phpmain.php
<?php
declare(strict_types=1);

final class Playlist implements Countable, IteratorAggregate, Stringable
{
    private array $songs = [];

    public function __construct(private string $name) {}

    public function add(string $song): static
    {
        $this->songs[] = $song;
        return $this;
    }

    public function count(): int                // Countable: count($obj)
    {
        return count($this->songs);
    }

    public function getIterator(): Iterator     // IteratorAggregate: foreach
    {
        return new ArrayIterator($this->songs);
    }

    public function __toString(): string         // Stringable: echo $obj
    {
        return "{$this->name} ({$this->count()} songs)";
    }
}

$p = (new Playlist('Focus'))->add('Raga One')->add('Lo-fi Two')->add('Rain');
echo count($p), "\n";
foreach ($p as $i => $song) {
    echo $i + 1, ". $song\n";
}
echo $p, "\n";

function show(Stringable|string $x): void
{
    echo "> $x\n";
}
show($p);
show("plain text");
Outputcompiled & run with real PHP
3
1. Raga One
2. Lo-fi Two
3. Rain
Focus (3 songs)
> Focus (3 songs)
> plain text

Callers never see the private array, yet the playlist feels like a native collection. A generator — yield from $this->songs; inside getIterator() — is an equally valid body.

Your turn

Implement JsonSerializable on Playlist so that json_encode($p) prints {"name":"Focus","songs":[...]}.

Inheritance
class B extends A: B gets A's public and protected members and can override them. One parent per class.
parent::
Calls the parent class's version of a method or constructor from inside a child.
Override
A child method with the same name as a parent method; its signature must stay compatible. #[\Override] checks it exists.
final
On a method: cannot be overridden. On a class: cannot be extended.
Abstract class
A class that cannot be instantiated and may declare abstract methods its children must implement.
Interface
A contract of public method signatures and constants; a class can implement many.
Polymorphism
One call running different code depending on the object's class, chosen at run time.
Trait
Reusable methods and properties copied into a class with use; not a type.
Late static binding
static:: resolves to the class that was called at run time; self:: to the class where the code is written.

Frequently asked questions

What is the difference between an abstract class and an interface in PHP?
An abstract class can contain properties, a constructor and finished methods alongside abstract ones, and a class can extend only one. An interface contains only public method signatures and constants, and a class can implement many. Use an abstract class to share code between closely related classes, and an interface to describe a capability that unrelated classes can offer.
When should I use a trait in PHP?
Use a trait to share a small, self-contained piece of behaviour, such as timestamps or soft deletes, between classes that do not share a parent. Traits are copied into the class and are not types, so when callers need to depend on the behaviour, pair the trait with an interface.
What is the difference between self and static in PHP?
self refers to the class in which the code is written, while static refers to the class that was actually called at run time (late static binding). They differ only when a static method inherited from a parent is called through a child class, as in User::create() returning new static().

Finish the PHP handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.