Skip to content

Cloning an object breaks serialization recursion #12265

Open
@azuradara

Description

@azuradara

Description

Overview

Starting from 8.1.0 cloning recursive objects inline when calling serialize() breaks the serialization recursion. For example:

serialize(clone $obj);

However, assigning the clone to a variable and then passing it to the serialize() function will not break.

$clone = clone $obj;

serialize($clone);

Additionally, there is some undefined behavior related to defining an "instance" property on the object and assigning the object's instance to it within the __clone() magic method which somehow fixes everything. Not sure about this part, but this is the only way I got it to work consistently in my usecase.

Here's a 3v4l link: https://3v4l.org/6FKnp#v810

Reproduction

The following code:

Click to expand codeblock
<?php

class A {
    public B|C $x;
    
    public function __construct(B|C $x) {
        $this->x = $x;
    }
    
    public function __sleep()
    {
        return ['x'];
    }
}

class B {
    public A $a;
    
    public function __serialize()
    {
        return ['a' => new A($this)];
    }
}

class C {
    public A $a;
    private self $instance;
    
    public function __serialize()
    {
        return ['a' => new A($this)];
    }
    
    public function __clone() {
        $this->instance = $this;
    }
}

// instantiate
$b = new B();
$c = new C();

// clone and assign
$ib = clone $b;
$ic = clone $c;

// serialize
$sb = serialize($b);
$scb = serialize(clone $b);
$sib = serialize($ib);

$sc = serialize($c);
$scc = serialize(clone $c);
$sic = serialize($ic);

// unserialize
$ub = unserialize($sb);
$ucb = unserialize($scb);
$uib = unserialize($sib);

$uc = unserialize($sc);
$ucc = unserialize($scc);
$uic = unserialize($sic);


// expected behavior is b == b->a->x
printf("original ids:          b:%s, b->a:%s, b->a->x:%s \n", spl_object_id($ub), spl_object_id($ub->a), spl_object_id($ub->a->x));
printf("cloned ids:            b:%s, b->a:%s, b->a->x:%s \n", spl_object_id($ucb), spl_object_id($ucb->a), spl_object_id($ucb->a->x));
printf("cloned & assigned ids: b:%s, b->a:%s, b->a->x:%s \n", spl_object_id($uib), spl_object_id($uib->a), spl_object_id($uib->a->x));

printf(PHP_EOL);


// expected behavior is c == c->a->x
printf("original ids:          c:%s, c->a:%s, c->a->x:%s \n", spl_object_id($uc), spl_object_id($uc->a), spl_object_id($uc->a->x));
printf("cloned ids:            c:%s, c->a:%s, c->a->x:%s \n", spl_object_id($ucc), spl_object_id($ucc->a), spl_object_id($ucc->a->x));
printf("cloned & assigned ids: c:%s, c->a:%s, c->a->x:%s \n", spl_object_id($uic), spl_object_id($uic->a), spl_object_id($uic->a->x));

printf(PHP_EOL);

printf("serialized original:    %s\n", $sb);
printf("serialized clone   :    %s\n", $scb);

?>

Outputs this:

original ids:          b:6, b->a:7, b->a->x:6 
cloned ids:            b:8, b->a:9, b->a->x:10 
cloned & assigned ids: b:12, b->a:13, b->a->x:12 

original ids:          c:14, c->a:15, c->a->x:14 
cloned ids:            c:16, c->a:17, c->a->x:16 
cloned & assigned ids: c:18, c->a:19, c->a->x:18 

serialized original:    O:1:"B":1:{s:1:"a";O:1:"A":1:{s:1:"x";r:1;}}
serialized clone   :    O:1:"B":1:{s:1:"a";O:1:"A":1:{s:1:"x";O:1:"B":1:{s:1:"a";O:1:"A":1:{s:1:"x";r:3;}}}}

But I expected this output instead:

original ids:          b:6, b->a:7, b->a->x:6 
cloned ids:            b:8, b->a:9, b->a->x:8 
cloned & assigned ids: b:10, b->a:11, b->a->x:10 

original ids:          c:12, c->a:13, c->a->x:12 
cloned ids:            c:14, c->a:15, c->a->x:14 
cloned & assigned ids: c:16, c->a:17, c->a->x:16 

serialized original:    O:1:"B":1:{s:1:"a";O:1:"A":1:{s:1:"x";r:1;}}
serialized clone   :    O:1:"B":1:{s:1:"a";O:1:"A":1:{s:1:"x";r:1;}}

PHP Version

8.2.0

Operating System

Debian 12

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions