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
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
226 changes: 56 additions & 170 deletions src/MockSerializableClosure.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
<?php namespace Apigee\MockClient;

use Closure;
use SuperClosure\Exception\ClosureUnserializationException;
use SuperClosure\Serializer;
use function Opis\Closure\serialize as opis_serialize;
use function Opis\Closure\unserialize as opis_unserialize;
use Psr\Http\Message\ResponseInterface;

/**
* This class acts as a wrapper for a closure, and allows it to be serialized.
* This is a copy paste of SuperClosure\SerializableClosure along with
* __serializer() and __unserializer() methods.
*
* With the combined power of the Reflection API, code parsing, and the infamous
* `eval()` function, you can serialize a closure, unserialize it somewhere
* else (even a different PHP process), and execute it.
* It has been updated to use opis/closure v4.x helper functions.
*/
class MockSerializableClosure implements \Serializable
{
Expand All @@ -22,32 +18,15 @@ class MockSerializableClosure implements \Serializable
*/
private $closure;

/**
* The serializer doing the serialization work.
*
* @var SerializerInterface
*/
private $serializer;

/**
* The data from unserialization.
*
* @var array
*/
private $data;

/**
* Create a new serializable closure instance.
*
* @param Closure $closure
* @param SerializerInterface|null $serializer
* @param Closure $closure
* @param mixed $serializer Kept for backwards compatibility with the signature.
*/
public function __construct(
\Closure $closure,
SerializerInterface $serializer = null
) {
public function __construct(Closure $closure, $serializer = null)
{
$this->closure = $closure;
$this->serializer = $serializer ?: new Serializer;
}

/**
Expand All @@ -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()
Expand All @@ -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);
}

/**
Expand All @@ -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;
}
2 changes: 1 addition & 1 deletion tests/ApigeeSdk/ManagementSdkEntityCreationTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion tests/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class ClientTest extends TestCase {

protected $client;

public function setUp() {
public function setUp(): void {
parent::setUp();

// Create a test client.
Expand Down
2 changes: 1 addition & 1 deletion tests/MockHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
4 changes: 3 additions & 1 deletion tests/ResponseFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,13 +25,15 @@
use Apigee\MockClient\ResponseGeneratorInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Http\Message\ResponseInterface;

/**
* Tests the response factory.
*/
class ResponseFactoryTest extends TestCase {

use ProphecyTrait;
/**
* Test the response factory can find a generator for an object or throw an
* error.
Expand Down
2 changes: 1 addition & 1 deletion tests/SimpleMockStorageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down