PHP Interfaces, Traits and Enums

Design flexible PHP code with abstract classes, interfaces and polymorphism, share methods with traits, and model fixed choices with enums and readonly properties.

Lesson 19 of 86Advanced17 min read

In the classes and objects lesson you wrote classes with properties, constructors and methods, and saw how one class can extend another. Those basics take you a long way. As an application grows, though, you start needing more structure.

How do you say "every payment method must have a charge() method" without writing the same code three times? How do several unrelated classes share a handy logging method? How do you stop an order's status being set to a typo like "shiped"? PHP answers these with abstract classes, interfaces, traits and enums.

These are the tools that frameworks like Laravel and Symfony are built from, so learning them also makes framework code much easier to read. In this lesson you'll use each one in a small, realistic example and learn when to reach for which.

Abstract classes

An abstract class is a base class that can't be created on its own. It can contain normal methods with code, and abstract methods with no body that every child class must write:

PHP
abstract class Shape {
    abstract public function area(): float;

    public function describe(): string {
        return static::class . " with area " . number_format($this->area(), 2);
    }
}

new Shape() is an error, but class Circle extends Shape works as long as Circle defines area(). Abstract classes are ideal when related classes share real code but each needs to fill in a few details.

Interfaces and polymorphism

An interface is a promise: a list of public methods a class must provide, with no code at all:

PHP
interface PaymentMethod {
    public function charge(float $amount): bool;
}
class CardPayment implements PaymentMethod { /* ... */ }

A class can implement several interfaces but extend only one class. When a function's parameter is typed with an interface, function checkout(PaymentMethod $method), it accepts any object that implements it. Calling $method->charge() runs the right version for each class: that's polymorphism. Adding a new payment method later needs no change to checkout.

Traits

A trait is a bundle of methods (and properties) that can be copied into any class with use:

PHP
trait HasTimestamps {
    public ?string $createdAt = null;
    public function touch(): void { $this->createdAt = date('Y-m-d H:i:s'); }
}
class Post { use HasTimestamps; }
class Comment { use HasTimestamps; }

Traits solve a specific problem: sharing code between classes that aren't related by inheritance. They aren't types, so you can't type a parameter as a trait. Use them for small, focused helpers, not as a way to avoid designing classes properly.

Enums, readonly and final

PHP 8.1 added enums: a type with a fixed set of possible values.

PHP
enum Status: string {
    case Pending = 'pending';
    case Shipped = 'shipped';
}
  • Status::Shipped is a value of type Status; a typo is an error, not a silent bug.
  • A backed enum has a string or int for each case, perfect for saving in a database. Status::from('shipped') converts back, and tryFrom() returns null for unknown values.
  • Enums can have methods, such as label().

A readonly property can be set once, in the constructor, and never changed. A final class can't be extended, and a final method can't be overridden.

PHP OOP Syntax

OOP building blocks

abstract class Base { abstract public function run(): void; }
interface Speaks { public function speak(): string; }
class Dog extends Animal implements Speaks, JsonSerializable { }
trait Loggable { public function log(string $m): void { } }
class Order { use Loggable; }
enum Size: string { case S = 's'; case L = 'l'; }
Size::from('s');  Size::tryFrom('x');  Size::cases();
public function __construct(public readonly int $id) {}
final class Config { }

PHP OOP Summary

  • Abstract classes can't be created directly; abstract methods must be written by child classes.
  • Interfaces list methods a class promises to provide; a class can implement several.
  • Typing parameters with an interface enables polymorphism: any implementing class works.
  • Traits share methods between unrelated classes with use.
  • Enums (PHP 8.1) give fixed sets of values; readonly properties can't change after construction.

PHP OOP Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Shapes with an abstract class

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

    public function describe(): string {
        return static::class . " with area " . number_format($this->area(), 2);
    }
}

class Circle extends Shape {
    public function __construct(private float $radius) {}
    public function area(): float { return M_PI * $this->radius ** 2; }
}

class Rectangle extends Shape {
    public function __construct(private float $w, private float $h) {}
    public function area(): float { return $this->w * $this->h; }
}

$shapes = [new Circle(1.5), new Rectangle(4, 2.5)];
foreach ($shapes as $shape) {
    echo $shape->describe() . "\n";
}
Output
Circle with area 7.07
Rectangle with area 10.00
Line-by-line explanation
  1. Line 3Every shape must calculate its own area.
  2. Lines 5 to 7Shared code lives in the parent. static::class gives the real child class's name.
  3. Line 11Constructor promotion creates a private property in one line; line 16 does the same for two.
  4. Lines 20 to 22describe() calls the right area() for each object.
Example 2

Interchangeable payment methods

PHP
<?php
interface PaymentMethod {
    public function charge(float $amount): bool;
    public function name(): string;
}

class CardPayment implements PaymentMethod {
    public function charge(float $amount): bool { return $amount <= 500; }
    public function name(): string { return "Card"; }
}

class VoucherPayment implements PaymentMethod {
    public function __construct(private float $balance) {}
    public function charge(float $amount): bool { return $amount <= $this->balance; }
    public function name(): string { return "Voucher"; }
}

function checkout(PaymentMethod $method, float $total): void {
    $result = $method->charge($total) ? "approved" : "declined";
    echo $method->name() . " payment of £" . number_format($total, 2) . ": $result\n";
}

