Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
35 changes: 23 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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};
Expand All @@ -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.
Expand Down Expand Up @@ -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"
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<directory suffix="Test.php" phpVersion="8.2" phpVersionOperator=">=">./tests/PHP82</directory>
<directory suffix="Test.php" phpVersion="8.3" phpVersionOperator=">=">./tests/PHP83</directory>
<directory suffix="Test.php" phpVersion="8.4" phpVersionOperator=">=">./tests/PHP84</directory>
<directory suffix="Test.php" phpVersion="8.5" phpVersionOperator=">=">./tests/PHP85</directory>
</testsuite>
</testsuites>
</phpunit>
4 changes: 2 additions & 2 deletions src/ReflectionClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -148,4 +148,4 @@ public static function getRawProperties(object $object, array $properties, ?stri

return $data;
}
}
}
6 changes: 3 additions & 3 deletions src/SerializationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -289,4 +289,4 @@ private function &getCachedInfo(AbstractInfo $info): array
$this->info[$key] ??= $info->__serialize();
return $this->info[$key];
}
}
}
17 changes: 17 additions & 0 deletions tests/PHP81/MyInt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Opis\Closure\Test\PHP81;

class MyInt
{
private int $value;

public function __construct(int $value)
{
$this->value = $value;
}

public function read(): int {
return $this->value;
}
}
6 changes: 6 additions & 0 deletions tests/PHP81/SerializeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
8 changes: 8 additions & 0 deletions tests/PHP84/SyntaxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
35 changes: 35 additions & 0 deletions tests/PHP85/SerializeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Opis\Closure\Test\PHP85;

use Opis\Closure\Test\SerializeTestCase;

class SerializeTest extends SerializeTestCase
{
public function testPipeOperatorResult()
{
$wrap = static function (string $input) {
return fn() => $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);
}
}
92 changes: 92 additions & 0 deletions tests/PHP85/SyntaxTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

namespace Opis\Closure\Test\PHP85;

use Opis\Closure\Test\SyntaxTestCase;

class SyntaxTest extends SyntaxTestCase
{
public function closureProvider(): iterable
{
yield [
'Pipe operator',
static fn(string $input) => $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,
];
}
}