Cloning, Late Static Binding and Object Lifecycle

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05. It covers the life of an object after new: copying it, referring to it, resolving static at runtime, iterating it, referring to it weakly, and creating it lazily. Construction itself is covered in Classes and Objects.

clone and __clone

clone $object creates a shallow copy: a new object of the same class whose properties have the same values. For scalar and array properties, that is a real copy (arrays are values); for object properties, both copies share the same inner objects, because only the handle is copied. If the class defines __clone(), PHP calls it on the new object after copying, which is where deep copies are made.

<?php

declare(strict_types=1);

final class Reservation
{
    public array $members = [];

    public function __construct(public ArrayObject $notes, public DateTime $placedAt) {}

    public function __clone(): void
    {
        $this->notes = clone $this->notes;          // deep-copy mutable objects
        $this->placedAt = clone $this->placedAt;
    }
}

$a = new Reservation(new ArrayObject(['first']), new DateTime('2026-10-05'));
$a->members[] = 'M-17';

$b = clone $a;
$b->members[] = 'M-18';           // arrays: always independent
$b->notes[] = 'second';           // independent thanks to __clone
$b->placedAt->modify('+1 day');   // independent thanks to __clone

echo count($a->members), count($b->members), ' ', count($a->notes), count($b->notes), ' ',
    $a->placedAt->format('d'), $b->placedAt->format('d'), "\n";    // 12 12 0506

Without __clone(), $b->notes[] = 'second' would also appear in $a->notes. Using immutable inner types (DateTimeImmutable, readonly value objects) removes the need for most deep copies. Since PHP 8.3 __clone() may re-initialise readonly properties, which made deep-cloning readonly objects possible.

Source: Object Cloning

clone() with properties (PHP 8.5)

Since PHP 8.5, clone is also usable as a function that takes an array of property values to set on the copy: clone($object, ['prop' => $value]). The properties are assigned after __clone() runs, from the scope of the code calling clone(), and visibility rules apply: a readonly property (implicitly protected(set)) can therefore only be changed this way from inside the class. This is exactly what immutable "wither" methods need:

<?php

declare(strict_types=1);

final readonly class Loan
{
    public function __construct(
        public string $isbn,
        public DateTimeImmutable $due,
        public int $renewals = 0,
    ) {}

    public function renew(int $days = 14): static
    {
        return clone($this, [
            'due' => $this->due->modify("+{$days} days"),
            'renewals' => $this->renewals + 1,
        ]);
    }
}

$loan = new Loan('978-0-00-000001-1', new DateTimeImmutable('2026-10-26'));
$renewed = $loan->renew();
echo $loan->renewals, ' ', $renewed->renewals, ' ', $renewed->due->format('Y-m-d'), "\n";   // 0 1 2026-11-09

try {
    clone($loan, ['renewals' => 99]);        // from outside the class
} catch (Error $e) {
    echo $e->getMessage(), "\n";             // Cannot modify protected(set) readonly property Loan::$renewals from global scope
}

Before 8.5 the same wither needed either a non-readonly property, new self(...) repeating every argument, or a __clone() trick with a temporary property.

Objects and references

PHP objects are not passed "by reference" in the PHP sense (&); a variable holds an object handle, and copying the variable copies the handle. The difference matters when you reassign: assigning a new object to a parameter does not affect the caller, while & would.

<?php

declare(strict_types=1);

final class Shelf { public function __construct(public string $code) {} }

function relabel(Shelf $shelf): void { $shelf->code = 'B'; }          // changes the shared object
function replace(Shelf $shelf): void { $shelf = new Shelf('C'); }     // rebinds the local variable only
function replaceByRef(Shelf &$shelf): void { $shelf = new Shelf('D'); }   // rebinds the caller's variable

$s = new Shelf('A');
relabel($s);      echo $s->code;   // B
replace($s);      echo $s->code;   // B
replaceByRef($s); echo $s->code;   // D
echo "\n";

Late static binding

self:: always refers to the class in which the code is written. static:: refers to the class that was called at runtime, which is called late static binding (5.3). It makes static factories and fluent interfaces work with inheritance: new static() creates an instance of the subclass, and the static return type (8.0) declares it.

<?php

declare(strict_types=1);

abstract class Model
{
    protected static string $table = 'models';

    public static function create(): static           // static return type
    {
        return new static();                            // the called class, not Model
    }

    public static function describe(): string
    {
        return sprintf('%s uses table %s (self is %s)', static::class, static::$table, self::class);
    }
}

final class Book extends Model
{
    protected static string $table = 'books';
}

$book = Book::create();
echo get_class($book), "\n";            // Book
echo Book::describe(), "\n";            // Book uses table books (self is Model)

Laravel’s Eloquent relies on this: Book::query(), Book::create([...]) and Book::find(1) are declared once on Model and return Book instances. parent:: and self:: forward the late static binding information, while ClassName:: resets it.

