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:
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:
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:
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.
enum Status: string {
case Pending = 'pending';
case Shipped = 'shipped';
}Status::Shippedis a value of typeStatus; 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, andtryFrom()returnsnullfor 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.
Shapes with an abstract class
<?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";
}Circle with area 7.07
Rectangle with area 10.00Line-by-line explanation
- Line 3Every shape must calculate its own area.
- Lines 5 to 7Shared code lives in the parent. static::class gives the real child class's name.
- Line 11Constructor promotion creates a private property in one line; line 16 does the same for two.
- Lines 20 to 22describe() calls the right area() for each object.
Interchangeable payment methods
<?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);Card payment of £89.99: approved
Voucher payment of £40.00: declinedLine-by-line explanation
- Lines 2 to 5The interface lists what every payment method must be able to do.
- Line 18checkout accepts any PaymentMethod and doesn't care which class it is.
- Line 24The voucher only holds £25, so a £40 charge is declined.
A trait and a backed enum
<?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'));Order 1042: On its way
Saved as: shipped
History: shipped
NULLLine-by-line explanation
- Lines 2 to 5A backed enum: each case has a string value for saving in a database.
- Lines 7 to 13Enums can have methods. match must cover every case.
- Lines 16 to 20A trait adds history-keeping to any class that uses it.
- Line 24readonly means the id can never change after the order is created.
- Line 36tryFrom returns null for an unknown value instead of throwing.
Common PHP OOP Mistakes
Creating an abstract class
$s = new Shape(); // Error: cannot instantiate abstract class$s = new Circle(2);Missing an interface method
class PayPalPayment implements PaymentMethod {
public function charge(float $amount): bool { return true; }
} // Fatal error: name() isn't implementedclass PayPalPayment implements PaymentMethod {
public function charge(float $amount): bool { return true; }
public function name(): string { return "PayPal"; }
}Strings instead of an enum
$order->status = 'shiped'; // typo saved silently$order->status = Status::Shipped; // a typo would be an errorPHP 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.
Complete the class so it promises to provide the methods of the PaymentMethod interface.
class CardPayment PaymentMethod {
}
How many interfaces can a PHP class implement?
What does a trait let you do?
What does this print?
enum Size: string { case Small = 's'; case Large = 'l'; }
echo Size::from('l')->name;
Which parameter type lets a function accept any class that can charge a payment?
PHP OOP Quiz
5 questions to check what you learned.
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
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
What does
readonlyon 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
What does
Status::tryFrom('unknown')return?- An exception
- null
- The first case
- false
What does
final class Configprevent?- Other classes extending Config
- Creating objects
- Changing properties
- Using interfaces