10min.

PHP Clone All The Things

Cet article est aussi disponible en 🇫🇷 Français : La guerre des clones PHP.

Every time I see clone in PHP code, I can’t help but be a little scared. This simple keyword, although very clear, always puts me in doubt.

What exactly is going to be cloned?

First, the question brings me back to the basics of PHP: passing variables by value or by reference. Then, to the complexity of the cloned object: its depth levels and other magic methods of PHP.

In short, nothing reassuring, a pity as I was on a basic code review.

Today, for me, and for you too, we’ll try to find out how to approach a clone without fear next time.

Section intitulée refresherRefresher

According to the documentation, clone will make a shallow copy.

When an object is cloned, PHP will perform a shallow copy of all of the object’s properties. Any properties that are references to other variables will remain references. official documentation

This is where double-checking our basics seems important to me, especially since the vocabulary used by PHP to talk about variable passing by values or references can be confusing.

Indeed, if we look for documentation on what a reference is in PHP, we come across this page: References. Unfortunately, the word “reference” seems to be a bad choice for me, because that word often has another meaning in other programming languages.

A reference in PHP is not a pointer to a memory address, we can rather see it as an alias to the same object. The PHP documentation explains this several times, on the page mentioned above, but also here: Objects and references.

If you want to go into more detail, I recommend reading this chapter: memory management from the PHP Internals.

In fact, clone will make a copy of the requested object by copying the references as such and not the referenced object. Hence, the shallow copy.

So the question is, is this really what you want when you use clone? Probably not.

function objectClassId(mixed $object): string {
    return get_class($object) . ' #' . spl_object_id($object);
}

class Birthday {
    public function __construct(
        // These ints are values
        public int $year,
        public int $month,
        public int $day
    ) {
        echo objectClassId($this) . ' __construct called' . PHP_EOL;
    }

    public function __toString(): string {
        return sprintf('%d/%02d/%02d', $this->year, $this->month, $this->day);
    }
}

class Person {
    public function __construct(
        // This string is a value
        public string $name,
        // This Birthday object is a "reference"
        public Birthday $birthday
    ) {
        echo objectClassId($this) . ' __construct called' . PHP_EOL;
    }
}

$aliceBirthday = new Birthday(1985, 9, 21);
$alice = new Person('Alice', $aliceBirthday);
echo $alice->name . PHP_EOL;
echo $alice->birthday . PHP_EOL;
echo "Alice Birthday object hash: " . objectClassId($alice->birthday) . PHP_EOL;
echo PHP_EOL;

$dolly = clone $alice;
echo "Dolly's name is Alice as expected:" . PHP_EOL;
echo $dolly->name . PHP_EOL;
echo "Dolly's birthday representation is 1985/09/21 as expected:" . PHP_EOL;
echo $dolly->birthday . PHP_EOL;
echo "Dolly's birthday object is the same one as Alice, this can lead to errors:" . PHP_EOL;
echo "Dolly Birthday object hash: " . objectClassId($dolly->birthday) . PHP_EOL;
echo PHP_EOL;

echo "Let's say Dolly can now have a life of their own:" . PHP_EOL;
$dolly->name = 'Dolly';
$dolly->birthday->year = 1996;
$dolly->birthday->month = 7;
$dolly->birthday->day = 5;
echo $dolly->name . PHP_EOL;
echo $dolly->birthday . PHP_EOL;
echo PHP_EOL;

echo "The problem is that we changed the birthday of Alice too:" . PHP_EOL;
echo $alice->name . PHP_EOL;
echo $alice->birthday . PHP_EOL;
echo PHP_EOL;
Birthday #1 __construct called
Person #2 __construct called
Alice
1985/09/21
Alice Birthday object hash: Birthday #1

Dolly's name is Alice as expected:
Alice
Dolly's birthday representation is 1985/09/21 as expected:
1985/09/21
Dolly's birthday object is the same one as Alice, this can lead to errors:
Dolly Birthday object hash: Birthday #1

Let's say Dolly can now have a life of their own:
Dolly
1996/07/05

The problem is that we changed the birthday of Alice too:
Alice
1996/07/05

I don’t know about you, but when I read “clone”, I’d like a new and fresh copied object, and I don’t imagine that there will be any side effects possible.

That’s why I personally don’t like this keyword that much, because I don’t think it reflects its meaning.

Section intitulée php-s-magicPHP’s magic

As you can imagine, there is a solution to this problem. Indeed, PHP has the magic methods.

Note: the most used of these __construct() is not called when cloning an object.

Let’s check the __clone() one. This magic method is called once the clone is ready. It can be seen as a constructor for the clone. It is an opportunity for the person who maintains a class to do what is necessary for the objects referenced by their class. Unfortunately this implementation is hidden, so as the user, we have to learn about the API of the class and check the behavior of the object during the cloning.

This is mainly why I don’t like to come across this keyword.

class Birthday {
    public function __construct(
        // These ints are values
        public int $year,
        public int $month,
        public int $day
    ) {
        echo objectClassId($this) . ' __construct called' . PHP_EOL;
    }

    public function __toString(): string {
        return sprintf('%d/%02d/%02d', $this->year, $this->month, $this->day);
    }
}

class Person {
    public function __construct(
        // This string is a value
        public string $name,
        // This Birthday object is a "reference"
        public Birthday $birthday
    ) {
        echo objectClassId($this) . ' __construct called' . PHP_EOL;
    }

    /**
     * As the maintainer of this class/API I took care of that
     */
    public function __clone(): void {
        // Most of the time, cloning manually the referred object will do
        $this->birthday = clone $this->birthday;
    }
}

$aliceBirthday = new Birthday(1985, 9, 21);
$alice = new Person('Alice', $aliceBirthday);
echo $alice->name . PHP_EOL;
echo $alice->birthday . PHP_EOL;
echo "Alice Birthday object hash: " . objectClassId($alice->birthday) . PHP_EOL;
echo PHP_EOL;

// As the user of this class/API I must check what is the behavior of it when cloned
$dolly = clone $alice;
echo "Dolly's name is Alice as expected:" . PHP_EOL;
echo $dolly->name . PHP_EOL;
echo "Dolly's birthday representation is 1985/09/21 as expected:" . PHP_EOL;
echo $dolly->birthday . PHP_EOL;
echo "Dolly's birthday object is not the same one as Alice anymore:" . PHP_EOL;
echo "Dolly Birthday object hash: " . objectClassId($dolly->birthday) . PHP_EOL;
echo PHP_EOL;

echo "Let's say Dolly can now have a life of their own:" . PHP_EOL;
$dolly->name = 'Dolly';
$dolly->birthday->year = 1996;
$dolly->birthday->month = 7;
$dolly->birthday->day = 5;
echo $dolly->name . PHP_EOL;
echo $dolly->birthday . PHP_EOL;
echo PHP_EOL;

echo "The problem is now fixed, Dolly and Alice have two different Birthday objects:" . PHP_EOL;
echo $alice->name . PHP_EOL;
echo $alice->birthday . PHP_EOL;
echo PHP_EOL;
Birthday #1 __construct called
Person #2 __construct called
Alice
1985/09/21
Alice Birthday object hash: Birthday #1

Dolly's name is Alice as expected:
Alice
Dolly's birthday representation is 1985/09/21 as expected:
1985/09/21
Dolly's birthday object is not the same one as Alice anymore:
Dolly Birthday object hash: Birthday #4

Let's say Dolly can now have a life of their own:
Dolly
1996/07/05

The problem is now fixed, Dolly and Alice have two different Birthday objects:
Alice
1985/09/21

I think there are many issues with this.

First is, in my opinion, the fact that the cloning implementation is hidden from the user. I guess this can be acceptable and can be seen as an implementation detail, so the user doesn’t have to worry about it, but personally I like to know what is going on.

Once __clone is defined, then everything seems fine!?

Well no, the naive approach of cloning the objects referenced in our __clone method is limited. Indeed, if several objects reference the same object, cloning the parent will create as many clones than references.

Ok, I lost you, I admit that I am quite lost myself too. A small example speaks more than a thousand words.

class Skin {
    public function __construct(
        // This string is a value
        public string $color
    ) {
        echo objectClassId($this) . ' __construct called' . PHP_EOL;
    }

    public function __toString(): string {
        return 'Skin color: ' . $color;
    }
}

class Arm {
    public function __construct(
        // This Skin object is a "reference"
        public Skin $skin
    ) {
        echo objectClassId($this) . ' __construct called' . PHP_EOL;
    }

    /**
     * As the maintainer of this class/API I took care of that
     */
    public function __clone(): void {
        // Most of the time, cloning manually the referred object will do
        $this->skin = clone $this->skin;
    }
}

class Person {
    public function __construct(
        // This string is a value
        public string $name,
        // Theses objects are "references"
        public Arm $leftArm,
        public Arm $rightArm,
    ) {
        echo objectClassId($this) . ' __construct called' . PHP_EOL;
    }

    /**
     * As the maintainer of this class/API I took care of that
     */
    public function __clone(): void {
        // Most of the time, cloning manually the referred object will do
        $this->leftArm = clone $this->leftArm;
        $this->rightArm = clone $this->rightArm;
    }
}

$aliceSkin = new Skin('Blue');
$aliceLeftArm = new Arm($aliceSkin);
$aliceRightArm = new Arm($aliceSkin);
$alice = new Person('Alice', $aliceLeftArm, $aliceRightArm);
echo $alice->name . PHP_EOL;
echo "Alice left arm skin object hash: " . objectClassId($alice->leftArm->skin) . PHP_EOL;
echo "Alice right arm skin object hash: " . objectClassId($alice->rightArm->skin) . PHP_EOL;
echo PHP_EOL;

// As the user of this class/API I must check what is the behavior of it when cloned
$dolly = clone $alice;
echo "Dolly's name is Alice as expected:" . PHP_EOL;
echo $dolly->name . PHP_EOL;
echo "Dolly skin must be two equal objects but different from Alice " .
     "but they are not, our __clone logic is too dumb for that purpose:" . PHP_EOL;