Object iteration

foreach over an object iterates its visible properties: public ones from outside the class, all of them from inside. Implementing Iterator or IteratorAggregate replaces this default with your own logic (see Iterators and Generators).

<?php

declare(strict_types=1);

final class Book
{
    public string $title = 'Kindred';
    protected int $year = 1979;
    private string $isbn = '978-0-00-000001-1';

    public function fields(): array
    {
        $out = [];
        foreach ($this as $name => $value) {      // inside: every property
            $out[] = $name;
        }
        return $out;
    }
}

$b = new Book();
foreach ($b as $name => $value) {                 // outside: public only
    echo "{$name}={$value}\n";                    // title=Kindred
}
echo implode(', ', $b->fields()), "\n";           // title, year, isbn

Destruction, WeakReference and WeakMap

An object is destroyed when its reference count drops to zero, or later by the cycle collector if it is part of a reference cycle (see References and Memory Management); its destructor runs then. Two classes let code refer to objects without keeping them alive:

  • WeakReference (7.4) holds a reference that does not prevent destruction; ->get() returns the object or null.

  • WeakMap (8.0) is a map with objects as keys whose entries disappear when the key object is destroyed — the right structure for caches and metadata attached to objects you do not own.

<?php

declare(strict_types=1);

final class Book { public function __construct(public string $title) {} }

$book = new Book('Kindred');
$weak = WeakReference::create($book);
var_dump($weak->get()?->title);      // string(7) "Kindred"
unset($book);
var_dump($weak->get());              // NULL -- the object was destroyed

// A per-object cache that does not leak
$wordCounts = new WeakMap();
$b1 = new Book('The Left Hand of Darkness');
$wordCounts[$b1] ??= str_word_count($b1->title);
var_dump(count($wordCounts), $wordCounts[$b1]);   // int(1) int(5)
unset($b1);
var_dump(count($wordCounts));                     // int(0) -- the entry went away with the object

SplObjectStorage also maps objects to data, but holds strong references and must be cleaned up by hand.

Source: WeakReference and WeakMap

Lazy objects (PHP 8.4)

A lazy object is created without running its initialisation; initialisation happens automatically the first time its state is accessed. PHP 8.4 added two kinds, created through Reflection:

  • a lazy ghost (ReflectionClass::newLazyGhost($initializer)) is an instance of the class itself; the initializer receives the object and fills it in place (usually by calling its constructor);

  • a lazy proxy (ReflectionClass::newLazyProxy($factory)) forwards every access to a real instance returned by the factory on first use.

Lazy objects are mainly a tool for frameworks: dependency-injection containers create expensive services lazily, and ORMs return lazy entities that load from the database on first access. Before 8.4 this required generating proxy classes in userland.

<?php

declare(strict_types=1);

final class Catalogue
{
    public array $titles;

    public function __construct()
    {
        echo "loading the catalogue...\n";        // imagine a slow database query
        $this->titles = ['Solaris', 'Kindred', 'Dawn'];
    }
}

$reflector = new ReflectionClass(Catalogue::class);

$ghost = $reflector->newLazyGhost(function (Catalogue $catalogue): void {
    $catalogue->__construct();                     // initialise in place
});

echo "ghost created\n";
var_dump($reflector->isUninitializedLazyObject($ghost));   // bool(true)
echo count($ghost->titles), " titles\n";                    // triggers "loading the catalogue..."
var_dump($reflector->isUninitializedLazyObject($ghost));   // bool(false)

$proxy = $reflector->newLazyProxy(fn (Catalogue $p): Catalogue => new Catalogue());
echo "proxy created\n";
echo $proxy->titles[0], "\n";                               // loads, then forwards to the real instance
ghost created
bool(true)
loading the catalogue...
3 titles
bool(false)
proxy created
loading the catalogue...
Solaris

Reflection also offers initializeLazyObject(), markLazyObjectAsInitialized(), resetAsLazyGhost()/ resetAsLazyProxy() and, per property, skipLazyInitialization() and setRawValueWithoutLazyInitialization(). Operations that do not touch state, such as instanceof, get_class() and spl_object_id(), do not trigger initialisation.

Where older books and tutorials are outdated

  • "Objects are passed by reference" (a common simplification in PHP 5-era books) is not accurate: handles are passed by value; & is a different mechanism.

  • Readonly deep cloning (8.3), clone() with properties (8.5) and lazy objects (8.4) are absent from older books, which rely on hand-written withers and generated proxy classes.

  • WeakMap (8.0) and WeakReference (7.4) did not exist; caches keyed by spl_object_hash() leaked memory and could even return entries for a new object that reused the hash of a destroyed one.

  • The static return type (8.0) was written as @return static PHPDoc.