-
Notifications
You must be signed in to change notification settings - Fork 43
Description
Description
v2.0.9 introduced a regression where closures nested inside objects that implement __serialize are no longer wrapped by Native::wrapClosures() and Native::mapByReference(), causing Serialization of 'Closure' is not allowed errors.
This was introduced by #122 which added || $reflection->hasMethod('__serialize') to skip walking object properties when the class implements __serialize. While the original fix correctly addressed the TypeError: Cannot assign Native to property of type Closure issue, it also prevents the library from finding and wrapping closures that live inside those objects (e.g. in array properties or untyped properties).
Reproduction
<?php
require __DIR__ . '/vendor/autoload.php';
use Laravel\SerializableClosure\SerializableClosure;
// Object with __serialize that has closures in an array property
class TaskRunner {
public string $taskName = 'daily-report';
/** @var \Closure[] */
public array $filters = [];
public function __serialize(): array
{
return [
'taskName' => $this->taskName,
'filters' => $this->filters,
];
}
public function __unserialize(array $data): void
{
$this->taskName = $data['taskName'];
$this->filters = $data['filters'];
}
public function createTask(): \Closure
{
$this->filters[] = function () { return true; };
return function () {
return "running {$this->taskName}";
};
}
}
$runner = new TaskRunner();
$closure = $runner->createTask();
// PASS on v2.0.8, FAIL on v2.0.9+
$serialized = serialize(new SerializableClosure($closure));
$unserialized = unserialize($serialized);
echo $unserialized->getClosure()();Expected behavior
Closures nested in objects with __serialize (in array properties, untyped properties, etc.) should still be wrapped with Native instances before serialization, as they were in v2.0.8.
Actual behavior (v2.0.9+)
[Exception]
Serialization of 'Closure' is not allowed
Versions tested
| Version | Result |
|---|---|
| v2.0.8 | ✅ PASS |
| v2.0.9 | ❌ FAIL |
| v2.0.10 | ❌ FAIL |
| 2.x-dev | ❌ FAIL |
Tested on PHP 8.5.2.
Root cause
The change in src/Serializers/Native.php (#122):
- if (! $reflection->isUserDefined()) {
+ if (! $reflection->isUserDefined() || $reflection->hasMethod('__serialize')) {
$storage[$instance] = $data;
return;
}This skips the entire object when it has __serialize, so closures inside it are never replaced with serializable Native wrappers. When PHP later serializes the object via __serialize(), those raw \Closure values pass through and hit PHP's native "Serialization of 'Closure' is not allowed" error.
Suggested fix
Instead of skipping the entire object, call __serialize() to get the data array, then walk that array for closures. This avoids the TypeError on typed Closure properties (the original #122 issue) while still wrapping nested closures:
if ($reflection->hasMethod('__serialize')) {
$storage[$instance] = $data;
// Walk the __serialize output for nested closures
$serializeData = $instance->__serialize();
static::wrapClosures($serializeData, $storage);
return;
}(The exact implementation may need more nuance around re-injecting the wrapped data, but the principle is: don't skip closure discovery just because the object handles its own serialization format.)
Impact
This breaks any library using SerializableClosure where the serialized closure captures or is bound to an object implementing __serialize that contains closures. Known affected: crunzphp/crunz#122.