diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 5c9c116..a4b6557 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -24,7 +24,7 @@ jobs: restore-keys: | ${{ runner.os }}-php- - name: Install suggested dependencies - run: composer require jeremeamia/superclosure + run: composer require jeremeamia/superclosure opis/closure - name: Install dependencies if: steps.composer-cache.outputs.cache-hit != 'true' run: composer install --prefer-dist --no-progress --no-suggest diff --git a/CHANGELOG.md b/CHANGELOG.md index ab70c62..c6d9ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## [Unreleased] +## [3.1.0] - 2023-07-18 ### Added -- Support of UnitEnum and BackedEnum deserialization (PHP > 8.1) +- Support of UnitEnum and BackedEnum deserialization (PHP > 8.1). Thanks @marcimat +- Support to multiple closure serializers +- Built in closure serializer using opis/closure ### Fixed - Fixed deprecated with DateTimeImmutable deserialization with PHP 8.2 diff --git a/README.md b/README.md index 681f33f..3fc9900 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Limitations: This project should not be confused with `JsonSerializable` interface added on PHP 5.4. This interface is used on `json_encode` to encode the objects. There is no unserialization with this interface, differently from this project. -*Json Serializer requires PHP >= 7.0 and tested until PHP 7.4* +*Json Serializer requires PHP >= 7.0 and tested until PHP 8.2* ## Example @@ -98,17 +98,16 @@ $json = $serializer->serialize($data); ``` -## Serializing Closures +## Serializing Closure -For serializing PHP closures you have to pass an object implementing `SuperClosure\SerializerInterface`. -This interface is provided by the project [SuperClosure](https://github.com/jeremeamia/super_closure). This -project also provides a closure serializer that implements this interface. +For serializing PHP closures you can either use [OpisClosure](https://github.com/opis/closure) (preferred) or +[SuperClosure](https://github.com/jeremeamia/super_closure) (the project is abandoned, so kept here for backward +compatibility). -Closure serialization has some limitations. Please check the SuperClosure project to check if it fits your -needs. +Closure serialization has some limitations. Please check the OpisClosure or SuperClosure project to check if it fits +your needs. -To use the SuperClosure with JsonSerializer, just pass the SuperClosure object as the first parameter -on JsonSerializer constructor. Example: +To use the OpisClosure with JsonSerializer, just add it to the closure serializer list. Example: ```php $toBeSerialized = [ @@ -122,13 +121,15 @@ $toBeSerialized = [ } ]; -$superClosure = new SuperClosure\Serializer(); -$jsonSerializer = new Zumba\JsonSerializer\JsonSerializer($superClosure); +$jsonSerializer = new \Zumba\JsonSerializer\JsonSerializer(); +$jsonSerializer->addClosureSerializer(new \Zumba\JsonSerializer\ClosureSerializer\OpisClosureSerializer()); $serialized = $jsonSerializer->serialize($toBeSerialized); ``` -PS: JsonSerializer does not have a hard dependency of SuperClosure. If you want to use both projects -make sure you add both on your composer requirements. +You can load multiple closure serializers in case you are migrating from SuperClosure to OpisClosure for example. + +PS: JsonSerializer does not have a hard dependency of OpisClosure or SuperClosure. If you want to use both projects +make sure you add both on your composer requirements and load them with `addClosureSerializer()` method. ## Custom Serializers diff --git a/composer.json b/composer.json index 84fb59e..63c7d4a 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "ext-mbstring": "*" }, "suggest": { - "jeremeamia/superclosure": "Allow to serialize PHP closures" + "opis/closure": "Allow to serialize PHP closures" }, "require-dev": { "phpunit/phpunit": ">=6.0 <10.0" diff --git a/src/JsonSerializer/ClosureSerializer/ClosureSerializer.php b/src/JsonSerializer/ClosureSerializer/ClosureSerializer.php new file mode 100644 index 0000000..dcd9811 --- /dev/null +++ b/src/JsonSerializer/ClosureSerializer/ClosureSerializer.php @@ -0,0 +1,25 @@ +closureSerializer[$classname] = $closureSerializer; + return $this; + } + + /** + * Get preferred closure serializer + * + * @return ClosureSerializer|null + */ + public function getPreferredSerializer() + { + if (empty($this->closureSerializer)) { + return null; + } + + foreach ($this->preferred as $preferred) { + if (isset($this->closureSerializer[$preferred])) { + return $this->closureSerializer[$preferred]; + } + } + return current($this->closureSerializer); + } + + /** + * Get closure serializer + * + * @param string $classname + * @return ClosureSerializer|null + */ + public function getSerializer(string $classname) + { + if (isset($this->closureSerializer[$classname])) { + return $this->closureSerializer[$classname]; + } + return null; + } +} diff --git a/src/JsonSerializer/ClosureSerializer/OpisClosureSerializer.php b/src/JsonSerializer/ClosureSerializer/OpisClosureSerializer.php new file mode 100644 index 0000000..b9c4ab2 --- /dev/null +++ b/src/JsonSerializer/ClosureSerializer/OpisClosureSerializer.php @@ -0,0 +1,32 @@ +getClosure(); + } + +} diff --git a/src/JsonSerializer/ClosureSerializer/SuperClosureSerializer.php b/src/JsonSerializer/ClosureSerializer/SuperClosureSerializer.php new file mode 100644 index 0000000..5314c94 --- /dev/null +++ b/src/JsonSerializer/ClosureSerializer/SuperClosureSerializer.php @@ -0,0 +1,49 @@ +serializer = $serializer; + } + + /** + * Serialize a closure + * + * @param Closure $closure + * @return string + */ + public function serialize(Closure $closure) + { + return $this->serializer->serialize($closure); + } + + /** + * Unserialize a closure + * + * @param string $serialized + * @return Closure + */ + public function unserialize($serialized) + { + return $this->serializer->unserialize($serialized); + } + +} diff --git a/src/JsonSerializer/JsonSerializer.php b/src/JsonSerializer/JsonSerializer.php index 59cb357..6c5fb41 100644 --- a/src/JsonSerializer/JsonSerializer.php +++ b/src/JsonSerializer/JsonSerializer.php @@ -49,11 +49,11 @@ class JsonSerializer protected $objectMappingIndex = 0; /** - * Closure serializer instance + * Closure manager * - * @var ClosureSerializerInterface + * @var ClosureSerializer\ClosureSerializerManager */ - protected $closureSerializer; + protected $closureManager; /** * Map of custom object serializers @@ -72,17 +72,33 @@ class JsonSerializer /** * Constructor. * - * @param ClosureSerializerInterface $closureSerializer + * @param ClosureSerializerInterface $closureSerializer This parameter is deprecated and will be removed in 5.0.0. Use addClosureSerializer() instead. * @param array $customObjectSerializerMap */ public function __construct( ClosureSerializerInterface $closureSerializer = null, $customObjectSerializerMap = [] ) { - $this->closureSerializer = $closureSerializer; + $this->closureManager = new ClosureSerializer\ClosureSerializerManager(); + if ($closureSerializer) { + trigger_error( + 'Passing a ClosureSerializerInterface to the constructor is deprecated and will be removed in 4.0.0. Use addClosureSerializer() instead.', + E_USER_DEPRECATED + ); + $this->addClosureSerializer(new ClosureSerializer\SuperClosureSerializer($closureSerializer)); + } + $this->customObjectSerializerMap = (array)$customObjectSerializerMap; } + /** + * Add a closure serializer + */ + public function addClosureSerializer(ClosureSerializer\ClosureSerializer $closureSerializer) + { + $this->closureManager->addSerializer($closureSerializer); + } + /** * Serialize the value in JSON * @@ -242,12 +258,14 @@ protected function serializeData($value) return array_map([$this, __FUNCTION__], $value); } if ($value instanceof \Closure) { - if (!$this->closureSerializer) { + $closureSerializer = $this->closureManager->getPreferredSerializer(); + if (!$closureSerializer) { throw new JsonSerializerException('Closure serializer not given. Unable to serialize closure.'); } return [ static::CLOSURE_IDENTIFIER_KEY => true, - 'value' => $this->closureSerializer->serialize($value) + 'serializer' => $closureSerializer::class, + 'value' => $closureSerializer->serialize($value) ]; } return $this->serializeObject($value); @@ -349,10 +367,12 @@ protected function unserializeData($value) } if (!empty($value[static::CLOSURE_IDENTIFIER_KEY])) { - if (!$this->closureSerializer) { + $serializerClass = isset($value['serializer']) ? $value['serializer'] : ClosureSerializer\SuperClosureSerializer::class; + $serializer = $this->closureManager->getSerializer($serializerClass); + if (!$serializer) { throw new JsonSerializerException('Closure serializer not provided to unserialize closure'); } - return $this->closureSerializer->unserialize($value['value']); + return $serializer->unserialize($value['value']); } return array_map([$this, __FUNCTION__], $value); diff --git a/tests/ClosureSerializer/ClosureSerializerManagerTest.php b/tests/ClosureSerializer/ClosureSerializerManagerTest.php new file mode 100644 index 0000000..b33dfef --- /dev/null +++ b/tests/ClosureSerializer/ClosureSerializerManagerTest.php @@ -0,0 +1,26 @@ +assertEmpty($manager->getSerializer('foo')); + $manager->addSerializer(new SuperClosureSerializer(new \SuperClosure\Serializer())); + $this->assertNotEmpty($manager->getSerializer(SuperClosureSerializer::class)); + } + + public function testGetPreferredSerializer() { + $manager = new ClosureSerializerManager(); + $this->assertNull($manager->getPreferredSerializer()); + + $serializer = new SuperClosureSerializer(new \SuperClosure\Serializer()); + $manager->addSerializer($serializer); + $this->assertSame($serializer, $manager->getPreferredSerializer()); + } +} diff --git a/tests/ClosureSerializer/OpisClosureSerializerTest.php b/tests/ClosureSerializer/OpisClosureSerializerTest.php new file mode 100644 index 0000000..5950c23 --- /dev/null +++ b/tests/ClosureSerializer/OpisClosureSerializerTest.php @@ -0,0 +1,32 @@ +serialize($closure); + $this->assertNotEmpty($serialized); + $this->assertTrue(is_string($serialized)); + $this->assertNotEquals($closure, $serialized); + } + + public function testUnserialize() { + $closure = function() { + return 'foo'; + }; + $serializer = new OpisClosureSerializer(); + $serialized = $serializer->serialize($closure); + $unserialized = $serializer->unserialize($serialized); + $this->assertNotEmpty($unserialized); + $this->assertTrue($unserialized instanceof \Closure); + $this->assertEquals($closure(), $unserialized()); + } +} diff --git a/tests/ClosureSerializer/SuperClosureSerializerTest.php b/tests/ClosureSerializer/SuperClosureSerializerTest.php new file mode 100644 index 0000000..7ba40c3 --- /dev/null +++ b/tests/ClosureSerializer/SuperClosureSerializerTest.php @@ -0,0 +1,32 @@ +serialize($closure); + $this->assertNotEmpty($serialized); + $this->assertTrue(is_string($serialized)); + $this->assertNotEquals($closure, $serialized); + } + + public function testUnserialize() { + $closure = function() { + return 'foo'; + }; + $serializer = new SuperClosureSerializer(new \SuperClosure\Serializer()); + $serialized = $serializer->serialize($closure); + $unserialized = $serializer->unserialize($serialized); + $this->assertNotEmpty($unserialized); + $this->assertTrue($unserialized instanceof \Closure); + $this->assertEquals($closure(), $unserialized()); + } +} diff --git a/tests/JsonSerializerTest.php b/tests/JsonSerializerTest.php index bd3a8e7..15fd8df 100644 --- a/tests/JsonSerializerTest.php +++ b/tests/JsonSerializerTest.php @@ -2,10 +2,11 @@ namespace Zumba\JsonSerializer\Test; +use Zumba\JsonSerializer\ClosureSerializer; use Zumba\JsonSerializer\JsonSerializer; use Zumba\JsonSerializer\Exception\JsonSerializerException; use stdClass; -use SuperClosure\Serializer as ClosureSerializer; +use SuperClosure\Serializer as SuperClosureSerializer; use PHPUnit\Framework\TestCase; use ReflectionProperty; @@ -246,7 +247,7 @@ public function testUnserializeObjects() * * @return void */ - public function testSerializeEnums() + public function testSerializeEnums() { if (PHP_VERSION_ID < 80100) { $this->markTestSkipped("Enums are only available since PHP 8.1"); @@ -266,7 +267,7 @@ public function testSerializeEnums() * * @return void */ - public function testUnserializeEnums() + public function testUnserializeEnums() { if (PHP_VERSION_ID < 80100) { $this->markTestSkipped("Enums are only available since PHP 8.1"); @@ -389,13 +390,13 @@ public function testSerializationOfDateTime() * * @return void */ - public function testSerializationOfClosure() + public function testSerializationOfClosureWithSuperClosureOnConstructor() { if (!class_exists('SuperClosure\Serializer')) { $this->markTestSkipped('SuperClosure is not installed.'); } - $closureSerializer = new ClosureSerializer(); + $closureSerializer = new SuperClosureSerializer(); $serializer = new JsonSerializer($closureSerializer); $serialized = $serializer->serialize( array( @@ -413,6 +414,111 @@ public function testSerializationOfClosure() $this->assertSame('it works', $unserialized['func']()); } + /** + * Test the serialization of closures providing closure serializer + * + * @return void + */ + public function testSerializationOfClosureWithSuperClosureOnManager() + { + if (!class_exists('SuperClosure\Serializer')) { + $this->markTestSkipped('SuperClosure is not installed.'); + } + + $closureSerializer = new SuperClosureSerializer(); + $serializer = new JsonSerializer(); + $serializer->addClosureSerializer(new ClosureSerializer\SuperClosureSerializer($closureSerializer)); + $serialized = $serializer->serialize( + array( + 'func' => function () { + return 'it works'; + }, + 'nice' => true + ) + ); + + $unserialized = $serializer->unserialize($serialized); + $this->assertTrue(is_array($unserialized)); + $this->assertTrue($unserialized['nice']); + $this->assertInstanceOf('Closure', $unserialized['func']); + $this->assertSame('it works', $unserialized['func']()); + } + + /** + * Test the serialization of closures providing closure serializer + * + * @return void + */ + public function testSerializationOfClosureWitOpisClosure() + { + if (!class_exists('Opis\Closure\SerializableClosure')) { + $this->markTestSkipped('OpisClosure is not installed.'); + } + + $serializer = new JsonSerializer(); + $serializer->addClosureSerializer(new ClosureSerializer\OpisClosureSerializer()); + $serialized = $serializer->serialize( + array( + 'func' => function () { + return 'it works'; + }, + 'nice' => true + ) + ); + + $unserialized = $serializer->unserialize($serialized); + $this->assertTrue(is_array($unserialized)); + $this->assertTrue($unserialized['nice']); + $this->assertInstanceOf('Closure', $unserialized['func']); + $this->assertSame('it works', $unserialized['func']()); + } + + /** + * Test the serialization of closures providing closure serializer + * + * @return void + */ + public function testSerializationOfClosureWitMultipleClosures() + { + if (!class_exists('SuperClosure\Serializer')) { + $this->markTestSkipped('SuperClosure is not installed.'); + } + if (!class_exists('Opis\Closure\SerializableClosure')) { + $this->markTestSkipped('OpisClosure is not installed.'); + } + + $closureSerializer = new SuperClosureSerializer(); + $serializer = new JsonSerializer(); + $serializer->addClosureSerializer(new ClosureSerializer\SuperClosureSerializer($closureSerializer)); + + $serializeData = array( + 'func' => function () { + return 'it works'; + }, + 'nice' => true + ); + + // Make sure it was serialized with SuperClosure + $serialized = $serializer->serialize($serializeData); + echo $serialized; + $this->assertGreaterThanOrEqual(0, strpos($serialized, 'SuperClosure')); + $this->assertFalse(strpos($serialized, 'OpisClosure')); + + // Test adding a new preferred closure serializer + $serializer->addClosureSerializer(new ClosureSerializer\OpisClosureSerializer()); + + $unserialized = $serializer->unserialize($serialized); + $this->assertTrue(is_array($unserialized)); + $this->assertTrue($unserialized['nice']); + $this->assertInstanceOf('Closure', $unserialized['func']); + $this->assertSame('it works', $unserialized['func']()); + + // Serialize again with the new preferred closure serializer + $serialized = $serializer->serialize($serializeData); + $this->assertFalse(strpos($serialized, 'SuperClosure')); + $this->assertGreaterThanOrEqual(0, strpos($serialized, 'OpisClosure')); + } + /** * Test the unserialization of closures without providing closure serializer * @@ -424,7 +530,7 @@ public function testUnserializeOfClosureWithoutSerializer() $this->markTestSkipped('SuperClosure is not installed.'); } - $closureSerializer = new ClosureSerializer(); + $closureSerializer = new SuperClosureSerializer(); $serializer = new JsonSerializer($closureSerializer); $serialized = $serializer->serialize( array(