Chapter 9: Object-Oriented PHP
PHP supports a modern object model that lets you structure programs around collaborating objects. This chapter introduces class declarations, properties, and methods; it moves through construction and visibility; then explores inheritance, interfaces, traits, magic, and newer features such as enums and readonly properties.
Classes, Properties, and Methods
A class defines a blueprint for objects. You declare properties to hold state and methods to define behavior. An object is an instance created with new; each object carries its own property values while sharing the same method definitions.
Declaring a basic class and creating objects
A minimal class uses the class keyword and braces. Methods are functions that live inside the class; properties are variables declared inside the class scope. Instantiate with new to get a working object.
class Counter {
public int $value = 0;
public function increment(): void {
$this->value++;
}
}
$c = new Counter();
$c->increment();
echo $c->value; // 1
Accessing object state with $this
Inside a method, the pseudo variable $this refers to the current object. Use the object operator -> to access properties and call other methods on the same instance.
class Greeter {
public string $name = "World";
public function hello(): string {
return "Hello, " . $this->name;
}
}
Typing properties and method signatures
PHP supports property types and parameter or return types. Use scalar types, class names, union types, and nullable types to make contracts explicit and self documenting.
class Repository {
/** @var array<int, string> */
private array $items = [];
public function add(int $id, string $value): void {
$this->items[$id] = $value;
}
public function get(int $id): ?string {
return $this->items[$id] ?? null;
}
}
Using constants and static members
Class constants hold values that do not change. Static properties and static methods belong to the class itself. Access constants with ClassName::CONST and static members with ClassName::member.
class MathUtil {
public const PI = 3.14159;
public static function circleArea(float $r): float {
return self::PI * $r * $r;
}
}
echo MathUtil::circleArea(2.0);
Constructors, Promotion, and Visibility
Constructors prepare objects for use. Property promotion shortens constructor boilerplate. Visibility controls who may read or call members. Together these features help you create clear object lifecycles and safe encapsulation.
Initializing state in __construct()
The __construct() method runs on new. Validate inputs and set defaults here; avoid heavy work that you could defer to a separate method if possible.
class User {
private string $email;
public function __construct(string $email) {
// Basic check for illustration only
if ($email === "") {
throw new InvalidArgumentException("Email required");
}
$this->email = $email;
}
}
Using constructor property promotion for brevity
Constructor property promotion lets you declare and assign properties in the parameter list. This reduces duplication while keeping types and visibility in one place.
class Point {
public function __construct(
public float $x,
public float $y
) {}
}
$p = new Point(1.2, 3.4);
echo $p->x;
Choosing public, protected, or private
public exposes members to any caller; protected exposes to the class and subclasses; private limits to the declaring class. Prefer the most restrictive visibility that still meets your needs.
| Visibility | Accessible From |
public | Anywhere |
protected | Class and subclasses |
private | Declaring class only |
Using promoted readonly parameters for safe construction
Combine promotion with readonly to guarantee a property can only be written during construction. This ensures the object’s state remains stable after it is built.
class Config {
public function __construct(
public readonly string $dsn,
public readonly string $user,
public readonly string $password
) {}
}
Inheritance and Late Static Binding
Inheritance allows a class to extend another and reuse or refine behavior. When working with static references, late static binding helps subclasses get the correct class context during static access.
Extending behavior with extends
Use extends to inherit properties and methods. Override a method by redeclaring it with the same signature; call the parent implementation with parent::method() when you need to build on it.
class Logger {
public function log(string $msg): void {
echo "[LOG] " . $msg . PHP_EOL;
}
}
class FileLogger extends Logger {
public function log(string $msg): void {
parent::log($msg);
// write to file …
}
}
Calling parent constructors safely
If a subclass defines its own constructor, call parent::__construct(…) when the base class requires initialization. This ensures shared state is prepared correctly.
class Connection {
public function __construct(protected string $dsn) {}
}
class PdoConnection extends Connection {
private \PDO $pdo;
public function __construct(string $dsn) {
parent::__construct($dsn);
$this->pdo = new \PDO($dsn);
}
}
Understanding late static binding with static::
Late static binding resolves a static reference to the called class rather than the class where the method was defined. Use static:: or the static return type to make fluent inheritance behave correctly.
class Base {
public static function factory(): static {
return new static();
}
}
class Child extends Base {}
$made = Child::factory(); // instance of Child
Preventing inheritance with final
Mark a class or a specific method as final to prevent extension or overriding. This locks behavior when variation would be unsafe.
final class IdGenerator {
public static function make(): string {
return bin2hex(random_bytes(16));
}
}
Interfaces, Abstract Classes, and Traits
Interfaces define capabilities without implementation; abstract classes can include shared code and abstract methods; traits let you reuse method implementations across unrelated classes.
Defining capabilities with an interface
An interface declares method signatures. Any class that implements the interface agrees to provide those methods. This enables polymorphism without coupling to a concrete type.
interface Cache {
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttl = 0): void;
}
Sharing partial implementations via an abstract class
Abstract classes can provide common code while declaring abstract methods that subclasses must implement. Use this pattern when implementations share data layout or helper logic.
abstract class Shape {
public function area(): float {
return $this->computeArea();
}
abstract protected function computeArea(): float;
}
class Rectangle extends Shape {
public function __construct(private float $w, private float $h) {}
protected function computeArea(): float {
return $this->w * $this->h;
}
}
Reusing method sets with a trait
Traits inject methods into classes through use. They are ideal for cross cutting helpers that do not fit an inheritance hierarchy.
trait Timestamps {
protected \DateTimeImmutable $createdAt;
public function markCreated(): void {
$this->createdAt = new \DateTimeImmutable();
}
}
class Post {
use Timestamps;
}
Resolving trait method conflicts
When two traits provide the same method, use insteadof and as to resolve conflicts or rename. This keeps call sites simple while avoiding ambiguity.
trait A { public function run() { echo "A"; } }
trait B { public function run() { echo "B"; } }
class Task {
use A, B {
A::run insteadof B;
B::run as runB;
}
}
Magic Methods and Overloading
Magic methods are special hooks that begin with double underscores. PHP calls them automatically to support string casting, serialization, property or method overloading, and object lifecycle events.
Using __toString() for friendly output
Return a concise representation that helps debugging and logging. Keep it safe; never throw exceptions from __toString().
class Money {
public function __construct(
public readonly int $cents,
public readonly string $currency
) {}
public function __toString(): string {
return $this->currency . " " . number_format($this->cents / 100, 2);
}
}
Intercepting property access with __get() and __set()
These hooks run when reading or writing inaccessible properties. Use them to proxy to internal storage or compute values on demand.
class DataBag {
private array $data = [];
public function __get(string $name): mixed {
return $this->data[$name] ?? null;
}
public function __set(string $name, mixed $value): void {
$this->data[$name] = $value;
}
}
Catching dynamic calls with __call() and __callStatic()
When a method is not found, PHP invokes these handlers. Common uses include forwarding to composed objects or implementing simple query builders; keep behavior predictable.
class Forwarder {
public function __call(string $name, array $args): mixed {
// Forward to an inner service …
return null;
}
public static function __callStatic(string $name, array $args): mixed {
// Handle static calls …
return null;
}
}
Managing lifecycle with __construct() and __destruct()
Use __construct() for setup and __destruct() for best effort cleanup such as closing files. Destructors run during shutdown; avoid throwing and avoid long blocking operations.
class TempFile {
public function __construct(private string $path) {}
public function __destruct() {
@unlink($this->path);
}
}
| Magic Hook | Triggered When |
__get(), __set(), __isset(), __unset() | Accessing non public or missing properties |
__call(), __callStatic() | Calling inaccessible or missing methods |
__toString() | Casting to string |
__serialize(), __unserialize() | Custom serialization |
__debugInfo() | Var dumping |
Enums and Readonly Properties
Enums model a fixed set of values with type safety. Readonly properties protect state after construction. Both features reduce accidental mutation and make intent clear.
Declaring a pure enum for fixed choices
A pure enum defines named cases without payloads. Use it when you need a stable closed set like statuses or modes.
enum Status {
case Pending;
case Active;
case Suspended;
}
function activate(Status $s): bool {
return $s === Status::Pending;
}
Using a backed enum for external representation
Backed enums associate each case with a scalar. This is useful for persistence or APIs that work with strings or integers.
enum Role: string {
case Admin = "admin";
case Editor = "editor";
case Viewer = "viewer";
}
$role = Role::from("editor"); // throws if unknown
$maybe = Role::tryFrom("guest"); // null if unknown
Attaching behavior to enum cases
Enums can include methods. Add helpers for display labels or transitions; keep logic small and focused.
enum Traffic: string {
case Red = "red";
case Amber = "amber";
case Green = "green";
public function canGo(): bool {
return $this === self::Green;
}
}
Protecting state with readonly properties
A readonly property can be written during construction and then remains immutable. Use this for identifiers or configuration that should never change after the object is built.
class Order {
public function __construct(
public readonly string $id,
public readonly Status $status
) {}
}
// $order->id = "new"; // Error