checkout(new CardPayment(), 89.99);
checkout(new VoucherPayment(25), 40);
Output
Card payment of £89.99: approved
Voucher payment of £40.00: declined
Line-by-line explanation
  1. Lines 2 to 5The interface lists what every payment method must be able to do.
  2. Line 18checkout accepts any PaymentMethod and doesn't care which class it is.
  3. Line 24The voucher only holds £25, so a £40 charge is declined.
Example 3

A trait and a backed enum

PHP
<?php
enum Status: string {
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Delivered = 'delivered';

    public function label(): string {
        return match ($this) {
            Status::Pending => 'Waiting to be packed',
            Status::Shipped => 'On its way',
            Status::Delivered => 'Delivered',
        };
    }
}

trait HasHistory {
    private array $history = [];
    public function record(string $event): void { $this->history[] = $event; }
    public function history(): array { return $this->history; }
}

class Order {
    use HasHistory;
    public function __construct(public readonly int $id, public Status $status = Status::Pending) {}
    public function ship(): void {
        $this->status = Status::Shipped;
        $this->record("shipped");
    }
}

$order = new Order(1042);
$order->ship();
echo "Order {$order->id}: {$order->status->label()}\n";
echo "Saved as: {$order->status->value}\n";
echo "History: " . implode(", ", $order->history()) . "\n";
var_dump(Status::tryFrom('lost'));
Output
Order 1042: On its way
Saved as: shipped
History: shipped
NULL
Line-by-line explanation
  1. Lines 2 to 5A backed enum: each case has a string value for saving in a database.
  2. Lines 7 to 13Enums can have methods. match must cover every case.
  3. Lines 16 to 20A trait adds history-keeping to any class that uses it.
  4. Line 24readonly means the id can never change after the order is created.
  5. Line 36tryFrom returns null for an unknown value instead of throwing.

Common PHP OOP Mistakes

Creating an abstract class

Don't
$s = new Shape();          // Error: cannot instantiate abstract class
Do
$s = new Circle(2);

Missing an interface method

Don't
class PayPalPayment implements PaymentMethod {
    public function charge(float $amount): bool { return true; }
}   // Fatal error: name() isn't implemented
Do
class PayPalPayment implements PaymentMethod {
    public function charge(float $amount): bool { return true; }
    public function name(): string { return "PayPal"; }
}

Strings instead of an enum

Don't
$order->status = 'shiped';           // typo saved silently
Do
$order->status = Status::Shipped;    // a typo would be an error

PHP OOP in Real Projects

OOP in PHP frameworks

Laravel and Symfony are built on interfaces: you can swap the mail service, cache or database driver because your code depends on an interface, not a specific class. Eloquent models share behaviour through traits such as SoftDeletes and HasFactory, and modern apps use enums for statuses, roles and payment types, with enum casts saving them to the database automatically.

PHP OOP FAQ

Should I use an interface or an abstract class?

Use an interface to describe what a class can do, especially when unrelated classes should be interchangeable, such as several payment methods. Use an abstract class when closely related classes share real code and properties. A class can implement many interfaces but extend only one class, and the two are often combined.

Can a class use more than one trait?

Yes: use Timestamps, SoftDeletes;. If two traits define a method with the same name, resolve the conflict with insteadof to choose one, and as to keep the other under a different name. The traits lesson shows the syntax.

What is the difference between an enum and class constants?

Class constants are just named values, so a function typed as string accepts any string, not only your constants. An enum is its own type: a parameter typed Status only accepts Status cases. Enums can also have methods and implement interfaces, which keeps related behaviour together.

Can interfaces contain constants?

Yes. An interface can declare constants, such as const MAX_RETRIES = 3;, which every implementing class shares. Interfaces can't contain method bodies or ordinary properties. PHP 8.4 lets an interface require a property through a property hook declaration, but most interfaces simply list methods.

When should I make a class final?

Mark a class final when it isn't designed to be extended, for example a value object whose rules subclasses could break. It prevents subtle bugs and lets you change the class later without worrying about unknown subclasses. You can always remove final if extension becomes a real need.

PHP OOP Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the class so it promises to provide the methods of the PaymentMethod interface.

class CardPayment  PaymentMethod {
}
Multiple choiceeasy

How many interfaces can a PHP class implement?

Multiple choicemedium

What does a trait let you do?

Predict the outputmedium

What does this print?

enum Size: string { case Small = 's'; case Large = 'l'; }
echo Size::from('l')->name;
Multiple choicehard

Which parameter type lets a function accept any class that can charge a payment?

PHP OOP Quiz

5 questions to check what you learned.

  1. Can you create an object from an abstract class?

    • Yes
    • Only with static
    • Only in PHP 8
    • No, only from its non-abstract child classes
  2. What's the difference between an interface and an abstract class?

    • An interface only lists methods; an abstract class can also contain shared code and properties
    • There's none
    • Interfaces can have constructors
    • Abstract classes can be implemented many times
  3. What does readonly on a property mean?

    • It's private
    • It can't be read
    • It can be set once, in the constructor, and never changed
    • It's static
  4. What does Status::tryFrom('unknown') return?

    • An exception
    • null
    • The first case
    • false
  5. What does final class Config prevent?

    • Other classes extending Config
    • Creating objects
    • Changing properties
    • Using interfaces