Description
While migrating TYPO3 Extbase lazy loading to native lazy objects (https://forge.typo3.org/issues/110347) I noticed serialize() doesn't unify a lazy proxy with its real instance:
class Entity {
public int $value = 42;
}
$r = new ReflectionClass(Entity::class);
$real = new Entity();
$proxy = $r->newLazyProxy(fn (): Entity => $real);
$restored = unserialize(serialize([$proxy, $real]));
var_dump($restored[0] === $restored[1]); // false
payload is a:2:{i:0;O:6:"Entity":1:{s:5:"value";i:42;}i:1;O:6:"Entity":1:{s:5:"value";i:42;}} - two full copies, no r: reference. Doing the same with [$real, $real] gives r:2 and identity survives unserialize. Same result when the proxy was already initialized before calling serialize.
Gets worse with cycles, every proxy embeds another full copy of the graph:
class Post { public array $comments = []; }
class Comment { public ?Post $post = null; }
$r = new ReflectionClass(Post::class);
$post = new Post();
for ($i = 0; $i < 3; $i++) {
$c = new Comment();
$c->post = $r->newLazyProxy(fn (): Post => $post);
$post->comments[] = $c;
}
$restored = unserialize(serialize($post));
var_dump($restored->comments[0]->post === $restored); // false
var_dump($restored->comments[0]->post === $restored->comments[1]->post); // also false!
so after the roundtrip every comment points to a different Post copy, none of them is the root.
In our real object graphs in TYPO3 (extbase entities with lazy collections and lazy proxies) this even produced payloads that unserialize() rejects completely with unserialize(): Error at offset 12869 of 62854 bytes - serialize emitted a r:249 at a point where the value counter was only at 246. Cant reproduce that part in a small script yet, happy to provide the bigger repro if needed.
Ghosts are fine btw, only proxies are affected.
Workaround we use now in TYPO3 core: implement __serialize() and initialize lazy objects there explicitly via ReflectionClass::initializeLazyObject(), so the engine serializer never runs into an unintialized lazy object itself.
PHP Version
tested on 8.4, 8.5.5 and 8.5.7, all same behaviour
Operating System
macOS + Linux
Description
While migrating TYPO3 Extbase lazy loading to native lazy objects (https://forge.typo3.org/issues/110347) I noticed serialize() doesn't unify a lazy proxy with its real instance:
payload is
a:2:{i:0;O:6:"Entity":1:{s:5:"value";i:42;}i:1;O:6:"Entity":1:{s:5:"value";i:42;}}- two full copies, no r: reference. Doing the same with[$real, $real]givesr:2and identity survives unserialize. Same result when the proxy was already initialized before calling serialize.Gets worse with cycles, every proxy embeds another full copy of the graph:
so after the roundtrip every comment points to a different Post copy, none of them is the root.
In our real object graphs in TYPO3 (extbase entities with lazy collections and lazy proxies) this even produced payloads that unserialize() rejects completely with
unserialize(): Error at offset 12869 of 62854 bytes- serialize emitted ar:249at a point where the value counter was only at 246. Cant reproduce that part in a small script yet, happy to provide the bigger repro if needed.Ghosts are fine btw, only proxies are affected.
Workaround we use now in TYPO3 core: implement __serialize() and initialize lazy objects there explicitly via ReflectionClass::initializeLazyObject(), so the engine serializer never runs into an unintialized lazy object itself.
PHP Version
tested on 8.4, 8.5.5 and 8.5.7, all same behaviour
Operating System
macOS + Linux