echo "Dolly left arm skin object hash: " . objectClassId($dolly->leftArm->skin) . PHP_EOL;
echo "Dolly right arm skin object hash: " . objectClassId($dolly->rightArm->skin) . PHP_EOL;
echo PHP_EOL;
Skin #1 __construct called
Arm #2 __construct called
Arm #3 __construct called
Person #4 __construct called
Alice
Alice left arm skin object hash: Skin #1
Alice right arm skin object hash: Skin #1

Dolly's name is Alice as expected:
Alice
Dolly skin must be two equal objects but different from Alice but they are not, our __clone logic is too dumb for that purpose:
Dolly left arm skin object hash: Skin #7
Dolly right arm skin object hash: Skin #9
// composer require myclabs/deep-copy
require('vendor/autoload.php');

class Skin {
    public function __construct(
        // This string is a value
        public string $color
    ) {
        echo objectClassId($this) . ' __construct called' . PHP_EOL;
    }

    public function __toString(): string {
        return 'Skin color: ' . $color;
    }
}

class Arm {
    public function __construct(
        // This Skin object is a "reference"
        public Skin $skin
    ) {
        echo objectClassId($this) . ' __construct called' . PHP_EOL;
    }
}

class Person {
    public function __construct(
        // This string is a value
        public string $name,
        // Theses objects are "references"
        public Arm $leftArm,
        public Arm $rightArm,
    ) {
        echo objectClassId($this) . ' __construct called' . PHP_EOL;
    }
}

$aliceSkin = new Skin('Blue');
$aliceLeftArm = new Arm($aliceSkin);
$aliceRightArm = new Arm($aliceSkin);
$alice = new Person('Alice', $aliceLeftArm, $aliceRightArm);
echo $alice->name . PHP_EOL;
echo "Alice left arm skin object hash: " . objectClassId($alice->leftArm->skin) . PHP_EOL;
echo "Alice right arm skin object hash: " . objectClassId($alice->rightArm->skin) . PHP_EOL;
echo PHP_EOL;

$copier = new \DeepCopy\DeepCopy();
$dolly = $copier->copy($alice);

echo "Dolly's name is Alice as expected:" . PHP_EOL;
echo $dolly->name . PHP_EOL;
echo "Dolly skin must be two equal objects but different from Alice:" . PHP_EOL;
echo "Dolly left arm skin object hash: " . objectClassId($dolly->leftArm->skin) . PHP_EOL;
echo "Dolly right arm skin object hash: " . objectClassId($dolly->rightArm->skin) . PHP_EOL;
echo PHP_EOL;
Skin #3 __construct called
Arm #2 __construct called
Arm #4 __construct called
Person #5 __construct called
Alice
Alice left arm skin object hash: Skin #3
Alice right arm skin object hash: Skin #3

Dolly's name is Alice as expected:
Alice
Dolly skin must be two equal objects but different from Alice:
Dolly left arm skin object hash: Skin #22
Dolly right arm skin object hash: Skin #22

Section intitulée to-clone-or-not-to-cloneTo clone or not to clone?

Obviously, it is not that simple,

As the maintainer of the class you can judge if it is a pretty basic one, containing only variables passed by values and no references, then cloning is the most elegant way to get a copy of the object. Still, I would take care to implement and comment the __clone() method. As a user of the API, I would add a comment when using the clone keyword to reassure the person doing the proofreading and especially my future self.

Regarding this point, as an improvement for IDEs, I suggest that the __clone() PHPDoc should show on the related clone keyword for the given class, it would be a nice way to expose the class’ clone behavior and make it explicit.

We have an example of this type of clone in the Symfony code on a simple object such as Component/String/ByteString.php#L220.

Here, clone is used without problem because the object itself only contains String and Boolean variables.

When it is necessary to make a full copy of a complex object, then I will use a library that takes care of it. For example, it is possible to use github.com/myclabs/DeepCopy, a library specialized in this task. It takes care of the details and avoids the concerns raised in this article.

It is also possible and sometimes simpler to rebuild the object from scratch, it is certainly more lines, but at least it is very explicit.

In any case I invite you to avoid the simplistic solution that we can find everywhere on the internet:

function deep_clone($object) {
    return unserialize(serialize($object));
}

With this implementation, the performances are not good, but the security can also be a problem if your objects contain user generated data.

Section intitulée conclusionConclusion

While PHP seems to have what is needed to clone an object, we have seen that it is not as simple as it seems. As always, there are several solutions and it is important to use the right tool for the given situation.

My personal approach would be to question the use of the clone in the given context, I can’t see that many practical cases. I hope this blog post has reassured you, there is no need to be afraid of cloning objects in PHP, but you should have a look at its implementation. It is a mandatory step to understand the clone that will be returned to you.

Happy cloning!

Commentaires et discussions

Nos articles sur le même sujet

Ces clients ont profité de notre expertise