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
