From 9a0b9b47a2d52a24106c3dfb78a418ad86d6b344 Mon Sep 17 00:00:00 2001 From: Ethan Setnik Date: Tue, 24 Feb 2026 11:52:51 -0500 Subject: [PATCH 1/2] fix: limit laravel/serializable-closure to <2.0.9 v2.0.9 introduced a change in Native.php (laravel/serializable-closure#122) that skips walking object properties when the object implements __serialize. This breaks crunz's closure serialization, causing "Serialization of 'Closure' is not allowed" errors in LaravelClosureSerializer::serialize(). Constrains the dependency to >=2.0 <2.0.9 until a compatible fix is available. Fixes crunzphp/crunz#122 Co-authored-by: Cursor --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index cded361..b4a6bf1 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "php": ">=8.2", "composer-runtime-api": "^2.0", "dragonmantank/cron-expression": "^3.4.0", - "laravel/serializable-closure": "^2.0", + "laravel/serializable-closure": ">=2.0 <2.0.9", "psr/log": "^2.0 || ^3.0", "symfony/config": "^6.4.25 || ^7.4.0 || ^8.0.0", "symfony/console": "^6.4.25 || ^7.4.0 || ^8.0.0", From 02330af23dfb693036ae241f01eaaac06ca08a8d Mon Sep 17 00:00:00 2001 From: Ethan Setnik Date: Tue, 24 Feb 2026 12:07:32 -0500 Subject: [PATCH 2/2] test: add serialization round-trip tests for LaravelClosureSerializer Adds test coverage for closure serialization including a regression test for laravel/serializable-closure#126 where objects implementing __serialize with nested closure properties fail to serialize on v2.0.9+. Co-authored-by: Cursor --- .../Service/LaravelClosureSerializerTest.php | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/Unit/Service/LaravelClosureSerializerTest.php diff --git a/tests/Unit/Service/LaravelClosureSerializerTest.php b/tests/Unit/Service/LaravelClosureSerializerTest.php new file mode 100644 index 0000000..5cfc436 --- /dev/null +++ b/tests/Unit/Service/LaravelClosureSerializerTest.php @@ -0,0 +1,136 @@ +serializer = new LaravelClosureSerializer(); + } + + public function test_serialize_simple_closure(): void + { + $closure = static function (): string { + return 'hello'; + }; + + $serialized = $this->serializer->serialize($closure); + $result = $this->serializer->unserialize($serialized); + + self::assertSame('hello', $result()); + } + + public function test_serialize_closure_with_use_variable(): void + { + $name = 'crunz'; + $closure = static function () use ($name): string { + return "hello {$name}"; + }; + + $serialized = $this->serializer->serialize($closure); + $result = $this->serializer->unserialize($serialized); + + self::assertSame('hello crunz', $result()); + } + + public function test_serialize_closure_bound_to_object_with_closure_properties(): void + { + $runner = new TaskRunnerStub(); + $closure = $runner->createTask(); + + $serialized = $this->serializer->serialize($closure); + $result = $this->serializer->unserialize($serialized); + + self::assertSame('running daily-report', $result()); + } + + /** + * Regression test for laravel/serializable-closure#126. + * + * v2.0.9 skips walking properties of objects that implement __serialize, + * leaving nested closures unwrapped and causing "Serialization of 'Closure' + * is not allowed". + */ + public function test_serialize_closure_bound_to_object_with_serialize_and_closure_properties(): void + { + $runner = new TaskRunnerWithSerializeStub(); + $closure = $runner->createTask(); + + $serialized = $this->serializer->serialize($closure); + $result = $this->serializer->unserialize($serialized); + + self::assertSame('running daily-report', $result()); + } + + public function test_closure_code_can_be_extracted(): void + { + $testClosure = static fn (): \stdClass => new \stdClass(); + + $code = $this->serializer->closureCode($testClosure); + + self::assertSame('static fn (): \stdClass => new \stdClass()', $code); + } +} + +/** + * @internal + */ +class TaskRunnerStub +{ + public string $taskName = 'daily-report'; + /** @var \Closure[] */ + public array $filters = []; + + public function createTask(): \Closure + { + $this->filters[] = static function (): bool { return true; }; + + return function (): string { + return "running {$this->taskName}"; + }; + } +} + +/** + * @internal + */ +class TaskRunnerWithSerializeStub +{ + public string $taskName = 'daily-report'; + /** @var \Closure[] */ + public array $filters = []; + + /** @return array{taskName: string, filters: array<\Closure>} */ + public function __serialize(): array + { + return [ + 'taskName' => $this->taskName, + 'filters' => $this->filters, + ]; + } + + /** @param array{taskName: string, filters: array<\Closure>} $data */ + public function __unserialize(array $data): void + { + $this->taskName = $data['taskName']; + $this->filters = $data['filters']; + } + + public function createTask(): \Closure + { + $this->filters[] = static function (): bool { return true; }; + + return function (): string { + return "running {$this->taskName}"; + }; + } +}