From 627c6c75026e61906cd1b3f15b357c4b24a5c563 Mon Sep 17 00:00:00 2001 From: Sorin Sarca Date: Sat, 11 Oct 2025 12:39:14 +0300 Subject: [PATCH 1/5] Added php 8.5 tests --- .github/workflows/tests.yml | 2 +- phpunit.xml | 1 + tests/PHP84/SyntaxTest.php | 8 +++ tests/PHP85/SerializeTest.php | 35 +++++++++++++ tests/PHP85/SyntaxTest.php | 92 +++++++++++++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 tests/PHP85/SerializeTest.php create mode 100644 tests/PHP85/SyntaxTest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 75016f6..d4c8434 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: true matrix: - php: [8.0, 8.1, 8.2, 8.3, 8.4] + php: [8.0, 8.1, 8.2, 8.3, 8.4, 8.5] name: PHP ${{ matrix.php }} diff --git a/phpunit.xml b/phpunit.xml index c56b9e5..9959625 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -6,6 +6,7 @@ ./tests/PHP82 ./tests/PHP83 ./tests/PHP84 + ./tests/PHP85 \ No newline at end of file diff --git a/tests/PHP84/SyntaxTest.php b/tests/PHP84/SyntaxTest.php index 63ce20d..a5be283 100644 --- a/tests/PHP84/SyntaxTest.php +++ b/tests/PHP84/SyntaxTest.php @@ -8,6 +8,14 @@ class SyntaxTest extends SyntaxTestCase { public function closureProvider(): iterable { + yield [ + 'New without parenthesis', +static fn() => new class {}, + <<<'PHP' +namespace Opis\Closure\Test\PHP84; +return static fn() => new class {}; +PHP, + ]; yield [ 'Asymmetric Property Visibility', static fn() => new class("Opis") { diff --git a/tests/PHP85/SerializeTest.php b/tests/PHP85/SerializeTest.php new file mode 100644 index 0000000..b0261f5 --- /dev/null +++ b/tests/PHP85/SerializeTest.php @@ -0,0 +1,35 @@ + $input |> strtoupper(...); + }; + $fn = "Hello" |> $wrap(...); + + $fn2 = $this->process($fn); + $this->assertEquals($fn2(), "HELLO"); + } + + public function testConstExpression() { + $src = static function (callable $input = static function() {return 123;}): callable { + return $input; + }; + + $fn = $src(); + $this->assertEquals($fn(), 123); + } + + public function testConstExpression2() { + $src = static fn (callable $input = static function() {return 123;}): callable => $input; + + $fn = $src(); + $this->assertEquals($fn(), 123); + } +} \ No newline at end of file diff --git a/tests/PHP85/SyntaxTest.php b/tests/PHP85/SyntaxTest.php new file mode 100644 index 0000000..abafa7e --- /dev/null +++ b/tests/PHP85/SyntaxTest.php @@ -0,0 +1,92 @@ + $input |> strtoupper(...) |> trim(...), + <<<'PHP' +namespace Opis\Closure\Test\PHP85; +return static fn(string $input) => $input |> strtoupper(...) |> trim(...); +PHP, + ]; + yield [ + 'Closure as default argument', +static fn(int $number, callable $op = static function (int $value) { return $value * 2; }) => $op($number), + <<<'PHP' +namespace Opis\Closure\Test\PHP85; +return static fn(int $number, callable $op = static function (int $value) { return $value * 2; }) => $op($number); +PHP, + ]; + yield [ + 'Closure in const expression', +static function(array $callbacks = [ + static function () { + echo "1"; + }, + static function () { + echo "2"; + }, +]): void { + foreach ($callbacks as $callback) { + $callback(); + } +}, + <<<'PHP' +namespace Opis\Closure\Test\PHP85; +return static function(array $callbacks = [ + static function () { + echo "1"; + }, + static function () { + echo "2"; + }, +]): void { + foreach ($callbacks as $callback) { + $callback(); + } +}; +PHP, + ]; + yield [ + 'Closure in attribute expression', +static fn() => new class { + #[XValidator(static function (string $value): bool { + return strlen($value) <= 32; + })] + public string $value = ""; +}, + <<<'PHP' +namespace Opis\Closure\Test\PHP85; +return static fn() => new class { + #[XValidator(static function (string $value): bool { + return strlen($value) <= 32; + })] + public string $value = ""; +}; +PHP, + ]; + yield [ + 'final property promotion', +static fn() => new class { + public function __construct(public final int $value = 1) + { + } +}, + <<<'PHP' +namespace Opis\Closure\Test\PHP85; +return static fn() => new class { + public function __construct(public final int $value = 1) + { + } +}; +PHP, + ]; + } +} \ No newline at end of file From b44e1b782e0d88caac4a4ce5cbbbbbb9e4a2fc86 Mon Sep 17 00:00:00 2001 From: Sorin Sarca Date: Sat, 11 Oct 2025 12:39:37 +0300 Subject: [PATCH 2/5] Updated keywords --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 25d4fce..372c877 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "opis/closure", "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary data.", - "keywords": ["closure", "serialization", "function", "serializable", "serialize", "anonymous functions"], + "keywords": ["closure", "serialization", "function", "serializable", "serialize", "anonymous functions", "anonymous classes"], "homepage": "https://opis.io/closure", "license": "MIT", "authors": [ From 4967564394000da0827c8244bfc63601cfcfac7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=A4u=C3=9Fler?= Date: Tue, 14 Oct 2025 23:02:07 +0200 Subject: [PATCH 3/5] Prefer `offsetSet` over deprecated `attach` (#161) * Prefer `offsetSet` over deprecated `attach` `SplObjectStorage::attach()` is deprecated since PHP v8.5; `SplObjectStorage::offsetSet()` should be used instead [1]. [1]: https://github.com/php/php-src/pull/19424 --- src/ReflectionClass.php | 4 ++-- src/SerializationHandler.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ReflectionClass.php b/src/ReflectionClass.php index a069f16..bd57035 100644 --- a/src/ReflectionClass.php +++ b/src/ReflectionClass.php @@ -115,7 +115,7 @@ public static function getRefId(mixed &$reference, ?\SplObjectStorage $keepAlive } // we save this so the ref ids cannot be reused while serializing/deserializing - $keepAlive?->attach($ref); + $keepAlive?->offsetSet($ref); return $ref->getId(); } @@ -148,4 +148,4 @@ public static function getRawProperties(object $object, array $properties, ?stri return $data; } -} \ No newline at end of file +} diff --git a/src/SerializationHandler.php b/src/SerializationHandler.php index 602fe1c..de43d9d 100644 --- a/src/SerializationHandler.php +++ b/src/SerializationHandler.php @@ -125,7 +125,7 @@ private function handleObject(object $data): object if ($data instanceof stdClass) { // handle stdClass $obj = $this->handleStdClass($data); - $this->priority->attach($obj); + $this->priority->offsetSet($obj); return $obj; } @@ -158,7 +158,7 @@ private function handleObject(object $data): object $box->data[1] = $this->getObjectVars($data, $info); // Add to priority - $this->priority->attach($box); + $this->priority->offsetSet($box); return $box; } @@ -289,4 +289,4 @@ private function &getCachedInfo(AbstractInfo $info): array $this->info[$key] ??= $info->__serialize(); return $this->info[$key]; } -} \ No newline at end of file +} From 125b84354e907f1f0927077a0996d42e0140c960 Mon Sep 17 00:00:00 2001 From: Sorin Sarca Date: Thu, 20 Nov 2025 01:28:15 +0200 Subject: [PATCH 4/5] Added test --- tests/PHP81/MyInt.php | 17 +++++++++++++++++ tests/PHP81/SerializeTest.php | 6 ++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/PHP81/MyInt.php diff --git a/tests/PHP81/MyInt.php b/tests/PHP81/MyInt.php new file mode 100644 index 0000000..33962ad --- /dev/null +++ b/tests/PHP81/MyInt.php @@ -0,0 +1,17 @@ +value = $value; + } + + public function read(): int { + return $this->value; + } +} \ No newline at end of file diff --git a/tests/PHP81/SerializeTest.php b/tests/PHP81/SerializeTest.php index 8e8c57e..0e5e950 100644 --- a/tests/PHP81/SerializeTest.php +++ b/tests/PHP81/SerializeTest.php @@ -24,4 +24,10 @@ public function testEnum() $closure = $this->process(MyEnum::CASE1->getClosure()); $this->assertEquals(MyEnum::CASE1, $closure()); } + + public function testFirstClassCallable() + { + $closure = $this->process((new MyInt(5))->read(...)); + $this->assertEquals(5, $closure()); + } } \ No newline at end of file From 05543984845132ad004291352432d586cf1ea8f1 Mon Sep 17 00:00:00 2001 From: Sorin Sarca Date: Thu, 20 Nov 2025 02:18:25 +0200 Subject: [PATCH 5/5] Updated README --- README.md | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7ccfd29..e11dacb 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,29 @@ Opis Closure [![Packagist Downloads](https://img.shields.io/packagist/dt/opis/closure?label=Downloads)](https://packagist.org/packages/opis/closure) [![Packagist License](https://img.shields.io/packagist/l/opis/closure?color=teal&label=License)](https://packagist.org/packages/opis/closure) -Serialize closures, serialize anything +Serialize closures and anonymous classes ------------------ -**Opis Closure** is a PHP library that allows you to serialize closures, anonymous classes, and arbitrary data. +**Opis Closure** is a PHP library that allows you to serialize closures, +anonymous classes, and arbitrary data. + +Key features: + +- serialize [closures (anonymous functions)](https://www.php.net/manual/en/functions.anonymous.php) +- serialize [anonymous classes](https://www.php.net/manual/en/language.oop5.anonymous.php) +- does not rely on PHP extensions (no FFI or similar dependencies) +- supports PHP 8.0-8.5 syntax +- handles circular references +- works with [attributes](https://www.php.net/manual/en/language.attributes.overview.php) +- works with [readonly properties](https://www.php.net/manual/en/language.oop5.properties.php#language.oop5.properties.readonly-properties) +- works with [property hooks](https://www.php.net/manual/en/language.oop5.property-hooks.php) +- extensible via [custom serializers and deserializers](https://opis.io/closure/4.x/objects.html) +- supports [cryptographically signed data](https://opis.io/closure/4.x/security.html) +- supports PHP's built-in [SPL and Date classes](https://opis.io/closure/4.x/objects.html#default-object-serializers), and the popular [`nesbot/carbon`](https://github.com/CarbonPHP/carbon) package +- reconstructed code is close to the original and [debugger friendly](https://opis.io/closure/4.x/debug.html) +- and [many more][documentation] + +### Example of closure serialization ```php use function Opis\Closure\{serialize, unserialize}; @@ -19,8 +38,7 @@ $greet = unserialize($serialized); echo $greet(); // hello from closure! ``` -> [!IMPORTANT] -> Starting with version 4.2, **Opis Closure** supports serialization of anonymous classes. +### Example of anonymous class serialization ```php use function Opis\Closure\{serialize, unserialize}; @@ -37,13 +55,6 @@ $object = unserialize($serialized); echo $object->greet(); // hello from anonymous class! ``` -_A full rewrite was necessary to keep this project compatible with the PHP's new features, such as attributes, enums, -read-only properties, named parameters, anonymous classes, and so on. This wasn't an easy task, as the latest attempt -to launch a 4.x version involved using the FFI extension in exotic ways, and it failed hard. The main problem was that -very often the closures were bound to some object, thus in order to preserve functionality, we had to serialize the object -too. Since we had to do arbitrary data serialization, we decided to make this project about arbitrary data serialization, -providing support for serializing closures but also adding more effortless ways to serialize custom objects._ - ## Migrating from 3.x Version 4.x is a full rewrite of the library, but data deserialization from 3.x is possible. @@ -75,7 +86,7 @@ Or you could directly reference it into your `composer.json` file as a dependenc ```json { "require": { - "opis/closure": "^4.3" + "opis/closure": "^4.4" } } ```