Update v1.0.6

This commit is contained in:
Bhanu Slathia
2016-02-16 23:22:09 +05:30
parent 62d04a0372
commit c710c20b9e
7620 changed files with 244752 additions and 1070312 deletions

View File

@@ -1,141 +0,0 @@
<?php namespace SuperClosure\Analyzer;
use SuperClosure\Analyzer\Visitor\ThisDetectorVisitor;
use SuperClosure\Exception\ClosureAnalysisException;
use SuperClosure\Analyzer\Visitor\ClosureLocatorVisitor;
use SuperClosure\Analyzer\Visitor\MagicConstantVisitor;
use PhpParser\NodeTraverser;
use PhpParser\PrettyPrinter\Standard as NodePrinter;
use PhpParser\Error as ParserError;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Parser as CodeParser;
use PhpParser\ParserFactory;
use PhpParser\Lexer\Emulative as EmulativeLexer;
/**
* This is the AST based analyzer.
*
* We're using reflection and AST-based code parser to analyze a closure and
* determine its code and context using the nikic/php-parser library. The AST
* based analyzer and has more capabilities than the token analyzer, but is,
* unfortunately, about 25 times slower.
*/
class AstAnalyzer extends ClosureAnalyzer
{
protected function determineCode(array &$data)
{
// Find the closure by traversing through a AST of the code.
// Note: This also resolves class names to their FQCNs while traversing.
$this->locateClosure($data);
// Make a second pass through the AST, but only through the closure's
// nodes, to resolve any magic constants to literal values.
$traverser = new NodeTraverser;
$traverser->addVisitor(new MagicConstantVisitor($data['location']));
$traverser->addVisitor($thisDetector = new ThisDetectorVisitor);
$data['ast'] = $traverser->traverse([$data['ast']])[0];
$data['hasThis'] = $thisDetector->detected;
// Bounce the updated AST down to a string representation of the code.
$data['code'] = (new NodePrinter)->prettyPrint([$data['ast']]);
}
/**
* Parses the closure's code and produces an abstract syntax tree (AST).
*
* @param array $data
*
* @throws ClosureAnalysisException if there is an issue finding the closure
*/
private function locateClosure(array &$data)
{
try {
$locator = new ClosureLocatorVisitor($data['reflection']);
$fileAst = $this->getFileAst($data['reflection']);
$fileTraverser = new NodeTraverser;
$fileTraverser->addVisitor(new NameResolver);
$fileTraverser->addVisitor($locator);
$fileTraverser->traverse($fileAst);
} catch (ParserError $e) {
// @codeCoverageIgnoreStart
throw new ClosureAnalysisException(
'There was an error analyzing the closure code.', 0, $e
);
// @codeCoverageIgnoreEnd
}
$data['ast'] = $locator->closureNode;
if (!$data['ast']) {
// @codeCoverageIgnoreStart
throw new ClosureAnalysisException(
'The closure was not found within the abstract syntax tree.'
);
// @codeCoverageIgnoreEnd
}
$data['location'] = $locator->location;
}
/**
* Returns the variables that in the "use" clause of the closure definition.
* These are referred to as the "used variables", "static variables", or
* "closed upon variables", "context" of the closure.
*
* @param array $data
*/
protected function determineContext(array &$data)
{
// Get the variable names defined in the AST
$refs = 0;
$vars = array_map(function ($node) use (&$refs) {
if ($node->byRef) {
$refs++;
}
return $node->var;
}, $data['ast']->uses);
$data['hasRefs'] = ($refs > 0);
// Get the variable names and values using reflection
$values = $data['reflection']->getStaticVariables();
// Combine the names and values to create the canonical context.
foreach ($vars as $name) {
if (isset($values[$name])) {
$data['context'][$name] = $values[$name];
}
}
}
/**
* @param \ReflectionFunction $reflection
*
* @throws ClosureAnalysisException
*
* @return \PhpParser\Node[]
*/
private function getFileAst(\ReflectionFunction $reflection)
{
$fileName = $reflection->getFileName();
if (!file_exists($fileName)) {
throw new ClosureAnalysisException(
"The file containing the closure, \"{$fileName}\" did not exist."
);
}
return $this->getParser()->parse(file_get_contents($fileName));
}
/**
* @return CodeParser
*/
private function getParser()
{
if (class_exists('PhpParser\ParserFactory')) {
return (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
}
return new CodeParser(new EmulativeLexer);
}
}

View File

@@ -1,68 +0,0 @@
<?php namespace SuperClosure\Analyzer;
use SuperClosure\Exception\ClosureAnalysisException;
abstract class ClosureAnalyzer
{
/**
* Analyzer a given closure.
*
* @param \Closure $closure
*
* @throws ClosureAnalysisException
*
* @return array
*/
public function analyze(\Closure $closure)
{
$data = [
'reflection' => new \ReflectionFunction($closure),
'code' => null,
'hasThis' => false,
'context' => [],
'hasRefs' => false,
'binding' => null,
'scope' => null,
'isStatic' => $this->isClosureStatic($closure),
];
$this->determineCode($data);
$this->determineContext($data);
$this->determineBinding($data);
return $data;
}
abstract protected function determineCode(array &$data);
/**
* Returns the variables that are in the "use" clause of the closure.
*
* These variables are referred to as the "used variables", "static
* variables", "closed upon variables", or "context" of the closure.
*
* @param array $data
*/
abstract protected function determineContext(array &$data);
private function determineBinding(array &$data)
{
$data['binding'] = $data['reflection']->getClosureThis();
if ($scope = $data['reflection']->getClosureScopeClass()) {
$data['scope'] = $scope->getName();
}
}
private function isClosureStatic(\Closure $closure)
{
$closure = @$closure->bindTo(new \stdClass);
if ($closure === null) {
return true;
}
$rebound = new \ReflectionFunction($closure);
return $rebound->getClosureThis() === null;
}
}

View File

@@ -1,70 +0,0 @@
<?php namespace SuperClosure\Analyzer;
/**
* A Token object represents and individual token parsed from PHP code.
*
* Each Token object is a normalized token created from the result of the
* `get_token_all()`. function, which is part of PHP's tokenizer.
*
* @link http://us2.php.net/manual/en/tokens.php
*/
class Token
{
/**
* @var string The token name. Always null for literal tokens.
*/
public $name;
/**
* @var int|null The token's integer value. Always null for literal tokens.
*/
public $value;
/**
* @var string The PHP code of the token.
*/
public $code;
/**
* @var int|null The line number of the token in the original code.
*/
public $line;
/**
* Constructs a token object.
*
* @param string $code
* @param int|null $value
* @param int|null $line
*
* @throws \InvalidArgumentException
*/
public function __construct($code, $value = null, $line = null)
{
if (is_array($code)) {
list($value, $code, $line) = array_pad($code, 3, null);
}
$this->code = $code;
$this->value = $value;
$this->line = $line;
$this->name = $value ? token_name($value) : null;
}
/**
* Determines if the token's value/code is equal to the specified value.
*
* @param mixed $value The value to check.
*
* @return bool True if the token is equal to the value.
*/
public function is($value)
{
return ($this->code === $value || $this->value === $value);
}
public function __toString()
{
return $this->code;
}
}

View File

@@ -1,118 +0,0 @@
<?php namespace SuperClosure\Analyzer;
use SuperClosure\Exception\ClosureAnalysisException;
/**
* This is the token based analyzer.
*
* We're using Uses reflection and tokenization to analyze a closure and
* determine its code and context. This is much faster than the AST based
* implementation.
*/
class TokenAnalyzer extends ClosureAnalyzer
{
public function determineCode(array &$data)
{
$this->determineTokens($data);
$data['code'] = implode('', $data['tokens']);
$data['hasThis'] = (strpos($data['code'], '$this') !== false);
}
private function determineTokens(array &$data)
{
$potential = $this->determinePotentialTokens($data['reflection']);
$braceLevel = $index = $step = $insideUse = 0;
$data['tokens'] = $data['context'] = [];
foreach ($potential as $token) {
$token = new Token($token);
switch ($step) {
// Handle tokens before the function declaration.
case 0:
if ($token->is(T_FUNCTION)) {
$data['tokens'][] = $token;
$step++;
}
break;
// Handle tokens inside the function signature.
case 1:
$data['tokens'][] = $token;
if ($insideUse) {
if ($token->is(T_VARIABLE)) {
$varName = trim($token, '$ ');
$data['context'][$varName] = null;
} elseif ($token->is('&')) {
$data['hasRefs'] = true;
}
} elseif ($token->is(T_USE)) {
$insideUse++;
}
if ($token->is('{')) {
$step++;
$braceLevel++;
}
break;
// Handle tokens inside the function body.
case 2:
$data['tokens'][] = $token;
if ($token->is('{')) {
$braceLevel++;
} elseif ($token->is('}')) {
$braceLevel--;
if ($braceLevel === 0) {
$step++;
}
}
break;
// Handle tokens after the function declaration.
case 3:
if ($token->is(T_FUNCTION)) {
throw new ClosureAnalysisException('Multiple closures '
. 'were declared on the same line of code. Could not '
. 'determine which closure was the intended target.'
);
}
break;
}
}
}
private function determinePotentialTokens(\ReflectionFunction $reflection)
{
// Load the file containing the code for the function.
$fileName = $reflection->getFileName();
if (!is_readable($fileName)) {
throw new ClosureAnalysisException(
"Cannot read the file containing the closure: \"{$fileName}\"."
);
}
$code = '';
$file = new \SplFileObject($fileName);
$file->seek($reflection->getStartLine() - 1);
while ($file->key() < $reflection->getEndLine()) {
$code .= $file->current();
$file->next();
}
$code = trim($code);
if (strpos($code, '<?php') !== 0) {
$code = "<?php\n" . $code;
}
return token_get_all($code);
}
protected function determineContext(array &$data)
{
// Get the values of the variables that are closed upon in "use".
$values = $data['reflection']->getStaticVariables();
// Construct the context by combining the variable names and values.
foreach ($data['context'] as $name => &$value) {
if (isset($values[$name])) {
$value = $values[$name];
}
}
}
}

View File

@@ -1,120 +0,0 @@
<?php namespace SuperClosure\Analyzer\Visitor;
use SuperClosure\Exception\ClosureAnalysisException;
use PhpParser\Node\Stmt\Namespace_ as NamespaceNode;
use PhpParser\Node\Stmt\Trait_ as TraitNode;
use PhpParser\Node\Stmt\Class_ as ClassNode;
use PhpParser\Node\Expr\Closure as ClosureNode;
use PhpParser\Node as AstNode;
use PhpParser\NodeVisitorAbstract as NodeVisitor;
/**
* This is a visitor that extends the nikic/php-parser library and looks for a
* closure node and its location.
*
* @internal
*/
final class ClosureLocatorVisitor extends NodeVisitor
{
/**
* @var \ReflectionFunction
*/
private $reflection;
/**
* @var ClosureNode
*/
public $closureNode;
/**
* @var array
*/
public $location;
/**
* @param \ReflectionFunction $reflection
*/
public function __construct($reflection)
{
$this->reflection = $reflection;
$this->location = [
'class' => null,
'directory' => dirname($this->reflection->getFileName()),
'file' => $this->reflection->getFileName(),
'function' => $this->reflection->getName(),
'line' => $this->reflection->getStartLine(),
'method' => null,
'namespace' => null,
'trait' => null,
];
}
public function enterNode(AstNode $node)
{
// Determine information about the closure's location
if (!$this->closureNode) {
if ($node instanceof NamespaceNode) {
$namespace = ($node->name && is_array($node->name->parts))
? implode('\\', $node->name->parts)
: null;
$this->location['namespace'] = $namespace;
}
if ($node instanceof TraitNode) {
$this->location['trait'] = $node->name;
$this->location['class'] = null;
} elseif ($node instanceof ClassNode) {
$this->location['class'] = $node->name;
$this->location['trait'] = null;
}
}
// Locate the node of the closure
if ($node instanceof ClosureNode) {
if ($node->getAttribute('startLine') == $this->location['line']) {
if ($this->closureNode) {
$line = $this->location['file'] . ':' . $node->getAttribute('startLine');
throw new ClosureAnalysisException("Two closures were "
. "declared on the same line ({$line}) of code. Cannot "
. "determine which closure was the intended target.");
} else {
$this->closureNode = $node;
}
}
}
}
public function leaveNode(AstNode $node)
{
// Determine information about the closure's location
if (!$this->closureNode) {
if ($node instanceof NamespaceNode) {
$this->location['namespace'] = null;
}
if ($node instanceof TraitNode) {
$this->location['trait'] = null;
} elseif ($node instanceof ClassNode) {
$this->location['class'] = null;
}
}
}
public function afterTraverse(array $nodes)
{
if ($this->location['class']) {
$this->location['class'] = $this->location['namespace'] . '\\' . $this->location['class'];
$this->location['method'] = "{$this->location['class']}::{$this->location['function']}";
} elseif ($this->location['trait']) {
$this->location['trait'] = $this->location['namespace'] . '\\' . $this->location['trait'];
$this->location['method'] = "{$this->location['trait']}::{$this->location['function']}";
// If the closure was declared in a trait, then we will do a best
// effort guess on the name of the class that used the trait. It's
// actually impossible at this point to know for sure what it is.
if ($closureScope = $this->reflection->getClosureScopeClass()) {
$this->location['class'] = $closureScope ? $closureScope->getName() : null;
} elseif ($closureThis = $this->reflection->getClosureThis()) {
$this->location['class'] = get_class($closureThis);
}
}
}
}

View File

@@ -1,50 +0,0 @@
<?php namespace SuperClosure\Analyzer\Visitor;
use PhpParser\Node\Scalar\LNumber as NumberNode;
use PhpParser\Node\Scalar\String_ as StringNode;
use PhpParser\Node as AstNode;
use PhpParser\NodeVisitorAbstract as NodeVisitor;
/**
* This is a visitor that resolves magic constants (e.g., __FILE__) to their
* intended values within a closure's AST.
*
* @internal
*/
final class MagicConstantVisitor extends NodeVisitor
{
/**
* @var array
*/
private $location;
/**
* @param array $location
*/
public function __construct(array $location)
{
$this->location = $location;
}
public function leaveNode(AstNode $node)
{
switch ($node->getType()) {
case 'Scalar_MagicConst_Class' :
return new StringNode($this->location['class']);
case 'Scalar_MagicConst_Dir' :
return new StringNode($this->location['directory']);
case 'Scalar_MagicConst_File' :
return new StringNode($this->location['file']);
case 'Scalar_MagicConst_Function' :
return new StringNode($this->location['function']);
case 'Scalar_MagicConst_Line' :
return new NumberNode($node->getAttribute('startLine'));
case 'Scalar_MagicConst_Method' :
return new StringNode($this->location['method']);
case 'Scalar_MagicConst_Namespace' :
return new StringNode($this->location['namespace']);
case 'Scalar_MagicConst_Trait' :
return new StringNode($this->location['trait']);
}
}
}

View File

@@ -1,27 +0,0 @@
<?php namespace SuperClosure\Analyzer\Visitor;
use PhpParser\Node as AstNode;
use PhpParser\Node\Expr\Variable as VariableNode;
use PhpParser\NodeVisitorAbstract as NodeVisitor;
/**
* Detects if the closure's AST contains a $this variable.
*
* @internal
*/
final class ThisDetectorVisitor extends NodeVisitor
{
/**
* @var bool
*/
public $detected = false;
public function leaveNode(AstNode $node)
{
if ($node instanceof VariableNode) {
if ($node->name === 'this') {
$this->detected = true;
}
}
}
}

View File

@@ -1,9 +0,0 @@
<?php namespace SuperClosure\Exception;
/**
* This exception is thrown when there is a problem analyzing a closure.
*/
class ClosureAnalysisException extends \RuntimeException implements SuperClosureException
{
//
}

View File

@@ -1,9 +0,0 @@
<?php namespace SuperClosure\Exception;
/**
* This exception is thrown when there is a problem unserializing a closure.
*/
class ClosureUnserializationException extends \RuntimeException implements SuperClosureException
{
//
}

View File

@@ -1,9 +0,0 @@
<?php namespace SuperClosure\Exception;
/**
* This is a marker exception for the SuperClosure library.
*/
interface SuperClosureException
{
//
}

View File

@@ -1,217 +0,0 @@
<?php namespace SuperClosure;
use Closure;
use SuperClosure\Exception\ClosureUnserializationException;
/**
* This class acts as a wrapper for a closure, and allows it to be serialized.
*
* 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.
*/
class SerializableClosure implements \Serializable
{
/**
* The closure being wrapped for serialization.
*
* @var Closure
*/
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
*/
public function __construct(
\Closure $closure,
SerializerInterface $serializer = null
) {
$this->closure = $closure;
$this->serializer = $serializer ?: new Serializer;
}
/**
* Return the original closure object.
*
* @return Closure
*/
public function getClosure()
{
return $this->closure;
}
/**
* 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()
{
return call_user_func_array($this->closure, func_get_args());
}
/**
* 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.
*
* @return SerializableClosure
* @link http://www.php.net/manual/en/closure.bindto.php
*/
public function bindTo($newthis, $newscope = 'static')
{
return new self(
$this->closure->bindTo($newthis, $newscope),
$this->serializer
);
}
/**
* Serializes the code, context, and binding of the closure.
*
* @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);
} 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.
*
* @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']
);
}
}
/**
* Returns closure data for `var_dump()`.
*
* @return array
*/
public function __debugInfo()
{
return $this->data ?: $this->serializer->getData($this->closure, true);
}
}
/**
* 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;
}

View File

@@ -1,214 +0,0 @@
<?php namespace SuperClosure;
use SuperClosure\Analyzer\AstAnalyzer as DefaultAnalyzer;
use SuperClosure\Analyzer\ClosureAnalyzer;
use SuperClosure\Exception\ClosureUnserializationException;
/**
* This is the serializer class used for serializing Closure objects.
*
* We're abstracting away all the details, impossibilities, and scary things
* that happen within.
*/
class Serializer implements SerializerInterface
{
/**
* The special value marking a recursive reference to a closure.
*
* @var string
*/
const RECURSION = "{{RECURSION}}";
/**
* The keys of closure data required for serialization.
*
* @var array
*/
private static $dataToKeep = [
'code' => true,
'context' => true,
'binding' => true,
'scope' => true,
'isStatic' => true,
];
/**
* The closure analyzer instance.
*
* @var ClosureAnalyzer
*/
private $analyzer;
/**
* The HMAC key to sign serialized closures.
*
* @var string
*/
private $signingKey;
/**
* Create a new serializer instance.
*
* @param ClosureAnalyzer|null $analyzer Closure analyzer instance.
* @param string|null $signingKey HMAC key to sign closure data.
*/
public function __construct(
ClosureAnalyzer $analyzer = null,
$signingKey = null
) {
$this->analyzer = $analyzer ?: new DefaultAnalyzer;
$this->signingKey = $signingKey;
}
/**
* @inheritDoc
*/
public function serialize(\Closure $closure)
{
$serialized = serialize(new SerializableClosure($closure, $this));
if ($this->signingKey) {
$signature = $this->calculateSignature($serialized);
$serialized = '%' . base64_encode($signature) . $serialized;
}
return $serialized;
}
/**
* @inheritDoc
*/
public function unserialize($serialized)
{
// Strip off the signature from the front of the string.
$signature = null;
if ($serialized[0] === '%') {
$signature = base64_decode(substr($serialized, 1, 44));
$serialized = substr($serialized, 45);
}
// If a key was provided, then verify the signature.
if ($this->signingKey) {
$this->verifySignature($signature, $serialized);
}
set_error_handler(function () {});
$unserialized = unserialize($serialized);
restore_error_handler();
if ($unserialized === false) {
throw new ClosureUnserializationException(
'The closure could not be unserialized.'
);
} elseif (!$unserialized instanceof SerializableClosure) {
throw new ClosureUnserializationException(
'The closure did not unserialize to a SuperClosure.'
);
}
return $unserialized->getClosure();
}
/**
* @inheritDoc
*/
public function getData(\Closure $closure, $forSerialization = false)
{
// Use the closure analyzer to get data about the closure.
$data = $this->analyzer->analyze($closure);
// If the closure data is getting retrieved solely for the purpose of
// serializing the closure, then make some modifications to the data.
if ($forSerialization) {
// If there is no reference to the binding, don't serialize it.
if (!$data['hasThis']) {
$data['binding'] = null;
}
// Remove data about the closure that does not get serialized.
$data = array_intersect_key($data, self::$dataToKeep);
// Wrap any other closures within the context.
foreach ($data['context'] as &$value) {
if ($value instanceof \Closure) {
$value = ($value === $closure)
? self::RECURSION
: new SerializableClosure($value, $this);
}
}
}
return $data;
}
/**
* Recursively traverses and wraps all Closure objects within the value.
*
* NOTE: THIS MAY NOT WORK IN ALL USE CASES, SO USE AT YOUR OWN RISK.
*
* @param mixed $data Any variable that contains closures.
* @param SerializerInterface $serializer The serializer to use.
*/
public static function wrapClosures(&$data, SerializerInterface $serializer)
{
if ($data instanceof \Closure) {
// Handle and wrap closure objects.
$reflection = new \ReflectionFunction($data);
if ($binding = $reflection->getClosureThis()) {
self::wrapClosures($binding, $serializer);
$scope = $reflection->getClosureScopeClass();
$scope = $scope ? $scope->getName() : 'static';
$data = $data->bindTo($binding, $scope);
}
$data = new SerializableClosure($data, $serializer);
} elseif (is_array($data) || $data instanceof \stdClass || $data instanceof \Traversable) {
// Handle members of traversable values.
foreach ($data as &$value) {
self::wrapClosures($value, $serializer);
}
} elseif (is_object($data) && !$data instanceof \Serializable) {
// Handle objects that are not already explicitly serializable.
$reflection = new \ReflectionObject($data);
if (!$reflection->hasMethod('__sleep')) {
foreach ($reflection->getProperties() as $property) {
if ($property->isPrivate() || $property->isProtected()) {
$property->setAccessible(true);
}
$value = $property->getValue($data);
self::wrapClosures($value, $serializer);
$property->setValue($data, $value);
}
}
}
}
/**
* Calculates a signature for a closure's serialized data.
*
* @param string $data Serialized closure data.
*
* @return string Signature of the closure's data.
*/
private function calculateSignature($data)
{
return hash_hmac('sha256', $data, $this->signingKey, true);
}
/**
* Verifies the signature for a closure's serialized data.
*
* @param string $signature The provided signature of the data.
* @param string $data The data for which to verify the signature.
*
* @throws ClosureUnserializationException if the signature is invalid.
*/
private function verifySignature($signature, $data)
{
// Verify that the provided signature matches the calculated signature.
if (!hash_equals($signature, $this->calculateSignature($data))) {
throw new ClosureUnserializationException('The signature of the'
. ' closure\'s data is invalid, which means the serialized '
. 'closure has been modified and is unsafe to unserialize.'
);
}
}
}

View File

@@ -1,45 +0,0 @@
<?php namespace SuperClosure;
use SuperClosure\Exception\ClosureUnserializationException;
/**
* Interface for a serializer that is used to serialize Closure objects.
*/
interface SerializerInterface
{
/**
* Takes a Closure object, decorates it with a SerializableClosure object,
* then performs the serialization.
*
* @param \Closure $closure Closure to serialize.
*
* @return string Serialized closure.
*/
public function serialize(\Closure $closure);
/**
* Takes a serialized closure, performs the unserialization, and then
* extracts and returns a the Closure object.
*
* @param string $serialized Serialized closure.
*
* @throws ClosureUnserializationException if unserialization fails.
* @return \Closure Unserialized closure.
*/
public function unserialize($serialized);
/**
* Retrieves data about a closure including its code, context, and binding.
*
* The data returned is dependant on the `ClosureAnalyzer` implementation
* used and whether the `$forSerialization` parameter is set to true. If
* `$forSerialization` is true, then only data relevant to serializing the
* closure is returned.
*
* @param \Closure $closure Closure to analyze.
* @param bool $forSerialization Include only serialization data.
*
* @return \Closure
*/
public function getData(\Closure $closure, $forSerialization = false);
}