diff --git a/composer.json b/composer.json index 7fe9126..465811b 100644 --- a/composer.json +++ b/composer.json @@ -14,17 +14,18 @@ "license": "Apache-2.0", "require": { "guzzlehttp/psr7": "~1.0 || ^2.4", - "jeremeamia/superclosure": "^2.4", + "opis/closure": "^4.0", "php-http/client-common": "^2.0", "php-http/httplug": "^2.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { "apigee/apigee-client-php": "^2.0.6", "limedeck/phpunit-detailed-printer": "^3.2", "php-http/discovery": "^1.6.0", "php-http/guzzle6-adapter": "^2.0", - "phpunit/phpunit": "^6.4.0", + "phpspec/prophecy-phpunit": "^2.0@dev", + "phpunit/phpunit": "^11.0", "twig/twig": "~1.0" }, "autoload": { diff --git a/src/MockSerializableClosure.php b/src/MockSerializableClosure.php index 22f94ac..82b09ce 100644 --- a/src/MockSerializableClosure.php +++ b/src/MockSerializableClosure.php @@ -1,17 +1,13 @@ closure = $closure; - $this->serializer = $serializer ?: new Serializer; } /** @@ -63,14 +42,6 @@ public function getClosure() /** * Delegates the closure invocation to the actual closure object. * - * Important Notes: - * - * - `ReflectionFunction::invokeArgs()` should not be used here, because it - * does not work with closure bindings. - * - Args passed-by-reference lose their references when proxied through - * `__invoke()`. This is an unfortunate, but understandable, limitation - * of PHP that will probably never change. - * * @return mixed */ public function __invoke() @@ -81,130 +52,92 @@ public function __invoke() /** * Clones the SerializableClosure with a new bound object and class scope. * - * The method is essentially a wrapped proxy to the Closure::bindTo method. - * - * @param mixed $newthis The object to which the closure should be bound, - * or NULL for the closure to be unbound. - * @param mixed $newscope The class scope to which the closure is to be - * associated, or 'static' to keep the current one. - * If an object is given, the type of the object will - * be used instead. This determines the visibility of - * protected and private methods of the bound object. + * @param mixed $newthis The object to which the closure should be bound. + * @param mixed $newscope The class scope to which the closure is to be associated. * - * @return SerializableClosure - * @link http://www.php.net/manual/en/closure.bindto.php + * @return MockSerializableClosure */ public function bindTo($newthis, $newscope = 'static') { return new self( - $this->closure->bindTo($newthis, $newscope), - $this->serializer + $this->closure->bindTo($newthis, $newscope) ); } /** - * Deprecation notice is emitted if the class does not implement __serialize - * method. - * - */ - public function __serialize() + * Magic method for PHP 7.4+ serialization. + * + * @return array + */ + public function __serialize(): array { - try { - $this->data = $this->data ?: $this->serializer->getData($this->closure, true); - return $this->data; - } catch (\Exception $e) { - trigger_error( - 'Serialization of closure failed: ' . $e->getMessage(), - E_USER_NOTICE - ); - // Note: The serialize() method of Serializable must return a string - // or null and cannot throw exceptions. - return null; + $response = ($this->closure)(); + if ($response instanceof ResponseInterface) { + $data = [ + 'statusCode' => $response->getStatusCode(), + 'headers' => $response->getHeaders(), + 'body' => (string) $response->getBody(), + 'protocolVersion' => $response->getProtocolVersion(), + 'reasonPhrase' => $response->getReasonPhrase(), + ]; + } else { + $data = $response; } + + return ['data' => $data]; } /** - * Deprecation notice is emitted if the class does not implement __unserialize - * method. - * - */ - public function __unserialize(array $serialized):void + * Magic method for PHP 7.4+ unserialization. + * + * @param array $data + */ + public function __unserialize(array $data): void { - // Unserialize the closure data and reconstruct the closure object. - $this->data = $serialized; - $this->closure = __reconstruct_closure($this->data); - - // Throw an exception if the closure could not be reconstructed. - if (!$this->closure instanceof Closure) { - throw new ClosureUnserializationException( - 'The closure is corrupted and cannot be unserialized.' - ); - } - - // Rebind the closure to its former binding and scope. - if ($this->data['binding'] || $this->data['isStatic']) { - $this->closure = $this->closure->bindTo( - $this->data['binding'], - $this->data['scope'] + if (is_array($data['data']) && isset($data['data']['statusCode'])) { + $response = new \GuzzleHttp\Psr7\Response( + $data['data']['statusCode'], + $data['data']['headers'], + $data['data']['body'], + $data['data']['protocolVersion'], + $data['data']['reasonPhrase'] ); + $this->closure = function () use ($response) { + return $response; + }; + } else { + $this->closure = function () use ($data) { + return $data['data']; + }; } } /** - * Serializes the code, context, and binding of the closure. + * Serializes the closure wrapper. * * @return string|null - * @link http://php.net/manual/en/serializable.serialize.php */ public function serialize() { try { - $this->data = $this->data ?: $this->serializer->getData($this->closure, true); - return serialize($this->data); + return opis_serialize($this->closure); } catch (\Exception $e) { trigger_error( 'Serialization of closure failed: ' . $e->getMessage(), E_USER_NOTICE ); - // Note: The serialize() method of Serializable must return a string - // or null and cannot throw exceptions. return null; } } /** - * Unserializes the closure. - * - * Unserializes the closure's data and recreates the closure using a - * simulation of its original context. The used variables (context) are - * extracted into a fresh scope prior to redefining the closure. The - * closure is also rebound to its former object and scope. + * Unserializes the closure wrapper. * * @param string $serialized - * - * @throws ClosureUnserializationException - * @link http://php.net/manual/en/serializable.unserialize.php */ public function unserialize($serialized) { - // Unserialize the closure data and reconstruct the closure object. - $this->data = unserialize($serialized); - $this->closure = __reconstruct_closure($this->data); - - // Throw an exception if the closure could not be reconstructed. - if (!$this->closure instanceof Closure) { - throw new ClosureUnserializationException( - 'The closure is corrupted and cannot be unserialized.' - ); - } - - // Rebind the closure to its former binding and scope. - if ($this->data['binding'] || $this->data['isStatic']) { - $this->closure = $this->closure->bindTo( - $this->data['binding'], - $this->data['scope'] - ); - } + $this->closure = opis_unserialize($serialized); } /** @@ -214,55 +147,8 @@ public function unserialize($serialized) */ public function __debugInfo() { - return $this->data ?: $this->serializer->getData($this->closure, true); + return [ + 'closure' => $this->closure + ]; } } - -/** - * Reconstruct a closure. - * - * HERE BE DRAGONS! - * - * The infamous `eval()` is used in this method, along with the error - * suppression operator, and variable variables (i.e., double dollar signs) to - * perform the unserialization logic. I'm sorry, world! - * - * This is also done inside a plain function instead of a method so that the - * binding and scope of the closure are null. - * - * @param array $__data Unserialized closure data. - * - * @return Closure|null - * @internal - */ -function __reconstruct_closure(array $__data) -{ - // Simulate the original context the closure was created in. - foreach ($__data['context'] as $__var_name => &$__value) { - if ($__value instanceof SerializableClosure) { - // Unbox any SerializableClosures in the context. - $__value = $__value->getClosure(); - } elseif ($__value === Serializer::RECURSION) { - // Track recursive references (there should only be one). - $__recursive_reference = $__var_name; - } - - // Import the variable into this scope. - ${$__var_name} = $__value; - } - - // Evaluate the code to recreate the closure. - try { - if (isset($__recursive_reference)) { - // Special handling for recursive closures. - @eval("\${$__recursive_reference} = {$__data['code']};"); - $__closure = ${$__recursive_reference}; - } else { - @eval("\$__closure = {$__data['code']};"); - } - } catch (\ParseError $e) { - // Discard the parse error. - } - - return isset($__closure) ? $__closure : null; -} diff --git a/tests/ApigeeSdk/ManagementSdkEntityCreationTraitTest.php b/tests/ApigeeSdk/ManagementSdkEntityCreationTraitTest.php index 08ea793..903ad7d 100644 --- a/tests/ApigeeSdk/ManagementSdkEntityCreationTraitTest.php +++ b/tests/ApigeeSdk/ManagementSdkEntityCreationTraitTest.php @@ -36,7 +36,7 @@ class ManagementSdkEntityCreationTraitTest extends TestCase { use ManagementSdkEntityCreationTrait; use RandomStringGeneratorTrait; - public function setUp() { + public function setUp(): void { parent::setUp(); $this->mockClient = new MockClient(); diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 19910dd..14e769b 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -33,7 +33,7 @@ class ClientTest extends TestCase { protected $client; - public function setUp() { + public function setUp(): void { parent::setUp(); // Create a test client. diff --git a/tests/MockHandlerTest.php b/tests/MockHandlerTest.php index 84d0a8c..94e81f0 100644 --- a/tests/MockHandlerTest.php +++ b/tests/MockHandlerTest.php @@ -35,7 +35,7 @@ class MockHandlerTest extends TestCase { private $generic_request; - function setUp() { + function setUp(): void { parent::setUp(); $this->generic_request = new Request('GET', 'http://example.com'); diff --git a/tests/ResponseFactoryTest.php b/tests/ResponseFactoryTest.php index f62a287..4c20254 100644 --- a/tests/ResponseFactoryTest.php +++ b/tests/ResponseFactoryTest.php @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -25,6 +25,7 @@ use Apigee\MockClient\ResponseGeneratorInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\ResponseInterface; /** @@ -32,6 +33,7 @@ */ class ResponseFactoryTest extends TestCase { + use ProphecyTrait; /** * Test the response factory can find a generator for an object or throw an * error. diff --git a/tests/SimpleMockStorageTest.php b/tests/SimpleMockStorageTest.php index 4ed5bd0..70eb0da 100644 --- a/tests/SimpleMockStorageTest.php +++ b/tests/SimpleMockStorageTest.php @@ -55,7 +55,7 @@ class SimpleMockStorageTest extends TestCase { */ protected $matchableResult; - public function setup() { + public function setup(): void { $this->storage = new SimpleMockStorage(); $generator = MessageFactoryDiscovery::find(); $this->response = $generator->createResponse();