Skip to content

PHP object model

Status: Complete. Last reviewed 2026-08-28.

An object variable holds an object handle that identifies an instance. Assigning or passing that variable copies the handle, so both variables address the same object. This is often described casually as “objects are references,” but PHP references are a separate language mechanism that aliases variable containers. Keeping those mechanisms separate explains identity, cloning, mutation, and surprising API behavior more accurately.

After $second = $first, mutating the instance through either name is visible through the other because both handles identify one object. Rebinding $second to a different instance does not rebind $first. Adding & creates an explicit variable alias and changes rebinding behavior; it is unnecessary merely to let a function mutate an object it receives.

Object identity and equality are different tests. $a === $b requires the same instance. $a == $b requires the same class and recursively compares properties using loose value comparison. Value objects usually deserve an explicit equals() method because domain equality may require normalization, type-sensitive fields, or selected properties rather than PHP’s structural comparison rules.

Object identity is process-local. spl_object_id() can help diagnose instances during one process lifetime, but it is not a durable identifier and may be reused after an object is destroyed. Persisted entities need application/database identity.

clone $object creates a new top-level instance and copies its properties. Scalar and array values follow their normal value semantics, but a property containing another object initially contains a handle to the same nested instance. PHP then calls __clone() on the new outer object, if defined, so the class can clone selected collaborators, regenerate identity, or clear derived state.

A universal deep clone is not a coherent default. Object graphs may contain cycles, shared nodes whose sharing is meaningful, database connections, closures, external handles, or entities whose identity must not be duplicated. The class owning the invariant should decide what a copy means. Often a named method such as withAddress(), duplicateAsDraft(), or a fresh constructor communicates intent better than exposing arbitrary cloning.

PHP 8.5 adds clone-with property updates, useful for immutable “wither” operations. It still begins from cloning semantics; it does not recursively copy reachable objects or make an unsafe graph safe to duplicate.

final class Delivery
{
public function __construct(
public Address $address,
public string $note,
) {}
public function __clone(): void
{
$this->address = clone $this->address;
}
}

Without __clone(), the original and clone above would share one Address instance.

$this is the current object in an instance method. self:: resolves against the class where the method is declared. parent:: selects the parent implementation from that lexical class. static:: uses late static binding: it resolves against the class originally called at runtime and forwards that called-class information through qualifying calls.

Late static binding is useful for inherited named constructors and template methods intended to preserve the derived type. It can also make a hierarchy fragile when a base class silently depends on subclass static state. new self() deliberately constructs the declaring class; new static() constructs the runtime-called class and therefore assumes compatible construction. Return type static communicates that stronger late-bound promise.

Private methods are scoped to the declaring class and are not ordinary polymorphic extension points. Combining private methods, static::, and same-named child methods can produce behavior that is technically defined but hard to explain. Prefer explicit protected hooks or composition when extension is intentional.

Interfaces, abstract classes, traits, and composition

Section titled “Interfaces, abstract classes, traits, and composition”

An interface defines substitutable capabilities. An abstract class can combine a contract with shared state, implementation, and protected hooks, coupling descendants to one inheritance hierarchy. A trait copies members into the consuming class during composition; it is code reuse, not a runtime collaborator or a type callers can depend on.

Trait conflict operators resolve naming collisions, but collision resolution does not solve semantic conflict. Traits can also hide dependencies through assumed properties or methods. Small, stateless traits for genuinely mechanical behavior can be useful; collaborating services are usually clearer as constructor dependencies because their lifecycle and replacement are visible.

Inheritance is strongest for a stable “is usable as” relationship with a deliberately designed base contract. Reusing two methods is not enough justification. Composition lets a class delegate a capability without inheriting unrelated state or protected implementation, and it avoids the fragile-base-class problem.

final class prevents subclassing; final methods prevent overriding while allowing other extension. Finality can protect construction, equality, security checks, or state transitions whose invariants would be broken by partial override. It also makes the supported extension surface honest: callers depend on interfaces while implementations remain closed.

Final is not automatically better design. Frameworks, testing approaches, and product extension requirements may rely on subclassing. A class that must be replaced can implement an interface; a class designed for inheritance should document protected hooks and test substitutability. Removing unintended inheritance later can be a breaking change, so decide deliberately at public package boundaries.

Magic methods intercept defined engine operations: inaccessible property access, unknown method calls, invocation, string conversion, serialization, debugging, cloning, construction, and destruction. Except for construction, destruction, and cloning, magic methods must be public and must use their specified signatures.

They are powerful at narrow boundaries—proxies, value serialization, callable objects—but can erase contracts. __get() and __call() make typos look like dynamic behavior, weaken static analysis, and can hide I/O behind property syntax. Active Record models intentionally accept some of that trade-off; application services usually should not.

Prefer __serialize() and __unserialize() over legacy __sleep() and __wakeup() for controlled serialization. Never unserialize untrusted bytes merely because a class validates in __unserialize(); object injection can instantiate gadget graphs before application authorization. PHP 8.5 soft-deprecates the older sleep/wakeup hooks, which is a migration signal rather than permission for unsafe native serialization.

Destructors run when an object is destroyed or during shutdown, but their timing depends on reachability, cycles, and process termination. They must not own business commits, guaranteed delivery, or essential lock release. Use explicit lifecycle methods and try/finally for correctness-critical cleanup.

  • A “copied” request context leaks mutation because only an object handle was assigned.
  • A cloned aggregate shares a mutable nested collection and changes the original.
  • new static() reaches a child constructor with an incompatible signature.
  • A trait reads undeclared host-class state, making reuse order-dependent.
  • A proxy’s __get() triggers database I/O in serialization and causes an N+1 query pattern.
  • A destructor performs network work that disappears on fatal termination or forced worker exit.

Diagnose these by establishing instance identity, graph sharing, runtime class, call scope, and the exact engine hook invoked before changing abstractions.

  • Current: Prefer typed properties, explicit interfaces, intentional finality, __serialize(), and named copy operations. PHP 8.5 clone-with syntax can simplify immutable copies.
  • Common: Traits, Active Record magic properties, conventional __clone(), and inheritance-heavy library APIs remain normal in PHP 8.2–8.4 applications.
  • Legacy: Dynamic properties, native serialized object graphs, __sleep()/__wakeup(), and deep hierarchies deserve migration scrutiny. Characterize behavior before replacing magic with explicit APIs.