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", 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}"; + }; + } +}