Objects Namespaces And Application Architecture

Encapsulation And Protecting Object Invariants

Encapsulation is the practice of keeping an object responsible for maintaining its own valid state. It is not simply making every property private or generating getters and setters. A useful object exposes operations that express domain intent and refuses transitions that would make its state invalid.

Encapsulation puts state and the rules that protect it in the same object. The point is not hiding code for its own sake; it is preventing callers from creating states the model says should be impossible.

Why This Matters

Public writable state lets every caller become a partial owner of an invariant. Once mutation is scattered, no one method can guarantee that quantity, status, balance, or lifecycle values remain consistent.

Start with the object or service that owns the relevant invariant. Identify who supplies input, who may change state, and what result the caller is entitled to observe. For Encapsulation And Protecting Object Invariants, the most persuasive evidence comes from public behavior, contract tests, dependency direction, and valid state transitions; naming those observations before implementation prevents tool names from standing in for a design.

Working Model

An invariant is a fact that must remain true for every observable instance. Constructor validation establishes the invariant, methods preserve it, and visibility prevents callers from bypassing those rules. Encapsulation also controls side effects: an object should not silently perform network or database work just because one property changed.

The important vocabulary includes:

  • invariants established at construction
  • private state with intention-revealing methods
  • immutable value objects
  • collections that cannot be mutated accidentally
  • tell-dont-ask style decisions
  • serialization and persistence boundaries

Read the terms through ownership: private properties hold representation, public methods expose valid operations, invariants define what must always be true, and value objects package validated concepts.

Core Design Decisions

  1. Place a rule in an object when that object owns the state needed to enforce it consistently.
  2. Prefer methods such as reserve(), pay(), or rename() over setters that accept any value without context.
  3. Return immutable values or defensive copies when exposing internal collections or dates.
  4. Use readonly properties for stable data, but remember readonly syntax does not by itself validate a meaningful domain state.
  5. Keep application orchestration outside entities when the operation coordinates repositories, email, queues, or remote APIs.
  6. Design persistence mapping so hydration cannot leave an entity in a state normal code could never create.

Use abstraction where it isolates a real variation or responsibility; otherwise prefer direct, cohesive code. Record the reason beside the code or configuration when the choice is not obvious, especially when future maintainers might otherwise “simplify” away a required guarantee.

PHP-Facing Example

The example demonstrates a state-changing method rather than a public property assignment. That pattern lets the object reject invalid transitions at the point of mutation.

PHP example
<?php

declare(strict_types=1);

final class StockItem
{
    public function __construct(
        public readonly int $id,
        private int $available,
    ) {
        if ($available < 0) {
            throw new InvalidArgumentException('Stock cannot be negative.');
        }
    }

    public function reserve(int $quantity): void
    {
        if ($quantity < 1 || $quantity > $this->available) {
            throw new DomainException('Quantity is unavailable.');
        }

        $this->available -= $quantity;
    }

    public function available(): int
    {
        return $this->available;
    }
}

$item = new StockItem(42, 5);
$item->reserve(2);
echo $item->available();

// Prints:
// 3

In Encapsulation And Protecting Object Invariants, notice which values the example accepts and what form it returns. Production code should preserve that clarity when it crosses the object or service that owns the relevant invariant, translating external or loosely typed data at the edge instead of allowing it to spread through unrelated classes.

Implementation Workflow

Name the invariant first, then design construction and mutation around it. Keep properties private, expose operations that match domain language, and return values without leaking mutable internals. If persistence or serialization bypasses construction, add validation at that boundary too.

Failure Modes

  • Making fields private while exposing unrestricted setters that recreate public mutable state.
  • Putting every business operation into one entity until it becomes coupled to infrastructure and unrelated workflows.
  • Returning a mutable object that lets callers change internal state without the owning object knowing.
  • Allowing ORM hydration, reflection, or unserialization to bypass invariant checks without compensating validation.
  • Using getters to pull all state into a service that makes decisions the object should own.
  • Confusing encapsulation with secrecy and hiding behavior that callers need to reason about.

Invalid state escaping construction, public setters that allow illegal transitions, reflection-based hydration, and mutable returned collections all break encapsulation differently. Fix the path that owns the mutation rather than adding scattered checks.

Verification Strategy

Use the following checks as a starting point:

  • Construct valid and invalid instances and assert invalid states cannot escape.
  • Test every state-changing method at boundaries such as zero quantity, exhausted capacity, or a terminal status.
  • Search for direct property mutation and reflection-based writes in application code.
  • Verify emitted events and persistence changes correspond to successful transitions only.
  • Test serialization and hydration paths with old or malformed data.
  • Review whether error types tell the caller how to recover without exposing internal implementation.

Test valid construction, invalid construction, every state-changing method, boundary values, and attempts to mutate returned data. Integration tests should cover ORM or serializer paths that populate objects.

Security And Data Handling

Keep privileged collaborators explicit and prevent callers from bypassing invariant checks through public mutation or hidden service lookup.

Encapsulated objects may still be logged, serialized, cloned, or displayed. Keep sensitive fields out of debug output and expose response shapes through explicit mappers.

Tradeoffs And Evolution

Use abstraction where it isolates a real variation or responsibility; otherwise prefer direct, cohesive code.

