OOP means organising code around objects that hold data and the operations on it. PHP’s modern syntax makes this considerably shorter than it used to be.
<?php
class BankAccount {
public function __construct(
private string $owner,
private float $balance = 0
) {}
public function deposit(float $amount): void {
if ($amount <= 0) {
throw new InvalidArgumentException("Deposit must be positive");
}
$this->balance += $amount;
}
public function getBalance(): float {
return $this->balance;
}
}
?>That constructor uses property promotion (PHP 8+) — declaring and assigning the properties in one place instead of three.
Creating and using objects#
<?php
$a = new BankAccount("Ada", 100);
$b = new BankAccount("Sam", 50);
$a->deposit(25);
echo $a->getBalance(); // 125
echo $b->getBalance(); // 50
?>$this refers to the current object. The arrow -> reaches into it.
Visibility#
| Keyword | Reachable from |
|---|---|
public |
Anywhere |
protected |
This class and its children |
private |
This class only |
Default to private. Making $balance public means any line anywhere can set it to -5000, and you will never find where.
For values that must never change after construction, PHP 8.1 has readonly:
<?php
class Point {
public function __construct(
public readonly int $x,
public readonly int $y,
) {}
}
$p = new Point(1, 2);
$p->x = 5; // Error: Cannot modify readonly property
?>Inheritance#
<?php
class SavingsAccount extends BankAccount {
public function __construct(string $owner, float $opening, private float $rate) {
parent::__construct($owner, $opening);
}
public function addInterest(): void {
$this->deposit($this->getBalance() * $this->rate);
}
}
?>Use it only for a genuine “is a” relationship. When it is “has a”, hold the object instead — that is composition, and it should be your default.
Interfaces#
<?php
interface Payable {
public function calculatePay(): float;
}
class Employee implements Payable {
public function calculatePay(): float { return 3000.0; }
}
class Contractor implements Payable {
public function __construct(private float $hours, private float $rate) {}
public function calculatePay(): float { return $this->hours * $this->rate; }
}
function payrollTotal(Payable ...$people): float {
return array_sum(array_map(fn($p) => $p->calculatePay(), $people));
}
?>payrollTotal works with anything implementing Payable, including classes written later. That is polymorphism doing real work.
Traits#
PHP allows only one parent class, so traits exist to share code between unrelated classes:
<?php
trait Timestampable {
private ?string $createdAt = null;
public function touch(): void {
$this->createdAt = date("c");
}
public function createdAt(): ?string {
return $this->createdAt;
}
}
class Article {
use Timestampable;
}
?>Static members#
<?php
class Config {
private static array $values = [];
public static function set(string $key, mixed $value): void {
self::$values[$key] = $value;
}
public static function get(string $key, mixed $default = null): mixed {
return self::$values[$key] ?? $default;
}
}
Config::set("debug", true);
?>self:: refers to the class rather than an object. Static state is global state — convenient, and it makes code hard to test because there is no way to isolate it. Use sparingly.
Magic methods#
<?php
class Money {
public function __construct(private int $pence) {}
public function __toString(): string {
return "£" . number_format($this->pence / 100, 2);
}
}
echo new Money(12550); // £125.50
?>__toString is the one worth adding. __get and __call exist and mostly make code harder to follow — reach for them rarely.
Questions people ask#
Do I need OOP for a small script?
No. A hundred-line script of functions is perfectly good PHP. Classes earn their place when data and the rules about it start travelling together.
What is dependency injection?
Passing an object its collaborators rather than letting it create them. It makes classes testable, because you can pass in a fake. Constructor promotion makes it almost free to write.
self versus $this versus static?
$this is the current object; self:: is the class where the code was written; static:: is the class it was actually called on, which matters with inheritance.
Should every class have an interface?
No. Add one when you have, or expect, more than one implementation.