Clonación de objetos en PHP: clone, __clone y la diferencia entre copia superficial y profunda

Cuando asignas un objeto a una variable en PHP, no obtienes una copia: obtienes otro puntero al mismo objeto en memoria. Para duplicar un objeto necesitas la palabra clave clone. Pero hay una trampa: clone hace una copia superficial (shallow copy), lo que significa que las propiedades que contienen otros objetos siguen apuntando al mismo objeto original.

Asignación vs clone: la diferencia fundamental

<?php
class Contador {
    public int $valor = 0;
}

$a = new Contador();
$b = $a;          // $b es el mismo objeto, no una copia
$b->valor = 99;
echo $a->valor;   // 99 — ¡sorpresa! $a también cambió

$c = clone $a;    // $c es una copia independiente
$c->valor = 0;
echo $a->valor;   // 99 — $a no se ha visto afectado

Copia superficial (shallow copy): el bug típico

El problema con clone aparece cuando el objeto tiene propiedades que son a su vez objetos:

<?php
class Direccion {
    public function __construct(
        public string $calle,
        public string $ciudad
    ) {}
}

class Usuario {
    public function __construct(
        public string    $nombre,
        public Direccion $direccion
    ) {}
}

$u1 = new Usuario('Ana', new Direccion('Gran Vía 1', 'Madrid'));
$u2 = clone $u1;

// $u2->nombre es una copia independiente (string es inmutable)
$u2->nombre = 'Luis';
echo $u1->nombre; // Ana — correcto

// PERO $u2->direccion apunta al mismo objeto Direccion
$u2->direccion->ciudad = 'Barcelona';
echo $u1->direccion->ciudad; // Barcelona — ¡el original cambió!

__clone(): copia profunda (deep copy)

El método mágico __clone() se ejecuta automáticamente cuando se usa clone en el objeto. Úsalo para clonar también las propiedades que son objetos:

<?php
class Direccion {
    public function __construct(
        public string $calle,
        public string $ciudad
    ) {}
}

class Usuario {
    public function __construct(
        public string    $nombre,
        public Direccion $direccion
    ) {}

    public function __clone(): void {
        // Clonamos el objeto anidado para obtener una copia real
        $this->direccion = clone $this->direccion;
    }
}

$u1 = new Usuario('Ana', new Direccion('Gran Vía 1', 'Madrid'));
$u2 = clone $u1;

$u2->nombre = 'Luis';
$u2->direccion->ciudad = 'Barcelona';

echo $u1->nombre;            // Ana
echo $u1->direccion->ciudad; // Madrid — ahora sí es independiente
echo $u2->nombre;            // Luis
echo $u2->direccion->ciudad; // Barcelona

Arrays dentro de objetos

Los arrays en PHP siempre se copian por valor, así que clone los maneja correctamente sin necesitar __clone(). El problema es solo con propiedades que son objetos.

<?php
class Carrito {
    public function __construct(
        public array  $items = [],
        public ?object $cupon = null
    ) {}

    public function __clone(): void {
        // El array $items se copia automáticamente
        // Pero el objeto $cupon necesita clonarse manualmente
        if ($this->cupon !== null) {
            $this->cupon = clone $this->cupon;
        }
    }
}

Comparación de objetos: == vs ===

PHP tiene dos operadores de comparación para objetos con comportamientos diferentes:

<?php
class Punto {
    public function __construct(
        public int $x,
        public int $y
    ) {}
}

$p1 = new Punto(3, 4);
$p2 = new Punto(3, 4);
$p3 = clone $p1;
$p4 = $p1;

var_dump($p1 == $p2);   // true — misma clase y mismos valores
var_dump($p1 === $p2);  // false — son instancias distintas

var_dump($p1 == $p3);   // true — misma clase y mismos valores (clone)
var_dump($p1 === $p3);  // false — son instancias distintas (clone crea una nueva)

var_dump($p1 == $p4);   // true
var_dump($p1 === $p4);  // true — apuntan al mismo objeto
  • ==: dos objetos son iguales si son de la misma clase y tienen las mismas propiedades con los mismos valores.
  • ===: solo es verdadero si ambas variables apuntan exactamente a la misma instancia en memoria.

Cuándo usar clone

Usa clone cuando necesitas una copia del estado actual de un objeto para modificarla sin afectar el original. Es especialmente útil con objetos de valor inmutables en un patrón «wither» donde cada mutación devuelve un nuevo objeto con la propiedad cambiada:

<?php
final class Precio {
    public function __construct(
        public readonly float  $importe,
        public readonly string $moneda
    ) {}

    public function conDescuento(float $porcentaje): self {
        $copia = clone $this;
        // readonly en PHP 8.1 no permite reasignación desde fuera,
        // pero sí dentro de __clone o en el mismo objeto
        // Para este patrón, mejor usar propiedades no-readonly con constructor privado
        return new self($this->importe * (1 - $porcentaje / 100), $this->moneda);
    }
}

$original   = new Precio(100.0, 'EUR');
$conOferta  = $original->conDescuento(20);
echo $original->importe;   // 100.0
echo $conOferta->importe;  // 80.0

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP