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/README.md b/README.md
index 7ccfd29..e11dacb 100644
--- a/README.md
+++ b/README.md
@@ -5,10 +5,29 @@ Opis Closure
[](https://packagist.org/packages/opis/closure)
[](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"
}
}
```
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": [
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/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
+}
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
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