Phase 3 · Object-Oriented & Modern PHPModule 15~50 min read

Classes, Objects & Encapsulation

Model behavior and invariants with classes, typed properties, constructors, visibility, and immutable value objects.

What you'll learn

Move from procedural scripts to objects that protect business rules. You will design typed state, behavior-rich classes, and immutable value objects instead of exposing arrays that any caller can corrupt.

By the end of this lesson, you'll be able to:

  • Design a class around an invariant
  • Use visibility, constructor promotion, and readonly safely
  • Distinguish entities from value objects

Core mental model

Professional PHP is less about memorizing APIs and more about choosing a clear boundary for each responsibility. Use this table as a decision guide while reading the examples.

ConceptWhat it protectsDecision rule
EncapsulationValid stateChange private state only through named behavior.
EntityIdentity over timeUse identity when two otherwise equal records remain distinct.
Value objectMeaningful immutable valuesCompare by value and reject invalid construction.

Professional workflow

Build the feature in small, verifiable steps. Each step leaves the system in a state you can test.

  1. Describe the domain object boundary: its inputs, outputs, invariants, and expected failures.
  2. Implement the smallest happy path behind an explicit contract.
  3. Add validation and translate low-level failures into language the caller understands.
  4. Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
  5. Refactor only after behavior is protected by a repeatable check.

Make the boundary visible

Name inputs, outputs, side effects, and failure cases before adding framework or infrastructure code. That habit keeps advanced PHP understandable as the application grows.

Guided code lab

Protect a course lifecycle

The class exposes intentions—publish and rename—while its state remains private.

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

final class Course
{
    public function __construct(
        public readonly int $id,
        private string $title,
        private bool $published = false,
    ) {
        $this->rename($title);
    }

    public function rename(string $title): void
    {
        $title = trim($title);
        if ($title === '') {
            throw new InvalidArgumentException('Title is required.');
        }
        $this->title = $title;
    }

    public function publish(): void { $this->published = true; }
    public function title(): string { return $this->title; }
}

Make money impossible to misuse

Integer minor units avoid floating-point surprises and readonly prevents accidental mutation.

Money.php
<?php
final readonly class Money
{
    public function __construct(public int $cents, public string $currency)
    {
        if ($cents < 0) throw new InvalidArgumentException('Negative money');
        if (!preg_match('/^[A-Z]{3}$/', $currency)) throw new InvalidArgumentException('Currency');
    }

    public function add(self $other): self
    {
        if ($this->currency !== $other->currency) throw new LogicException('Currency mismatch');
        return new self($this->cents + $other->cents, $this->currency);
    }
}

Production practice

Contract

Keep properties private by default; expose queries and commands named in the domain.

Verification

Test construction boundaries, state transitions, equality, and rejected input.

Operations

Objects should not secretly open databases or read global request state.

Common failure mode

Public setters turn a class into an unprotected array. If every field can change independently, the object cannot guarantee its own rules.

Independent workshop

Create an Enrollment aggregate that can enroll, cancel, and report status without permitting an invalid transition.

Your finished workshop must include:

  • Typed Enrollment and StudentId classes
  • At least three rejected transitions
  • A small CLI demonstration

Definition of done

Run the happy path and at least two failure paths, explain one design tradeoff in a short README, and leave the code formatted and ready for review.

Recap & quick check

Key takeaways

  • Objects combine state with the behavior that protects it.
  • Constructor validation prevents invalid objects from escaping.
  • readonly is ideal for value objects.
  • Entities compare by identity; values compare by content.

Quick check

1. Why keep mutable properties private?

2. Which type best represents a currency amount?

3. What does readonly prevent?

Keep the workshop: later phases deliberately build on these boundaries, so today's small example can become part of your portfolio architecture.