Changing an invariant requires migration of stored data and older callers. Add new operations compatibly, reject legacy invalid states deliberately, and keep repair scripts auditable.

Review Questions

Before considering the topic implemented, answer these questions:

  • Which Encapsulation And Protecting Object Invariants guarantee matters to the caller?
  • Where does the object or service that owns the relevant invariant live?
  • Which input or state can be stale, malformed, duplicated, or unauthorized?
  • What evidence from public behavior, contract tests, dependency direction, and valid state transitions proves the normal path?
  • Which failure is retryable, and how are duplicate effects prevented?
  • How will public methods, subclass contracts, serialized objects, container wiring, and persistence mapping be handled during change?
  • Which operational signal reveals degradation?
  • What simpler choice was rejected, and why?

If a caller can bypass the method that enforces the rule, the invariant is not encapsulated. The class API should make the valid path easier than the invalid one.

Focused Application

Model an inventory reservation. The object should know available quantity, reserved quantity, and terminal states. A public $reserved property allows negative reservations or reservations after closure. A reserve(int $quantity) method can reject those cases and leave the object unchanged.

Then test persistence. Hydrate an old row, a corrupted row, and a valid row. Decide whether invalid historical data is rejected, repaired, or represented as a separate migration problem. Encapsulation is incomplete if the database path can create a state the constructor forbids.

Finally, return a read model for templates rather than the mutable entity. Display code needs facts, not permission to rewrite the aggregate.

Encapsulation Across Layers

Encapsulation does not stop at the class file. A repository can break an object's invariant by hydrating private fields incorrectly. A JSON mapper can expose a field the object deliberately hides. A form handler can rebuild an entity from partial request data and skip the method that enforces a transition.

Use separate input models, domain objects, and response models when their responsibilities differ. A request object can represent untrusted submitted fields. A domain object can protect business state. A response model can expose the exact data a template or API client may see. Collapsing all three into one mutable object makes every layer a possible mutation path.

Collections need the same care. Returning an array of mutable child objects may let callers mutate the aggregate without the aggregate noticing. If child mutation affects parent invariants, route changes through the parent or use immutable child values. If the child is independently owned, model that boundary explicitly.

Encapsulation also improves error messages. A method that rejects ship() on a cancelled order can return a domain-specific conflict. A public status setter can only leave later code to discover an impossible combination. Keeping the rule with the state makes failures earlier and easier to classify.

Review Checks

Ask reviewers to find every mutation path. Include constructors, named constructors, public methods, ORM hydration, deserialization, reflection-heavy libraries, tests, and administrative repair scripts. If any path can change state without invoking the rule that protects it, the object is not the only owner of the invariant. Either close that path or document it as a controlled maintenance boundary with its own validation.

Do not confuse encapsulation with large objects. A class that owns too many invariants becomes difficult to reason about even when every property is private. Split along ownership boundaries when separate rules change independently.

Official References

What You Should Be Able To Do

After this lesson, you should be able to explain encapsulation and protecting object invariants in operational terms, choose it only when its guarantees fit the requirement, implement a narrow PHP-facing boundary, recognize the common failure modes, and verify behavior using evidence from the real system rather than assuming the configuration is correct.

Practice

Model The Boundary

For Encapsulation And Protecting Object Invariants, write a short design note for a product-catalog application. Identify the caller, authoritative state, inputs, output, trust boundary, and one invariant. Include one success timeline and one partial-failure timeline.

Show solution

A strong answer names concrete ownership rather than only a tool. The authoritative state should be explicit, and derived copies should be labelled as such. The success timeline should identify when the result becomes observable. The failure timeline should stop after an intermediate step and explain whether retry is safe, whether status must be queried, or whether reconciliation is required.

The invariant should be testable. Examples include “one order causes at most one captured payment,” “only authorized tenant documents appear in results,” or “the latest valid configuration is the one served after rollout.”

Review A Design

Review an implementation of Encapsulation And Protecting Object Invariants that works in one local demonstration. Use the lesson’s failure modes to identify at least four production risks. For each risk, propose the narrowest change that restores a clear guarantee.

Show solution

The review should connect each risk to a violated guarantee. Good findings cover validation, authorization, retry or duplicate behavior, environment drift, and observability. Avoid broad recommendations such as “use best practices.” Name the responsible boundary and the evidence that would prove the repair.

A narrow repair may be a constraint, explicit comparison policy, interface contract, timeout, idempotency key, index, security rule, probe, IAM policy, or integration test. The selected mechanism must match the actual risk.

Build A Verification Plan

Create a verification plan for Encapsulation And Protecting Object Invariants. Include one unit test, one boundary-level integration test, one negative security or validation test, one failure-injection exercise, and two operational signals. State what each check proves and what it does not prove.

Show solution

The unit test should cover a pure decision. The integration test must cross the real boundary rather than only checking a prepared request. The negative test should use an identity or input that must be rejected. The failure exercise should create a timeout, retry, restart, unavailable dependency, or incompatible version deliberately.

Operational signals should reveal both health and correctness. Useful examples include latency percentiles, error classification, queue age, indexing lag, denied requests, restart count, duplicate side effects, resource saturation, and final business-state reconciliation.